From f5ccc309a559c6b79e84fc0e55c8ffedc4dd420f Mon Sep 17 00:00:00 2001 From: Laura Whitaker Date: Tue, 6 Aug 2024 18:32:06 -0600 Subject: [PATCH 01/13] Refactor FilterSearch to component for reuse on Schedules page --- .../filter-search/boolean-filter.svelte | 0 .../filter-search/close-filter-button.svelte | 0 .../filter-search/conditional-menu.svelte | 0 .../filter-search/datetime-filter.svelte | 0 .../filter-search/duration-filter.svelte | 0 .../filter-search/filter-list.svelte | 18 +- src/lib/components/filter-search/index.svelte | 242 +++++++++++++++++ .../filter-search/list-filter.svelte | 0 .../filter-search/number-filter.svelte | 0 .../search-attribute-menu.svelte | 20 +- .../filter-search/status-filter.svelte | 37 ++- .../filter-search/text-filter.svelte | 0 .../dropdown-filter/text-filter.svelte | 4 +- .../workflow-datetime-filter.svelte | 6 +- .../dropdown-filter/workflow-status.svelte | 6 +- .../workflow/filter-search/index.svelte | 256 ++---------------- .../filterable-table-cell.svelte | 4 +- src/lib/i18n/locales/en/schedules.ts | 3 +- src/lib/models/search-attribute-filters.ts | 14 + src/lib/models/workflow-filters.ts | 16 +- src/lib/pages/schedules.svelte | 133 +++++---- src/lib/services/schedule-service.ts | 7 +- src/lib/stores/filters.ts | 24 +- .../utilities/query/filter-workflow-query.ts | 6 +- .../query/to-list-workflow-filters.ts | 14 +- 25 files changed, 436 insertions(+), 374 deletions(-) rename src/lib/components/{workflow => }/filter-search/boolean-filter.svelte (100%) rename src/lib/components/{workflow => }/filter-search/close-filter-button.svelte (100%) rename src/lib/components/{workflow => }/filter-search/conditional-menu.svelte (100%) rename src/lib/components/{workflow => }/filter-search/datetime-filter.svelte (100%) rename src/lib/components/{workflow => }/filter-search/duration-filter.svelte (100%) rename src/lib/components/{workflow => }/filter-search/filter-list.svelte (90%) create mode 100644 src/lib/components/filter-search/index.svelte rename src/lib/components/{workflow => }/filter-search/list-filter.svelte (100%) rename src/lib/components/{workflow => }/filter-search/number-filter.svelte (100%) rename src/lib/components/{workflow => }/filter-search/search-attribute-menu.svelte (83%) rename src/lib/components/{workflow => }/filter-search/status-filter.svelte (76%) rename src/lib/components/{workflow => }/filter-search/text-filter.svelte (100%) create mode 100644 src/lib/models/search-attribute-filters.ts diff --git a/src/lib/components/workflow/filter-search/boolean-filter.svelte b/src/lib/components/filter-search/boolean-filter.svelte similarity index 100% rename from src/lib/components/workflow/filter-search/boolean-filter.svelte rename to src/lib/components/filter-search/boolean-filter.svelte diff --git a/src/lib/components/workflow/filter-search/close-filter-button.svelte b/src/lib/components/filter-search/close-filter-button.svelte similarity index 100% rename from src/lib/components/workflow/filter-search/close-filter-button.svelte rename to src/lib/components/filter-search/close-filter-button.svelte diff --git a/src/lib/components/workflow/filter-search/conditional-menu.svelte b/src/lib/components/filter-search/conditional-menu.svelte similarity index 100% rename from src/lib/components/workflow/filter-search/conditional-menu.svelte rename to src/lib/components/filter-search/conditional-menu.svelte diff --git a/src/lib/components/workflow/filter-search/datetime-filter.svelte b/src/lib/components/filter-search/datetime-filter.svelte similarity index 100% rename from src/lib/components/workflow/filter-search/datetime-filter.svelte rename to src/lib/components/filter-search/datetime-filter.svelte diff --git a/src/lib/components/workflow/filter-search/duration-filter.svelte b/src/lib/components/filter-search/duration-filter.svelte similarity index 100% rename from src/lib/components/workflow/filter-search/duration-filter.svelte rename to src/lib/components/filter-search/duration-filter.svelte diff --git a/src/lib/components/workflow/filter-search/filter-list.svelte b/src/lib/components/filter-search/filter-list.svelte similarity index 90% rename from src/lib/components/workflow/filter-search/filter-list.svelte rename to src/lib/components/filter-search/filter-list.svelte index 352fc47f4..dfe6563de 100644 --- a/src/lib/components/workflow/filter-search/filter-list.svelte +++ b/src/lib/components/filter-search/filter-list.svelte @@ -9,8 +9,8 @@ import Button from '$lib/holocene/button.svelte'; import Chip from '$lib/holocene/chip.svelte'; import { translate } from '$lib/i18n/translate'; + import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { isWorkflowStatusType } from '$lib/models/workflow-status'; - import { workflowFilters } from '$lib/stores/filters'; import { relativeTime, timeFormat, @@ -27,16 +27,18 @@ import { FILTER_CONTEXT, type FilterContext } from './index.svelte'; + export let filters: SearchAttributeFilter[]; + const { filter, activeQueryIndex } = getContext(FILTER_CONTEXT); const removeQuery = (index: number) => { - $workflowFilters.splice(index, 1); - $workflowFilters = $workflowFilters; - updateQueryParamsFromFilter($page.url, $workflowFilters); + filters.splice(index, 1); + filters = filters; + updateQueryParamsFromFilter($page.url, filters); - if (index === $workflowFilters.length) { - const previousQuery = $workflowFilters[$workflowFilters.length - 1]; + if (index === filters.length) { + const previousQuery = filters[filters.length - 1]; if (previousQuery) { previousQuery.operator = ''; } @@ -52,8 +54,8 @@ let totalFiltersInView = 5; - $: visibleFilters = $workflowFilters.slice(0, totalFiltersInView); - $: hasMoreFilters = totalFiltersInView < $workflowFilters.length; + $: visibleFilters = filters.slice(0, totalFiltersInView); + $: hasMoreFilters = totalFiltersInView < filters.length; const viewMoreFilters = () => { if (hasMoreFilters) { diff --git a/src/lib/components/filter-search/index.svelte b/src/lib/components/filter-search/index.svelte new file mode 100644 index 000000000..0fb13b571 --- /dev/null +++ b/src/lib/components/filter-search/index.svelte @@ -0,0 +1,242 @@ + + + + +
+
+ + {#if showFilter} +
+ {#if isStatusFilter(attribute)} + + {:else} + + {/if} + + {#if attribute} + {#if isTextFilter({ attribute, type })} +
+ + +
+ + + {:else if isDurationFilter(attribute)} +
+ + +
+ {:else if isNumberFilter({ attribute, type })} +
+ + +
+ {:else if isDateTimeFilter({ attribute, type })} +
+ + +
+ {:else if isBooleanFilter({ attribute, type })} +
+ + +
+ {/if} + {/if} +
+ {/if} +
+ {#if showClearAllButton} + + {/if} +
+ +
+ +
+ + diff --git a/src/lib/components/workflow/filter-search/list-filter.svelte b/src/lib/components/filter-search/list-filter.svelte similarity index 100% rename from src/lib/components/workflow/filter-search/list-filter.svelte rename to src/lib/components/filter-search/list-filter.svelte diff --git a/src/lib/components/workflow/filter-search/number-filter.svelte b/src/lib/components/filter-search/number-filter.svelte similarity index 100% rename from src/lib/components/workflow/filter-search/number-filter.svelte rename to src/lib/components/filter-search/number-filter.svelte diff --git a/src/lib/components/workflow/filter-search/search-attribute-menu.svelte b/src/lib/components/filter-search/search-attribute-menu.svelte similarity index 83% rename from src/lib/components/workflow/filter-search/search-attribute-menu.svelte rename to src/lib/components/filter-search/search-attribute-menu.svelte index 471141412..d41f689c2 100644 --- a/src/lib/components/workflow/filter-search/search-attribute-menu.svelte +++ b/src/lib/components/filter-search/search-attribute-menu.svelte @@ -10,9 +10,8 @@ MenuItem, } from '$lib/holocene/menu'; import { translate } from '$lib/i18n/translate'; - import type { WorkflowFilter } from '$lib/models/workflow-filters'; - import { workflowFilters } from '$lib/stores/filters'; - import { sortedSearchAttributeOptions } from '$lib/stores/search-attributes'; + import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; + import type { SearchAttributeOption } from '$lib/stores/search-attributes'; import type { SearchAttributesValue } from '$lib/types/workflows'; import { getFocusedElementId, @@ -22,10 +21,13 @@ import { FILTER_CONTEXT, type FilterContext } from './index.svelte'; + export let filters: SearchAttributeFilter[]; + export let options: SearchAttributeOption[]; + const { filter, activeQueryIndex, focusedElementId } = getContext(FILTER_CONTEXT); - function isOptionDisabled(value: string, filters: WorkflowFilter[]) { + function isOptionDisabled(value: string, filters: SearchAttributeFilter[]) { return filters.some( (filter) => (filter.conditional === '=' || filter.conditional === '!=') && @@ -41,13 +43,13 @@ let searchAttributeValue = ''; // TODO: Add KeywordList support - $: options = $sortedSearchAttributeOptions.filter( + $: _options = options.filter( ({ value, type }) => !isListFilter({ attribute: value, type }), ); $: filteredOptions = !searchAttributeValue - ? options - : options.filter((option) => + ? _options + : _options.filter((option) => option.value.toLowerCase().includes(searchAttributeValue.toLowerCase()), ); @@ -56,7 +58,7 @@ (searchAttributeValue = '')} class="text-nowrap {!!$filter.attribute && 'attribute-selected'}" > @@ -88,7 +90,7 @@ {#each filteredOptions as { value, label, type }} - {@const disabled = isOptionDisabled(value, $workflowFilters)} + {@const disabled = isOptionDisabled(value, filters)} { handleNewQuery(value, type); diff --git a/src/lib/components/workflow/filter-search/status-filter.svelte b/src/lib/components/filter-search/status-filter.svelte similarity index 76% rename from src/lib/components/workflow/filter-search/status-filter.svelte rename to src/lib/components/filter-search/status-filter.svelte index bbc2e68d1..b693e2706 100644 --- a/src/lib/components/workflow/filter-search/status-filter.svelte +++ b/src/lib/components/filter-search/status-filter.svelte @@ -15,24 +15,25 @@ MenuItem, } from '$lib/holocene/menu'; import Translate from '$lib/i18n/translate.svelte'; - import type { WorkflowFilter } from '$lib/models/workflow-filters'; + import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { workflowStatusFilters } from '$lib/models/workflow-status'; - import { workflowFilters } from '$lib/stores/filters'; import { isStatusFilter } from '$lib/utilities/query/filter-search'; import { updateQueryParamsFromFilter } from '$lib/utilities/query/to-list-workflow-filters'; import { FILTER_CONTEXT, type FilterContext } from './index.svelte'; + export let filters: SearchAttributeFilter[]; + const { filter, resetFilter } = getContext(FILTER_CONTEXT); const open = writable(true); - $: filters = [...$workflowFilters]; - $: statusFilters = filters.filter((filter) => + $: _filters = [...filters]; + $: statusFilters = _filters.filter((filter) => isStatusFilter(filter.attribute), ); function apply() { - $workflowFilters = filters; - updateQueryParamsFromFilter($page.url, $workflowFilters); + filters = _filters; + updateQueryParamsFromFilter($page.url, filters); } function mapStatusToFilter(value: string) { @@ -42,12 +43,12 @@ }; } - function mapStatusesToFilters(filters: WorkflowFilter[]) { - if (filters.length === 1) { - return [mapStatusToFilter(filters[0].value)]; + function mapStatusesToFilters(_filters: SearchAttributeFilter[]) { + if (_filters.length === 1) { + return [mapStatusToFilter(_filters[0].value)]; } else { - return filters.map((filter, i) => { - const operator = i === filters.length - 1 ? '' : 'OR'; + return _filters.map((filter, i) => { + const operator = i === _filters.length - 1 ? '' : 'OR'; return { ...filter, operator, @@ -58,14 +59,12 @@ const onStatusClick = (status: string) => { if (status === 'All') { - filters = $workflowFilters.filter( - (f) => f.attribute !== 'ExecutionStatus', - ); + _filters = filters.filter((f) => f.attribute !== 'ExecutionStatus'); } else if (statusFilters.find((s) => s.value === status)) { - const nonStatusFilters = $workflowFilters.filter( + const nonStatusFilters = filters.filter( (f) => !isStatusFilter(f.attribute), ); - filters = [ + _filters = [ ...nonStatusFilters, ...mapStatusesToFilters( statusFilters.filter((s) => s.value !== status), @@ -73,12 +72,12 @@ ]; } else { if (!statusFilters.length) { - filters = [...filters, mapStatusToFilter(status)]; + _filters = [..._filters, mapStatusToFilter(status)]; } else { - const nonStatusFilters = filters.filter( + const nonStatusFilters = _filters.filter( (f) => !isStatusFilter(f.attribute), ); - filters = [ + _filters = [ ...nonStatusFilters, ...mapStatusesToFilters([ ...statusFilters, diff --git a/src/lib/components/workflow/filter-search/text-filter.svelte b/src/lib/components/filter-search/text-filter.svelte similarity index 100% rename from src/lib/components/workflow/filter-search/text-filter.svelte rename to src/lib/components/filter-search/text-filter.svelte diff --git a/src/lib/components/workflow/dropdown-filter/text-filter.svelte b/src/lib/components/workflow/dropdown-filter/text-filter.svelte index 7c69f7e2d..a186762fe 100644 --- a/src/lib/components/workflow/dropdown-filter/text-filter.svelte +++ b/src/lib/components/workflow/dropdown-filter/text-filter.svelte @@ -5,11 +5,11 @@ import Input from '$lib/holocene/input/input.svelte'; import { Menu, MenuButton, MenuContainer } from '$lib/holocene/menu'; import { translate } from '$lib/i18n/translate'; + import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { attributeToHumanReadable, attributeToId, type TextFilterAttributes, - type WorkflowFilter, } from '$lib/models/workflow-filters'; import { workflowFilters } from '$lib/stores/filters'; import { updateQueryParamsFromFilter } from '$lib/utilities/query/to-list-workflow-filters'; @@ -25,7 +25,7 @@ const onInput = (e: Event) => { const { value } = e.target as HTMLInputElement; if (value) { - const filter: WorkflowFilter = { + const filter: SearchAttributeFilter = { attribute, type: 'Keyword', value, diff --git a/src/lib/components/workflow/dropdown-filter/workflow-datetime-filter.svelte b/src/lib/components/workflow/dropdown-filter/workflow-datetime-filter.svelte index 99fc72c5c..8ab40b25a 100644 --- a/src/lib/components/workflow/dropdown-filter/workflow-datetime-filter.svelte +++ b/src/lib/components/workflow/dropdown-filter/workflow-datetime-filter.svelte @@ -21,7 +21,7 @@ import MenuDivider from '$lib/holocene/menu/menu-divider.svelte'; import TimePicker from '$lib/holocene/time-picker.svelte'; import { translate } from '$lib/i18n/translate'; - import type { WorkflowFilter } from '$lib/models/workflow-filters'; + import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { supportsAdvancedVisibility } from '$lib/stores/advanced-visibility'; import { workflowFilters } from '$lib/stores/filters'; import { getLocalTime } from '$lib/utilities/format-date'; @@ -80,7 +80,7 @@ } else if (value === 'Custom') { custom = true; } else { - const filter: WorkflowFilter = { + const filter: SearchAttributeFilter = { attribute: timeField, type: 'Datetime', value, @@ -154,7 +154,7 @@ )}"` : `> "${formatISO(startDateWithTime)}"`; - const filter: WorkflowFilter = { + const filter: SearchAttributeFilter = { attribute: timeField, type: 'Datetime', value: query, diff --git a/src/lib/components/workflow/dropdown-filter/workflow-status.svelte b/src/lib/components/workflow/dropdown-filter/workflow-status.svelte index 978d8ba56..c9743a6fe 100644 --- a/src/lib/components/workflow/dropdown-filter/workflow-status.svelte +++ b/src/lib/components/workflow/dropdown-filter/workflow-status.svelte @@ -12,7 +12,7 @@ } from '$lib/holocene/menu'; import { translate } from '$lib/i18n/translate'; import Translate from '$lib/i18n/translate.svelte'; - import type { WorkflowFilter } from '$lib/models/workflow-filters'; + import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { workflowStatusFilters } from '$lib/models/workflow-status'; import { workflowFilters } from '$lib/stores/filters'; import { updateQueryParamsFromFilter } from '$lib/utilities/query/to-list-workflow-filters'; @@ -21,7 +21,7 @@ (f) => f.attribute === 'ExecutionStatus', ); - function mapStatusToFilter(value: string): WorkflowFilter { + function mapStatusToFilter(value: string): SearchAttributeFilter { return { attribute: 'ExecutionStatus', type: 'Keyword', @@ -32,7 +32,7 @@ }; } - function mapStatusesToFilters(filters: WorkflowFilter[]) { + function mapStatusesToFilters(filters: SearchAttributeFilter[]) { if (filters.length === 1) { return [mapStatusToFilter(filters[0].value)]; } else { diff --git a/src/lib/components/workflow/filter-search/index.svelte b/src/lib/components/workflow/filter-search/index.svelte index 6228f9b4d..39445c21d 100644 --- a/src/lib/components/workflow/filter-search/index.svelte +++ b/src/lib/components/workflow/filter-search/index.svelte @@ -1,27 +1,7 @@ - - -
-
- {#if $searchInputViewOpen} - - {:else} -
- {#if isStatusFilter(attribute)} - - {:else} - - {/if} - - {#if attribute} - {#if isTextFilter({ attribute, type })} -
- - -
- - - {:else if isDurationFilter(attribute)} -
- - -
- {:else if isNumberFilter({ attribute, type })} -
- - -
- {:else if isDateTimeFilter({ attribute, type })} -
- - -
- {:else if isBooleanFilter({ attribute, type })} -
- - -
- {/if} - {/if} -
- -
- {#if showClearAllButton} - - {/if} -
- {/if} + { + $refresh = Date.now(); + }} +> + {#if $searchInputViewOpen} + + {/if} + -
- -
- - + + diff --git a/src/lib/components/workflow/workflows-summary-configurable-table/filterable-table-cell.svelte b/src/lib/components/workflow/workflows-summary-configurable-table/filterable-table-cell.svelte index e3fdb61da..338cd42a7 100644 --- a/src/lib/components/workflow/workflows-summary-configurable-table/filterable-table-cell.svelte +++ b/src/lib/components/workflow/workflows-summary-configurable-table/filterable-table-cell.svelte @@ -4,10 +4,10 @@ import FilterOrCopyButtons from '$lib/holocene/filter-or-copy-buttons.svelte'; import Link from '$lib/holocene/link.svelte'; import { translate } from '$lib/i18n/translate'; + import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { searchAttributeToWorkflowKey, type TextFilterAttributes, - type WorkflowFilter, } from '$lib/models/workflow-filters'; import { workflowFilters } from '$lib/stores/filters'; import type { WorkflowExecution } from '$lib/types/workflows'; @@ -32,7 +32,7 @@ $workflowFilters.filter((f) => f.attribute !== attribute); if (!filter) { - const newFilter: WorkflowFilter = { + const newFilter: SearchAttributeFilter = { attribute, type: 'Keyword', value, diff --git a/src/lib/i18n/locales/en/schedules.ts b/src/lib/i18n/locales/en/schedules.ts index 2382b32b2..db142b7f1 100644 --- a/src/lib/i18n/locales/en/schedules.ts +++ b/src/lib/i18n/locales/en/schedules.ts @@ -13,7 +13,8 @@ export const Strings = { 'schedule-spec': 'Schedule Spec', 'schedule-input': 'Schedule Input', 'empty-state-title': 'No Schedules Found', - 'empty-state-description': 'Try a different search', + 'empty-state-description': + 'Try adjusting or clearing the filters to see the Schedules running on this Namespace.', 'error-message-fetching': 'Error fetching schedules', 'recent-runs': 'Recent Runs', 'recent-runs-empty-state-title': 'No Recent Runs', diff --git a/src/lib/models/search-attribute-filters.ts b/src/lib/models/search-attribute-filters.ts new file mode 100644 index 000000000..153fdd093 --- /dev/null +++ b/src/lib/models/search-attribute-filters.ts @@ -0,0 +1,14 @@ +import type { + SearchAttributes, + SearchAttributesValue, +} from '$lib/types/workflows'; + +export type SearchAttributeFilter = { + attribute: Extract; + type: SearchAttributesValue; + value: string; + operator: string; + parenthesis: string; + conditional: string; + customDate?: boolean; +}; diff --git a/src/lib/models/workflow-filters.ts b/src/lib/models/workflow-filters.ts index 1e006ea7f..583a4b558 100644 --- a/src/lib/models/workflow-filters.ts +++ b/src/lib/models/workflow-filters.ts @@ -1,8 +1,4 @@ -import type { - SearchAttributes, - SearchAttributesValue, - WorkflowExecution, -} from '$lib/types/workflows'; +import type { SearchAttributes, WorkflowExecution } from '$lib/types/workflows'; export type TextFilterAttributes = 'WorkflowId' | 'WorkflowType' | 'RunId'; export type TextFilterKeys = Extract< @@ -31,16 +27,6 @@ export const searchAttributeToWorkflowKey: Record< RunId: 'runId', }; -export type WorkflowFilter = { - attribute: Extract; - type: SearchAttributesValue; - value: string; - operator: string; - parenthesis: string; - conditional: string; - customDate?: boolean; -}; - export type SortOrder = 'asc' | 'desc'; export type WorkflowSort = { diff --git a/src/lib/pages/schedules.svelte b/src/lib/pages/schedules.svelte index 760d3c5fc..3c4cbebd5 100644 --- a/src/lib/pages/schedules.svelte +++ b/src/lib/pages/schedules.svelte @@ -1,29 +1,36 @@ -{#key namespace} +{#key [namespace, query, refresh]} - {#if !createDisabled && visibleItems.length} + {#if !createDisabled && (visibleItems.length || query)} - {/if} - +
+ {#if query} + + + + + + + + + + + {:else} + +

+ {translate('schedules.getting-started-docs-link-preface')} + {translate('schedules.getting-started-docs-link')} + {translate('schedules.getting-started-cli-link-preface')} + Temporal CLI. +

+ {#if !error && !createDisabled} + + {/if} +
+ {/if}
{/key} diff --git a/src/lib/services/schedule-service.ts b/src/lib/services/schedule-service.ts index 4ea51b71a..fbb053905 100644 --- a/src/lib/services/schedule-service.ts +++ b/src/lib/services/schedule-service.ts @@ -33,12 +33,17 @@ type PaginatedSchedulesPromise = ( export const fetchPaginatedSchedules = async ( namespace: string, + query: string, request = fetch, ): Promise => { return (pageSize = 100, token = '') => { const route = routeForApi('schedules', { namespace }); return requestFromAPI(route, { - params: { maximumPageSize: String(pageSize), nextPageToken: token }, + params: { + maximumPageSize: String(pageSize), + nextPageToken: token, + ...(query ? { query } : {}), + }, request, }).then(({ schedules, nextPageToken }) => { return { diff --git a/src/lib/stores/filters.ts b/src/lib/stores/filters.ts index 9d5330a9d..1072c2903 100644 --- a/src/lib/stores/filters.ts +++ b/src/lib/stores/filters.ts @@ -4,7 +4,7 @@ import { derived, get, writable } from 'svelte/store'; import { page } from '$app/stores'; import { allEventTypeOptions } from '$lib/models/event-history/get-event-categorization'; -import type { WorkflowFilter } from '$lib/models/workflow-filters'; +import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { persistStore } from '$lib/stores/persist-store'; import type { EventClassification, EventTypeCategory } from '$lib/types/events'; @@ -36,7 +36,9 @@ const parameters = derived( }, ); -const updateWorkflowFilters: StartStopNotifier = (set) => { +const updateWorkflowFilters: StartStopNotifier = ( + set, +) => { return parameters.subscribe(({ query }) => { if (!query && get(workflowFilters).length) { // Clear filters if there is no query @@ -51,11 +53,27 @@ export const searchInputViewOpen = persistStore( true, ); -export const workflowFilters = writable( +export const workflowFilters = writable( [], updateWorkflowFilters, ); +const updateScheduleFilters: StartStopNotifier = ( + set, +) => { + return parameters.subscribe(({ query }) => { + if (!query && get(scheduleFilters).length) { + // Clear filters if there is no query + set([]); + } + }); +}; + +export const scheduleFilters = writable( + [], + updateScheduleFilters, +); + const updateEventCategoryFilter: StartStopNotifier< EventTypeCategory[] | null > = (set) => { diff --git a/src/lib/utilities/query/filter-workflow-query.ts b/src/lib/utilities/query/filter-workflow-query.ts index 2af3b6c74..1baab1467 100644 --- a/src/lib/utilities/query/filter-workflow-query.ts +++ b/src/lib/utilities/query/filter-workflow-query.ts @@ -1,6 +1,6 @@ import { get } from 'svelte/store'; -import type { WorkflowFilter } from '$lib/models/workflow-filters'; +import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { supportsAdvancedVisibility } from '$lib/stores/advanced-visibility'; import type { SearchAttributes, @@ -89,7 +89,7 @@ const toFilterQueryStatement = ( }; const toQueryStatementsFromFilters = ( - filters: WorkflowFilter[], + filters: SearchAttributeFilter[], archived: boolean, ): string[] => { return filters @@ -128,7 +128,7 @@ const toQueryStatementsFromFilters = ( }; export const toListWorkflowQueryFromFilters = ( - filters: WorkflowFilter[] = [], + filters: SearchAttributeFilter[] = [], archived = false, ): string => { return toQueryStatementsFromFilters(filters, archived).join(''); diff --git a/src/lib/utilities/query/to-list-workflow-filters.ts b/src/lib/utilities/query/to-list-workflow-filters.ts index 8c22fa412..c76fdb188 100644 --- a/src/lib/utilities/query/to-list-workflow-filters.ts +++ b/src/lib/utilities/query/to-list-workflow-filters.ts @@ -1,6 +1,6 @@ import debounce from 'just-debounce'; -import type { WorkflowFilter } from '$lib/models/workflow-filters'; +import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters'; import { currentPageKey } from '$lib/stores/pagination'; import type { FilterParameters, SearchAttributes } from '$lib/types/workflows'; import { toListWorkflowQueryFromFilters } from '$lib/utilities/query/filter-workflow-query'; @@ -38,7 +38,7 @@ export const getLargestDurationUnit = (duration: Duration): Duration => { const isDatetimeStatement = is('Datetime'); const isBoolStatement = is('Bool'); -export const emptyFilter = (): WorkflowFilter => ({ +export const emptyFilter = (): SearchAttributeFilter => ({ attribute: '', type: 'Keyword', value: '', @@ -59,9 +59,9 @@ const DefaultAttributes: SearchAttributes = { export const toListWorkflowFilters = ( query: string, attributes: SearchAttributes = DefaultAttributes, -): WorkflowFilter[] => { +): SearchAttributeFilter[] => { const tokens = tokenize(query); - const filters: WorkflowFilter[] = []; + const filters: SearchAttributeFilter[] = []; let filter = emptyFilter(); if (!query) { @@ -127,7 +127,7 @@ export const toListWorkflowFilters = ( } }; -export const combineDropdownFilters = (filters: WorkflowFilter[]) => { +export const combineDropdownFilters = (filters: SearchAttributeFilter[]) => { const statusFilters = filters.filter( (f) => f.attribute === 'ExecutionStatus' && f.value, ); @@ -168,7 +168,7 @@ export const combineDropdownFilters = (filters: WorkflowFilter[]) => { ]; }; -export const combineFilters = (filters: WorkflowFilter[]) => { +export const combineFilters = (filters: SearchAttributeFilter[]) => { filters.forEach((filter, index) => { const previousFilter = filters[index - 1]; if (previousFilter && !previousFilter.operator) { @@ -198,7 +198,7 @@ export const combineFilters = (filters: WorkflowFilter[]) => { }; export const updateQueryParamsFromFilter = debounce( - (url: URL, filters: WorkflowFilter[], isDropdownFilter = false) => { + (url: URL, filters: SearchAttributeFilter[], isDropdownFilter = false) => { const allFilters = isDropdownFilter ? combineDropdownFilters(filters) : combineFilters(filters); From c9f8a6f05194a16c500c59c6dd9440c75cf5243d Mon Sep 17 00:00:00 2001 From: Laura Whitaker Date: Wed, 7 Aug 2024 17:42:55 -0600 Subject: [PATCH 02/13] Rename filter search to search attribute filter --- .../boolean-filter.svelte | 0 .../close-filter-button.svelte | 0 .../conditional-menu.svelte | 0 .../datetime-filter.svelte | 0 .../duration-filter.svelte | 4 ++-- .../filter-list.svelte | 2 +- .../{filter-search => search-attribute-filter}/index.svelte | 4 ++-- .../list-filter.svelte | 2 +- .../number-filter.svelte | 4 ++-- .../search-attribute-menu.svelte | 2 +- .../status-filter.svelte | 2 +- .../text-filter.svelte | 4 ++-- .../{filter-search => search-attribute-filter}/index.svelte | 6 +++--- src/lib/pages/schedules.svelte | 4 ++-- src/lib/pages/workflows-with-new-search.svelte | 4 ++-- ...ilter-search.test.ts => search-attribute-filter.test.ts} | 2 +- .../query/{filter-search.ts => search-attribute-filter.ts} | 2 +- ...ch.spec.ts => workflows-search-attribute-filter.spec.ts} | 0 18 files changed, 21 insertions(+), 21 deletions(-) rename src/lib/components/{filter-search => search-attribute-filter}/boolean-filter.svelte (100%) rename src/lib/components/{filter-search => search-attribute-filter}/close-filter-button.svelte (100%) rename src/lib/components/{filter-search => search-attribute-filter}/conditional-menu.svelte (100%) rename src/lib/components/{filter-search => search-attribute-filter}/datetime-filter.svelte (100%) rename src/lib/components/{filter-search => search-attribute-filter}/duration-filter.svelte (93%) rename src/lib/components/{filter-search => search-attribute-filter}/filter-list.svelte (98%) rename src/lib/components/{filter-search => search-attribute-filter}/index.svelte (99%) rename src/lib/components/{filter-search => search-attribute-filter}/list-filter.svelte (97%) rename src/lib/components/{filter-search => search-attribute-filter}/number-filter.svelte (91%) rename src/lib/components/{filter-search => search-attribute-filter}/search-attribute-menu.svelte (98%) rename src/lib/components/{filter-search => search-attribute-filter}/status-filter.svelte (97%) rename src/lib/components/{filter-search => search-attribute-filter}/text-filter.svelte (92%) rename src/lib/components/workflow/{filter-search => search-attribute-filter}/index.svelte (95%) rename src/lib/utilities/query/{filter-search.test.ts => search-attribute-filter.test.ts} (99%) rename src/lib/utilities/query/{filter-search.ts => search-attribute-filter.ts} (97%) rename tests/integration/{filter-search.spec.ts => workflows-search-attribute-filter.spec.ts} (100%) diff --git a/src/lib/components/filter-search/boolean-filter.svelte b/src/lib/components/search-attribute-filter/boolean-filter.svelte similarity index 100% rename from src/lib/components/filter-search/boolean-filter.svelte rename to src/lib/components/search-attribute-filter/boolean-filter.svelte diff --git a/src/lib/components/filter-search/close-filter-button.svelte b/src/lib/components/search-attribute-filter/close-filter-button.svelte similarity index 100% rename from src/lib/components/filter-search/close-filter-button.svelte rename to src/lib/components/search-attribute-filter/close-filter-button.svelte diff --git a/src/lib/components/filter-search/conditional-menu.svelte b/src/lib/components/search-attribute-filter/conditional-menu.svelte similarity index 100% rename from src/lib/components/filter-search/conditional-menu.svelte rename to src/lib/components/search-attribute-filter/conditional-menu.svelte diff --git a/src/lib/components/filter-search/datetime-filter.svelte b/src/lib/components/search-attribute-filter/datetime-filter.svelte similarity index 100% rename from src/lib/components/filter-search/datetime-filter.svelte rename to src/lib/components/search-attribute-filter/datetime-filter.svelte diff --git a/src/lib/components/filter-search/duration-filter.svelte b/src/lib/components/search-attribute-filter/duration-filter.svelte similarity index 93% rename from src/lib/components/filter-search/duration-filter.svelte rename to src/lib/components/search-attribute-filter/duration-filter.svelte index 9245ba40f..22191165c 100644 --- a/src/lib/components/filter-search/duration-filter.svelte +++ b/src/lib/components/search-attribute-filter/duration-filter.svelte @@ -33,11 +33,11 @@ }; - + diff --git a/src/lib/components/filter-search/number-filter.svelte b/src/lib/components/search-attribute-filter/number-filter.svelte similarity index 91% rename from src/lib/components/filter-search/number-filter.svelte rename to src/lib/components/search-attribute-filter/number-filter.svelte index daf405926..88f4c4fad 100644 --- a/src/lib/components/filter-search/number-filter.svelte +++ b/src/lib/components/search-attribute-filter/number-filter.svelte @@ -25,11 +25,11 @@
- + - + - import FilterSearch from '$lib/components/filter-search/index.svelte'; import IsTemporalServerVersionGuard from '$lib/components/is-temporal-server-version-guard.svelte'; + import SearchAttributeFilter from '$lib/components/search-attribute-filter/index.svelte'; import WorkflowAdvancedSearch from '$lib/components/workflow/workflow-advanced-search.svelte'; import Icon from '$lib/holocene/icon/icon.svelte'; import MenuButton from '$lib/holocene/menu/menu-button.svelte'; @@ -18,7 +18,7 @@ export let onClickConfigure: () => void; - { @@ -78,4 +78,4 @@ - + diff --git a/src/lib/pages/schedules.svelte b/src/lib/pages/schedules.svelte index 3c4cbebd5..2de215bcf 100644 --- a/src/lib/pages/schedules.svelte +++ b/src/lib/pages/schedules.svelte @@ -4,10 +4,10 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; - import FilterSearch from '$lib/components/filter-search/index.svelte'; import SchedulesCount from '$lib/components/schedule/schedules-count.svelte'; import SchedulesTableRow from '$lib/components/schedule/schedules-table-row.svelte'; import SchedulesTable from '$lib/components/schedule/schedules-table.svelte'; + import SearchAttributeFilter from '$lib/components/search-attribute-filter/index.svelte'; import ApiPagination from '$lib/holocene/api-pagination.svelte'; import Button from '$lib/holocene/button.svelte'; import EmptyState from '$lib/holocene/empty-state.svelte'; @@ -104,7 +104,7 @@ {#if visibleItems.length || query} - { diff --git a/src/lib/pages/workflows-with-new-search.svelte b/src/lib/pages/workflows-with-new-search.svelte index e169317cf..f230c9aad 100644 --- a/src/lib/pages/workflows-with-new-search.svelte +++ b/src/lib/pages/workflows-with-new-search.svelte @@ -35,7 +35,7 @@ import BatchTerminateConfirmationModal from '$lib/components/workflow/client-actions/batch-terminate-confirmation-modal.svelte'; import CancelConfirmationModal from '$lib/components/workflow/client-actions/cancel-confirmation-modal.svelte'; import TerminateConfirmationModal from '$lib/components/workflow/client-actions/terminate-confirmation-modal.svelte'; - import WorkflowFilterSearch from '$lib/components/workflow/filter-search/index.svelte'; + import WorkflowSearchAttributeFilter from '$lib/components/workflow/search-attribute-filter/index.svelte'; import WorkflowCountRefresh from '$lib/components/workflow/workflow-count-refresh.svelte'; import WorkflowCounts from '$lib/components/workflow/workflow-counts.svelte'; import WorkflowColumnsOrderableList from '$lib/components/workflow/workflows-summary-configurable-table/orderable-list.svelte'; @@ -226,7 +226,7 @@
- +
diff --git a/src/lib/utilities/query/filter-search.test.ts b/src/lib/utilities/query/search-attribute-filter.test.ts similarity index 99% rename from src/lib/utilities/query/filter-search.test.ts rename to src/lib/utilities/query/search-attribute-filter.test.ts index 4c2a64e8a..be0779843 100644 --- a/src/lib/utilities/query/filter-search.test.ts +++ b/src/lib/utilities/query/search-attribute-filter.test.ts @@ -12,7 +12,7 @@ import { isNumberFilter, isStatusFilter, isTextFilter, -} from './filter-search'; +} from './search-attribute-filter'; const store = writable({ BuildIds: 'KeywordList', diff --git a/src/lib/utilities/query/filter-search.ts b/src/lib/utilities/query/search-attribute-filter.ts similarity index 97% rename from src/lib/utilities/query/filter-search.ts rename to src/lib/utilities/query/search-attribute-filter.ts index 075df3d02..3e4c69005 100644 --- a/src/lib/utilities/query/filter-search.ts +++ b/src/lib/utilities/query/search-attribute-filter.ts @@ -80,7 +80,7 @@ export function getFocusedElementId({ ) return 'conditional-menu-button'; - if (isListFilter({ attribute, type })) return 'list-filter-search'; + if (isListFilter({ attribute, type })) return 'list-filter'; if (isBooleanFilter({ attribute, type })) return 'boolean-filter'; diff --git a/tests/integration/filter-search.spec.ts b/tests/integration/workflows-search-attribute-filter.spec.ts similarity index 100% rename from tests/integration/filter-search.spec.ts rename to tests/integration/workflows-search-attribute-filter.spec.ts From 1fbb15e7c0db7418c478c3cb77e0d9dd26464d0a Mon Sep 17 00:00:00 2001 From: Laura Whitaker Date: Fri, 16 Aug 2024 20:05:16 -0600 Subject: [PATCH 03/13] Make schedules table headers configurable --- .../schedule/schedules-table-row.svelte | 98 ++-- .../index.svelte | 47 ++ .../orderable-list.svelte | 33 +- .../search-attribute-filter/index.svelte | 4 +- ...orkflows-summary-configurable-table.svelte | 4 +- .../table-body-cell.svelte | 6 +- .../table-header-cell.svelte | 15 +- .../table-header-row.svelte | 2 +- .../table.svelte | 4 +- .../table/paginated-table/index.svelte | 4 +- src/lib/i18n/locales/en/workflows.ts | 9 +- src/lib/pages/schedules.svelte | 43 +- .../pages/workflows-with-new-search.svelte | 42 +- ....ts => configurable-table-columns.test.ts} | 25 +- src/lib/stores/configurable-table-columns.ts | 447 ++++++++++++++++++ src/lib/stores/workflow-table-columns.ts | 319 ------------- 16 files changed, 651 insertions(+), 451 deletions(-) create mode 100644 src/lib/components/workflow/configurable-table-headers-drawer/index.svelte rename src/lib/components/workflow/{workflows-summary-configurable-table => configurable-table-headers-drawer}/orderable-list.svelte (77%) rename src/lib/stores/{workflow-table-columns.test.ts => configurable-table-columns.test.ts} (89%) create mode 100644 src/lib/stores/configurable-table-columns.ts delete mode 100644 src/lib/stores/workflow-table-columns.ts diff --git a/src/lib/components/schedule/schedules-table-row.svelte b/src/lib/components/schedule/schedules-table-row.svelte index 3cb93a8c3..58ce282bc 100644 --- a/src/lib/components/schedule/schedules-table-row.svelte +++ b/src/lib/components/schedule/schedules-table-row.svelte @@ -4,7 +4,9 @@ import WorkflowStatus from '$lib/components/workflow-status.svelte'; import Link from '$lib/holocene/link.svelte'; import { translate } from '$lib/i18n/translate'; + import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns'; import { relativeTime, timeFormat } from '$lib/stores/time-format'; + import { decodePayloadAttributes } from '$lib/utilities/decode-payload'; import { formatDate } from '$lib/utilities/format-date'; import { routeForEventHistory, @@ -18,11 +20,14 @@ let { namespace } = $page.params; export let schedule: ScheduleListEntry; + export let columns: ConfigurableTableHeader[]; $: spec = schedule?.info?.spec; $: calendar = spec?.structuredCalendar?.[0]; $: interval = spec?.interval?.[0]; $: timezoneName = spec?.timezoneName || 'UTC'; + $: searchAttributes = schedule?.searchAttributes ?? {}; + $: decodedAttributes = decodePayloadAttributes({ searchAttributes }); const sortRecentActions = (recentActions: ScheduleActionResult[]) => { return ( @@ -43,45 +48,60 @@ - - - - - {schedule.scheduleId} - - - {schedule?.info?.workflowType?.name ?? ''} - - - {#each sortRecentActions(schedule?.info?.recentActions) as run} -

- {formatDate(run?.actualTime, $timeFormat, { - relative: $relativeTime, - })} -

- {/each} - - - {#each schedule?.info?.futureActionTimes?.slice(0, 5) ?? [] as run} -
- {formatDate(run, $timeFormat, { - relative: $relativeTime, - relativeLabel: translate('common.from-now'), - })} -
- {/each} - - -

{@html translate('common.timezone', { timezone: timezoneName })}

- - + {#each columns as { label } (label)} + {#if label === translate('common.status')} + + + + {:else if label === translate('schedules.name')} + + {schedule.scheduleId} + + {:else if label === translate('common.workflow-type')} + + {schedule?.info?.workflowType?.name ?? ''} + + {:else if label === translate('schedules.recent-runs')} + + {#each sortRecentActions(schedule?.info?.recentActions) as run} +

+ {formatDate(run?.actualTime, $timeFormat, { + relative: $relativeTime, + })} +

+ {/each} + + {:else if label === translate('schedules.upcoming-runs')} + + {#each schedule?.info?.futureActionTimes?.slice(0, 5) ?? [] as run} +
+ {formatDate(run, $timeFormat, { + relative: $relativeTime, + relativeLabel: translate('common.from-now'), + })} +
+ {/each} + + {:else if label === translate('schedules.schedule-spec')} + +

{@html translate('common.timezone', { timezone: timezoneName })}

+ + + {:else} + + {decodedAttributes?.searchAttributes?.indexedFields?.[label] ?? ''} + + {/if} + {/each} diff --git a/src/lib/components/workflow/workflows-summary-configurable-table/table-header-row.svelte b/src/lib/components/workflow/workflows-summary-configurable-table/table-header-row.svelte index 9baec53df..da24da995 100644 --- a/src/lib/components/workflow/workflows-summary-configurable-table/table-header-row.svelte +++ b/src/lib/components/workflow/workflows-summary-configurable-table/table-header-row.svelte @@ -65,7 +65,7 @@ } .batch-actions-table-cell { - @apply overflow-visible whitespace-nowrap text-sm font-medium; + @apply overflow-visible whitespace-nowrap font-medium; } .configuration-button-table-cell { diff --git a/src/lib/components/workflow/workflows-summary-configurable-table/table.svelte b/src/lib/components/workflow/workflows-summary-configurable-table/table.svelte index 497d62bab..5c329511a 100644 --- a/src/lib/components/workflow/workflows-summary-configurable-table/table.svelte +++ b/src/lib/components/workflow/workflows-summary-configurable-table/table.svelte @@ -1,11 +1,11 @@ diff --git a/src/lib/holocene/table/paginated-table/index.svelte b/src/lib/holocene/table/paginated-table/index.svelte index cfbcaea56..cc871daab 100644 --- a/src/lib/holocene/table/paginated-table/index.svelte +++ b/src/lib/holocene/table/paginated-table/index.svelte @@ -58,7 +58,7 @@ } :global(tr > th) { - @apply px-2 text-left first-of-type:rounded-tl last-of-type:rounded-tr; + @apply whitespace-nowrap px-2 text-left font-medium first-of-type:rounded-tl last-of-type:rounded-tr; } } @@ -68,7 +68,7 @@ } :global(tr > td) { - @apply px-2; + @apply whitespace-nowrap px-2; } :global(tr > td > .table-link) { diff --git a/src/lib/i18n/locales/en/workflows.ts b/src/lib/i18n/locales/en/workflows.ts index 7ac46b9a9..bffea3b6b 100644 --- a/src/lib/i18n/locales/en/workflows.ts +++ b/src/lib/i18n/locales/en/workflows.ts @@ -37,11 +37,10 @@ export const Strings = { 'The batch cancel request is processing in the background.', 'batch-reset-all-success': 'The batch reset request is processing in the background.', - 'configure-workflows': 'Configure Workflow List', - 'open-configure-workflows': 'Open workflow list configuration', - 'close-configure-workflows': 'Close workflow list configuration', - 'configure-workflows-description': - 'Add (<1>), re-arrange (<2>), and remove (<3>), Workflow Headings to personalize the Workflow List Table.', + 'configure-headers': 'Configure {{title}}', + 'close-configure-headers': 'Close {{title}} configuration', + 'configure-headers-description': + 'Add (<1>), re-arrange (<2>), and remove (<3>), {{type}} Headings to personalize the {{title}} Table.', 'all-statuses': 'All Statuses', running: 'Running', 'timed-out': 'Timed Out', diff --git a/src/lib/pages/schedules.svelte b/src/lib/pages/schedules.svelte index 6bb9c80f3..5ed34932d 100644 --- a/src/lib/pages/schedules.svelte +++ b/src/lib/pages/schedules.svelte @@ -7,12 +7,18 @@ import SchedulesCount from '$lib/components/schedule/schedules-count.svelte'; import SchedulesTableRow from '$lib/components/schedule/schedules-table-row.svelte'; import SearchAttributeFilter from '$lib/components/search-attribute-filter/index.svelte'; + import ConfigurableTableHeadersDrawer from '$lib/components/workflow/configurable-table-headers-drawer/index.svelte'; import Button from '$lib/holocene/button.svelte'; import EmptyState from '$lib/holocene/empty-state.svelte'; import Link from '$lib/holocene/link.svelte'; import PaginatedTable from '$lib/holocene/table/paginated-table/api-paginated.svelte'; import { translate } from '$lib/i18n/translate'; import { fetchPaginatedSchedules } from '$lib/services/schedule-service'; + import { + availableScheduleColumns, + configurableTableColumns, + TABLE_TYPE, + } from '$lib/stores/configurable-table-columns'; import { coreUserStore } from '$lib/stores/core-user'; import { scheduleFilters } from '$lib/stores/filters'; import { schedulesCount } from '$lib/stores/schedules'; @@ -24,10 +30,16 @@ import { routeForScheduleCreate } from '$lib/utilities/route-for'; import { writeActionsAreAllowed } from '$lib/utilities/write-actions-are-allowed'; - $: namespace = $page.params.namespace; - let refresh = Date.now(); let coreUser = coreUserStore(); + let customizationDrawerOpen = false; + + const openCustomizationDrawer = () => { + customizationDrawerOpen = true; + }; + + $: namespace = $page.params.namespace; + $: columns = $configurableTableColumns?.[namespace]?.schedules ?? []; $: createDisabled = $coreUser.namespaceWriteDisabled(namespace); $: searchAttributeOptions = Object.entries($customSearchAttributes).map( ([key, value]) => { @@ -40,6 +52,7 @@ ); $: query = $page.url.searchParams.get('query'); $: onFetch = () => fetchPaginatedSchedules(namespace, query); + $: availableColumns = availableScheduleColumns(namespace); onMount(() => { if (query) { @@ -79,8 +92,13 @@ refresh = Date.now(); }} /> + - - - - - - + {#each columns as { label }} + + {/each} {#each visibleItems as schedule} - + {/each} @@ -136,3 +151,11 @@ {/key} + + diff --git a/src/lib/pages/workflows-with-new-search.svelte b/src/lib/pages/workflows-with-new-search.svelte index f230c9aad..f7dc01307 100644 --- a/src/lib/pages/workflows-with-new-search.svelte +++ b/src/lib/pages/workflows-with-new-search.svelte @@ -35,19 +35,17 @@ import BatchTerminateConfirmationModal from '$lib/components/workflow/client-actions/batch-terminate-confirmation-modal.svelte'; import CancelConfirmationModal from '$lib/components/workflow/client-actions/cancel-confirmation-modal.svelte'; import TerminateConfirmationModal from '$lib/components/workflow/client-actions/terminate-confirmation-modal.svelte'; + import ConfigurableTableHeadersDrawer from '$lib/components/workflow/configurable-table-headers-drawer/index.svelte'; import WorkflowSearchAttributeFilter from '$lib/components/workflow/search-attribute-filter/index.svelte'; import WorkflowCountRefresh from '$lib/components/workflow/workflow-count-refresh.svelte'; import WorkflowCounts from '$lib/components/workflow/workflow-counts.svelte'; - import WorkflowColumnsOrderableList from '$lib/components/workflow/workflows-summary-configurable-table/orderable-list.svelte'; import WorkflowsSummaryConfigurableTable from '$lib/components/workflow/workflows-summary-configurable-table.svelte'; import Button from '$lib/holocene/button.svelte'; - import DrawerContent from '$lib/holocene/drawer-content.svelte'; - import Drawer from '$lib/holocene/drawer.svelte'; - import Icon from '$lib/holocene/icon/icon.svelte'; import { translate } from '$lib/i18n/translate'; import Translate from '$lib/i18n/translate.svelte'; import { supportsAdvancedVisibility } from '$lib/stores/advanced-visibility'; import { groupByCountEnabled } from '$lib/stores/capability-enablement'; + import { availableWorkflowSystemSearchAttributeColumns } from '$lib/stores/configurable-table-columns'; import { workflowFilters } from '$lib/stores/filters'; import { lastUsedNamespace } from '$lib/stores/namespaces'; import { searchAttributes } from '$lib/stores/search-attributes'; @@ -70,6 +68,11 @@ $: searchParams = $page.url.searchParams.toString(); $: searchParams, ($workflowsSearchParams = searchParams); + $: availableColumns = availableWorkflowSystemSearchAttributeColumns( + namespace, + $page.data.settings, + ); + onMount(() => { $lastUsedNamespace = $page.params.namespace; if (query) { @@ -164,10 +167,6 @@ const openCustomizationDrawer = () => { customizationDrawerOpen = true; }; - - const closeCustomizationDrawer = () => { - customizationDrawerOpen = false; - }; - - - - Add (), re-arrange (), and remove (), Workflow Headings - to personalize the Workflow List Table. - - - - - + diff --git a/src/lib/stores/workflow-table-columns.test.ts b/src/lib/stores/configurable-table-columns.test.ts similarity index 89% rename from src/lib/stores/workflow-table-columns.test.ts rename to src/lib/stores/configurable-table-columns.test.ts index aa5685701..12b31d225 100644 --- a/src/lib/stores/workflow-table-columns.test.ts +++ b/src/lib/stores/configurable-table-columns.test.ts @@ -8,13 +8,14 @@ import { persistedWorkflowTableColumns, pinColumn, removeColumn, -} from './workflow-table-columns'; + TABLE_TYPE, +} from './configurable-table-columns'; describe('Workflow Table Columns store', () => { describe('addColumn', () => { test('moves a column from the availableColumns array to the columns array', () => { persistedWorkflowTableColumns.set({ default: [] }); - addColumn('Workflow ID', 'default'); + addColumn('Workflow ID', 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [{ label: 'Workflow ID', pinned: false }], }); @@ -26,7 +27,7 @@ describe('Workflow Table Columns store', () => { persistedWorkflowTableColumns.set({ default: [{ label: 'Workflow ID', pinned: true }], }); - removeColumn('Workflow ID', 'default'); + removeColumn('Workflow ID', 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [] }); }); }); @@ -39,7 +40,7 @@ describe('Workflow Table Columns store', () => { { label: 'Start', pinned: false }, ], }); - moveColumn(1, 0, 'default'); + moveColumn(1, 0, 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [ { label: 'Start', pinned: true }, @@ -55,7 +56,7 @@ describe('Workflow Table Columns store', () => { { label: 'Start', pinned: false }, ], }); - moveColumn(0, 1, 'default'); + moveColumn(0, 1, 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [ { label: 'Start', pinned: false }, @@ -71,7 +72,7 @@ describe('Workflow Table Columns store', () => { { label: 'Start', pinned: true }, ], }); - moveColumn(1, 0, 'default'); + moveColumn(1, 0, 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [ { label: 'Start', pinned: true }, @@ -87,7 +88,7 @@ describe('Workflow Table Columns store', () => { { label: 'Start', pinned: true }, ], }); - moveColumn(0, 1, 'default'); + moveColumn(0, 1, 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [ { label: 'Start', pinned: true }, @@ -106,7 +107,7 @@ describe('Workflow Table Columns store', () => { { label: 'History Length', pinned: false }, ], }); - moveColumn(4, 0, 'default'); + moveColumn(4, 0, 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [ { label: 'History Length', pinned: true }, @@ -128,7 +129,7 @@ describe('Workflow Table Columns store', () => { { label: 'History Length', pinned: false }, ], }); - moveColumn(0, 4, 'default'); + moveColumn(0, 4, 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [ { label: 'End', pinned: true }, @@ -150,7 +151,7 @@ describe('Workflow Table Columns store', () => { { label: 'Run ID', pinned: false }, ], }); - pinColumn('End', 'default'); + pinColumn('End', 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [ { label: 'Start', pinned: true }, @@ -168,7 +169,7 @@ describe('Workflow Table Columns store', () => { { label: 'End', pinned: false }, ], }); - pinColumn('End', 'default'); + pinColumn('End', 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [ { label: 'Start', pinned: true }, @@ -186,7 +187,7 @@ describe('Workflow Table Columns store', () => { { label: 'End', pinned: true }, ], }); - pinColumn('Start', 'default'); + pinColumn('Start', 'default', TABLE_TYPE.WORKFLOWS); expect(get(persistedWorkflowTableColumns)).toEqual({ default: [ { label: 'Run ID', pinned: true }, diff --git a/src/lib/stores/configurable-table-columns.ts b/src/lib/stores/configurable-table-columns.ts new file mode 100644 index 000000000..338acb792 --- /dev/null +++ b/src/lib/stores/configurable-table-columns.ts @@ -0,0 +1,447 @@ +import { derived, type Readable, type Writable } from 'svelte/store'; + +import { page } from '$app/stores'; + +import { translate } from '$lib/i18n/translate'; +import type { Settings } from '$lib/types/global'; + +import { namespaces } from './namespaces'; +import { persistStore } from './persist-store'; +import { customSearchAttributes } from './search-attributes'; + +export const MAX_PINNED_COLUMNS = 2; +export const WorkflowHeaderLabels = [ + 'Status', + 'Workflow ID', + 'Run ID', + 'Type', + 'Start', + 'End', + 'History Size', + 'History Length', + 'Execution Time', + 'Execution Duration', + 'State Transitions', + 'Parent Namespace', + 'Task Queue', + 'Scheduled By ID', + 'Scheduled Start Time', +] as const; + +export type WorkflowHeaderLabel = (typeof WorkflowHeaderLabels)[number]; + +// https://github.com/microsoft/TypeScript/issues/29729 +// https://stackoverflow.com/a/61048124 +// eslint-disable-next-line @typescript-eslint/ban-types +type AnyWorkflowHeaderLabel = WorkflowHeaderLabel | (string & {}); + +export type ConfigurableTableHeader = { + label: AnyWorkflowHeaderLabel; + pinned: boolean; +}; + +export const TABLE_TYPE = { + WORKFLOWS: 'workflows', + SCHEDULES: 'schedules', +} as const; + +type Keys = keyof typeof TABLE_TYPE; +export type ConfigurableTableType = (typeof TABLE_TYPE)[Keys]; + +type TableColumns = { + [namespace: string]: + | { + [key in ConfigurableTableType]: ConfigurableTableHeader[]; + } + | undefined; +}; + +type State = { + [namespace: string]: ConfigurableTableHeader[] | undefined; +}; + +type Action = + | { + type: 'CONFIGURABLE_COLUMN.ADD'; + payload: { + label: AnyWorkflowHeaderLabel; + namespace: string; + table: ConfigurableTableType; + }; + } + | { + type: 'CONFIGURABLE_COLUMN.REMOVE'; + payload: { + label: AnyWorkflowHeaderLabel; + namespace: string; + table: ConfigurableTableType; + }; + } + | { + type: 'CONFIGURABLE_COLUMN.PIN'; + payload: { + label: AnyWorkflowHeaderLabel; + namespace: string; + table: ConfigurableTableType; + }; + } + | { + type: 'CONFIGURABLE_COLUMN.MOVE'; + payload: { + from: number; + to: number; + namespace: string; + table: ConfigurableTableType; + }; + }; + +const DEFAULT_WORKFLOWS_COLUMNS: ConfigurableTableHeader[] = [ + { label: 'Status', pinned: true }, + { label: 'Workflow ID', pinned: true }, + { label: 'Run ID', pinned: false }, + { label: 'Type', pinned: false }, + { label: 'Start', pinned: false }, + { label: 'End', pinned: false }, +]; + +const DEFAULT_AVAILABLE_WORKFLOWS_COLUMNS: ConfigurableTableHeader[] = [ + { label: 'History Size', pinned: false }, + { label: 'History Length', pinned: false }, + { label: 'Execution Time', pinned: false }, + { label: 'Execution Duration', pinned: false }, + { label: 'State Transitions', pinned: false }, + { label: 'Parent Namespace', pinned: false }, + { label: 'Task Queue', pinned: false }, + { label: 'Scheduled By ID', pinned: false }, + { label: 'Scheduled Start Time', pinned: false }, +]; + +const DEFAULT_SCHEDULES_COLUMNS: ConfigurableTableHeader[] = [ + { label: translate('common.status'), pinned: false }, + { label: translate('schedules.name'), pinned: false }, + { label: translate('common.workflow-type'), pinned: false }, + { label: translate('schedules.recent-runs'), pinned: false }, + { label: translate('schedules.upcoming-runs'), pinned: false }, + { label: translate('schedules.schedule-spec'), pinned: false }, +]; + +const isNotParentWorkflowIdColumn = (column: ConfigurableTableHeader) => + column.label !== 'Parent Workflow ID'; + +const getDefaultWorkflowsTableColumns = (): ConfigurableTableHeader[] => { + let columns: ConfigurableTableHeader[]; + try { + // try to get the list of columns that was stored last time they interacted + // with the table before we made it namespace-specific + const stringifiedOldColumns = window.localStorage.getItem( + 'workflow-table-columns', + ); + const parsedOldColumns = JSON.parse(stringifiedOldColumns); + + if (stringifiedOldColumns && parsedOldColumns?.length) { + const filteredOldColumns = parsedOldColumns.filter( + isNotParentWorkflowIdColumn, + ); + columns = filteredOldColumns; + } else { + columns = DEFAULT_WORKFLOWS_COLUMNS; + } + } catch { + columns = DEFAULT_WORKFLOWS_COLUMNS; + } + + return columns; +}; + +export const persistedWorkflowTableColumns = persistStore( + 'namespace-workflow-table-columns', + {}, +); + +export const persistedSchedulesTableColumns = persistStore( + 'namespace-schedules-table-columns', + {}, +); + +export const configurableTableColumns: Readable = derived( + [ + namespaces, + page, + persistedWorkflowTableColumns, + persistedSchedulesTableColumns, + ], + ([ + $namespaces, + $page, + $persistedWorkflowTableColumns, + $persistedSchedulesTableColumns, + ]) => { + const state: TableColumns = {}; + const useOrAddDefaultTableColumnsToNamespace = ( + columns: State, + namespace: string, + defaultColumns: ConfigurableTableHeader[], + update: (columns: State) => void, + ) => { + if (!columns?.[namespace]?.length) { + columns[namespace] = [...defaultColumns]; + return columns[namespace]; + } + const filteredColumns = columns[namespace].filter( + isNotParentWorkflowIdColumn, + ); + + if (filteredColumns.length !== columns[namespace].length) { + columns[namespace] = filteredColumns; + update(columns); + } + + return columns[namespace]; + }; + + const getTableColumns = (namespace: string) => ({ + workflows: useOrAddDefaultTableColumnsToNamespace( + $persistedWorkflowTableColumns, + namespace, + getDefaultWorkflowsTableColumns(), + (columns) => persistedWorkflowTableColumns.set(columns), + ), + schedules: useOrAddDefaultTableColumnsToNamespace( + $persistedSchedulesTableColumns, + namespace, + DEFAULT_SCHEDULES_COLUMNS, + (columns) => persistedSchedulesTableColumns.set(columns), + ), + }); + + const namespaceColumns = + $namespaces?.reduce( + (namespaceToColumnsMap, { namespaceInfo: { name } }): TableColumns => { + return { + ...namespaceToColumnsMap, + [name]: getTableColumns(name), + }; + }, + state, + ) ?? {}; + const { namespace: currentNamespace } = $page.params; + + return namespaceColumns[currentNamespace] + ? namespaceColumns + : { + ...namespaceColumns, + [currentNamespace]: getTableColumns(currentNamespace), + }; + }, +); + +export const pinnedColumnsWidth = persistStore( + 'workflow-table-pinned-columns-width', +); + +export const availableWorkflowSystemSearchAttributeColumns: ( + namespace: string, + settings: Settings, +) => Readable = (namespace, settings) => + derived(configurableTableColumns, ($configurableTableColumns) => + [ + ...DEFAULT_WORKFLOWS_COLUMNS, + ...(settings?.runtimeEnvironment?.isCloud + ? DEFAULT_AVAILABLE_WORKFLOWS_COLUMNS.filter( + (col) => col.label !== 'Parent Namespace', + ) + : DEFAULT_AVAILABLE_WORKFLOWS_COLUMNS), + ].filter( + (header) => + !$configurableTableColumns[namespace]?.workflows?.some( + (column) => column.label === header.label, + ), + ), + ); + +export const availableScheduleColumns: ( + namespace: string, +) => Readable = (namespace) => + derived(configurableTableColumns, ($configurableTableColumns) => + [...DEFAULT_SCHEDULES_COLUMNS].filter( + (header) => + !$configurableTableColumns[namespace]?.schedules?.some( + (column) => column.label === header.label, + ), + ), + ); + +export const availableCustomSearchAttributeColumns: ( + namespace: string, + table?: ConfigurableTableType, +) => Readable = ( + namespace: string, + table: ConfigurableTableType = TABLE_TYPE.WORKFLOWS, +) => + derived( + [customSearchAttributes, configurableTableColumns], + ([$customSearchAttributes, $configurableTableColumns]) => + Object.keys($customSearchAttributes) + .filter( + (searchAttribute) => + !$configurableTableColumns[namespace]?.[table]?.some( + (column) => column.label === searchAttribute, + ), + ) + .map((key) => ({ + label: key, + pinned: false, + })), + ); + +const getDefaultColumns = (table: ConfigurableTableType) => { + switch (table) { + case TABLE_TYPE.WORKFLOWS: + return DEFAULT_WORKFLOWS_COLUMNS; + case TABLE_TYPE.SCHEDULES: + return DEFAULT_SCHEDULES_COLUMNS; + } +}; + +const reducer = (action: Action, state: State): State => { + const defaultColumns = getDefaultColumns(action.payload.table); + switch (action.type) { + case 'CONFIGURABLE_COLUMN.ADD': { + const { label, namespace } = action.payload; + const columns = state?.[namespace] ?? defaultColumns; + + return { + ...state, + [namespace]: [...columns, { label, pinned: false }], + }; + } + case 'CONFIGURABLE_COLUMN.REMOVE': { + const { label: labelToRemove, namespace } = action.payload; + const columns = state?.[namespace] ?? defaultColumns; + + return { + ...state, + [namespace]: columns.filter(({ label }) => label !== labelToRemove), + }; + } + case 'CONFIGURABLE_COLUMN.PIN': { + const { label: labelToPin, namespace } = action.payload; + const columns = state?.[namespace] ?? defaultColumns; + const index = columns.findIndex(({ label }) => label === labelToPin); + + const isPinned = columns[index].pinned; + + let lastPinned = -1; + for (let i = columns.length - 1; i >= 0; i--) { + if (columns[i].pinned) { + lastPinned = i; + break; + } + } + + const newColumns = [...columns]; + newColumns[index].pinned = !isPinned; + + if (index > lastPinned && !isPinned) { + newColumns.splice(lastPinned + 1, 0, newColumns.splice(index, 1)[0]); + } else if (index < lastPinned && isPinned) { + newColumns.splice(lastPinned, 0, newColumns.splice(index, 1)[0]); + } + + return { + ...state, + [namespace]: newColumns, + }; + } + case 'CONFIGURABLE_COLUMN.MOVE': { + const { from, to, namespace } = action.payload; + const columns = state?.[namespace] ?? DEFAULT_WORKFLOWS_COLUMNS; + const isPinned = columns[from].pinned; + + let lastPinned = 0; + for (let i = columns.length - 1; i >= 0; i--) { + if (columns[i].pinned) { + lastPinned = i; + break; + } + } + + const tempColumns = [...columns]; + if (to <= lastPinned && !isPinned) { + tempColumns[from].pinned = true; + } else if (to > lastPinned && isPinned) { + tempColumns[from].pinned = false; + } + + tempColumns.splice(to, 0, tempColumns.splice(from, 1)[0]); + + return { + ...state, + [namespace]: tempColumns.map((c, idx) => + idx > MAX_PINNED_COLUMNS - 1 ? { ...c, pinned: false } : c, + ), + }; + } + default: + return state; + } +}; + +const getPersistedColumns = (table: ConfigurableTableType): Writable => { + switch (table) { + case TABLE_TYPE.WORKFLOWS: + return persistedWorkflowTableColumns; + case TABLE_TYPE.SCHEDULES: + return persistedSchedulesTableColumns; + } +}; + +const dispatch = (action: Action) => { + const columns = getPersistedColumns(action.payload.table); + columns.update((previousState) => reducer(action, previousState)); +}; + +export const addColumn = ( + label: AnyWorkflowHeaderLabel, + namespace: string, + table: ConfigurableTableType, +) => { + dispatch({ + type: 'CONFIGURABLE_COLUMN.ADD', + payload: { label, namespace, table }, + }); +}; + +export const removeColumn = ( + label: AnyWorkflowHeaderLabel, + namespace: string, + table: ConfigurableTableType, +) => { + dispatch({ + type: 'CONFIGURABLE_COLUMN.REMOVE', + payload: { label, namespace, table }, + }); +}; + +export const moveColumn = ( + from: number, + to: number, + namespace: string, + table: ConfigurableTableType, +) => { + dispatch({ + type: 'CONFIGURABLE_COLUMN.MOVE', + payload: { from, to, namespace, table }, + }); +}; + +export const pinColumn = ( + label: AnyWorkflowHeaderLabel, + namespace: string, + table: ConfigurableTableType, +) => { + dispatch({ + type: 'CONFIGURABLE_COLUMN.PIN', + payload: { label, namespace, table }, + }); +}; diff --git a/src/lib/stores/workflow-table-columns.ts b/src/lib/stores/workflow-table-columns.ts deleted file mode 100644 index f9dedbfb5..000000000 --- a/src/lib/stores/workflow-table-columns.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { derived, type Readable } from 'svelte/store'; - -import { page } from '$app/stores'; - -import type { Settings } from '$lib/types/global'; - -import { namespaces } from './namespaces'; -import { persistStore } from './persist-store'; -import { customSearchAttributes } from './search-attributes'; - -export const MAX_PINNED_COLUMNS = 2; -export const WorkflowHeaderLabels = [ - 'Status', - 'Workflow ID', - 'Run ID', - 'Type', - 'Start', - 'End', - 'History Size', - 'History Length', - 'Execution Time', - 'Execution Duration', - 'State Transitions', - 'Parent Namespace', - 'Task Queue', - 'Scheduled By ID', - 'Scheduled Start Time', -] as const; - -export type WorkflowHeaderLabel = (typeof WorkflowHeaderLabels)[number]; - -// https://github.com/microsoft/TypeScript/issues/29729 -// https://stackoverflow.com/a/61048124 -// eslint-disable-next-line @typescript-eslint/ban-types -type AnyWorkflowHeaderLabel = WorkflowHeaderLabel | (string & {}); - -export type WorkflowHeader = { - label: AnyWorkflowHeaderLabel; - pinned: boolean; -}; - -type State = { - [namespace: string]: WorkflowHeader[] | undefined; -}; - -type Action = - | { - type: 'WORKFLOW_COLUMN.ADD'; - payload: { label: AnyWorkflowHeaderLabel; namespace: string }; - } - | { - type: 'WORKFLOW_COLUMN.REMOVE'; - payload: { label: AnyWorkflowHeaderLabel; namespace: string }; - } - | { - type: 'WORKFLOW_COLUMN.PIN'; - payload: { label: AnyWorkflowHeaderLabel; namespace: string }; - } - | { - type: 'WORKFLOW_COLUMN.MOVE'; - payload: { from: number; to: number; namespace: string }; - }; - -const DEFAULT_COLUMNS: WorkflowHeader[] = [ - { label: 'Status', pinned: true }, - { label: 'Workflow ID', pinned: true }, - { label: 'Run ID', pinned: false }, - { label: 'Type', pinned: false }, - { label: 'Start', pinned: false }, - { label: 'End', pinned: false }, -]; - -const DEFAULT_AVAILABLE_COLUMNS: WorkflowHeader[] = [ - { label: 'History Size', pinned: false }, - { label: 'History Length', pinned: false }, - { label: 'Execution Time', pinned: false }, - { label: 'Execution Duration', pinned: false }, - { label: 'State Transitions', pinned: false }, - { label: 'Parent Namespace', pinned: false }, - { label: 'Task Queue', pinned: false }, - { label: 'Scheduled By ID', pinned: false }, - { label: 'Scheduled Start Time', pinned: false }, -]; - -const isNotParentWorkflowIdColumn = (column: WorkflowHeader) => - column.label !== 'Parent Workflow ID'; - -export const getDefaultColumns = (): WorkflowHeader[] => { - let columns: WorkflowHeader[]; - try { - // try to get the list of columns that was stored last time they interacted - // with the table before we made it namespace-specific - const stringifiedOldColumns = window.localStorage.getItem( - 'workflow-table-columns', - ); - const parsedOldColumns = JSON.parse(stringifiedOldColumns); - - if (stringifiedOldColumns && parsedOldColumns?.length) { - const filteredOldColumns = parsedOldColumns.filter( - isNotParentWorkflowIdColumn, - ); - columns = filteredOldColumns; - } else { - columns = DEFAULT_COLUMNS; - } - } catch { - columns = DEFAULT_COLUMNS; - } - - return columns; -}; - -export const persistedWorkflowTableColumns = persistStore( - 'namespace-workflow-table-columns', - {}, -); - -export const workflowTableColumns: Readable = derived( - [namespaces, page, persistedWorkflowTableColumns], - ([$namespaces, $page, $persistedWorkflowTableColumns]) => { - const state: State = {}; - - const useOrAddDefaultTableColumnsToNamespace = ( - columns: State, - namespace: string, - ) => { - if (!columns?.[namespace]?.length) { - columns[namespace] = [...getDefaultColumns()]; - return columns[namespace]; - } - const filteredColumns = columns[namespace].filter( - isNotParentWorkflowIdColumn, - ); - - if (filteredColumns.length !== columns[namespace].length) { - columns[namespace] = filteredColumns; - persistedWorkflowTableColumns.set(columns); - } - - return columns[namespace]; - }; - - const namespaceColumns = - $namespaces?.reduce( - (namespaceToColumnsMap, { namespaceInfo: { name } }) => { - return { - ...namespaceToColumnsMap, - [name]: useOrAddDefaultTableColumnsToNamespace( - $persistedWorkflowTableColumns, - name, - ), - }; - }, - state, - ) ?? {}; - const { namespace: currentNamespace } = $page.params; - - return namespaceColumns[currentNamespace] - ? namespaceColumns - : { - ...namespaceColumns, - [currentNamespace]: useOrAddDefaultTableColumnsToNamespace( - $persistedWorkflowTableColumns, - currentNamespace, - ), - }; - }, -); - -export const pinnedColumnsWidth = persistStore( - 'workflow-table-pinned-columns-width', -); - -export const availableSystemSearchAttributeColumns: ( - namespace: string, - settings: Settings, -) => Readable = (namespace, settings) => - derived(workflowTableColumns, ($workflowTableColumns) => - [ - ...DEFAULT_COLUMNS, - ...(settings?.runtimeEnvironment?.isCloud - ? DEFAULT_AVAILABLE_COLUMNS.filter( - (col) => col.label !== 'Parent Namespace', - ) - : DEFAULT_AVAILABLE_COLUMNS), - ].filter( - (header) => - !$workflowTableColumns[namespace]?.some( - (column) => column.label === header.label, - ), - ), - ); - -export const availableCustomSearchAttributeColumns: ( - namespace: string, -) => Readable = (namespace: string) => - derived( - [customSearchAttributes, workflowTableColumns], - ([$customSearchAttributes, $workflowTableColumns]) => - Object.keys($customSearchAttributes) - .filter( - (searchAttribute) => - !$workflowTableColumns[namespace]?.some( - (column) => column.label === searchAttribute, - ), - ) - .map((key) => ({ - label: key, - pinned: false, - })), - ); - -const reducer = (action: Action, state: State): State => { - switch (action.type) { - case 'WORKFLOW_COLUMN.ADD': { - const { label, namespace } = action.payload; - const columns = state?.[namespace] ?? DEFAULT_COLUMNS; - - return { - ...state, - [namespace]: [...columns, { label, pinned: false }], - }; - } - case 'WORKFLOW_COLUMN.REMOVE': { - const { label: labelToRemove, namespace } = action.payload; - const columns = state?.[namespace] ?? DEFAULT_COLUMNS; - - return { - ...state, - [namespace]: columns.filter(({ label }) => label !== labelToRemove), - }; - } - case 'WORKFLOW_COLUMN.PIN': { - const { label: labelToPin, namespace } = action.payload; - const columns = state?.[namespace] ?? DEFAULT_COLUMNS; - const index = columns.findIndex(({ label }) => label === labelToPin); - - const isPinned = columns[index].pinned; - - let lastPinned = -1; - for (let i = columns.length - 1; i >= 0; i--) { - if (columns[i].pinned) { - lastPinned = i; - break; - } - } - - const newColumns = [...columns]; - newColumns[index].pinned = !isPinned; - - if (index > lastPinned && !isPinned) { - newColumns.splice(lastPinned + 1, 0, newColumns.splice(index, 1)[0]); - } else if (index < lastPinned && isPinned) { - newColumns.splice(lastPinned, 0, newColumns.splice(index, 1)[0]); - } - - return { - ...state, - [namespace]: newColumns, - }; - } - case 'WORKFLOW_COLUMN.MOVE': { - const { from, to, namespace } = action.payload; - const columns = state?.[namespace] ?? DEFAULT_COLUMNS; - const isPinned = columns[from].pinned; - - let lastPinned = 0; - for (let i = columns.length - 1; i >= 0; i--) { - if (columns[i].pinned) { - lastPinned = i; - break; - } - } - - const tempColumns = [...columns]; - if (to <= lastPinned && !isPinned) { - tempColumns[from].pinned = true; - } else if (to > lastPinned && isPinned) { - tempColumns[from].pinned = false; - } - - tempColumns.splice(to, 0, tempColumns.splice(from, 1)[0]); - - return { - ...state, - [namespace]: tempColumns.map((c, idx) => - idx > MAX_PINNED_COLUMNS - 1 ? { ...c, pinned: false } : c, - ), - }; - } - default: - return state; - } -}; - -const dispatch = (action: Action) => { - persistedWorkflowTableColumns.update((previousState) => - reducer(action, previousState), - ); -}; - -export const addColumn = (label: AnyWorkflowHeaderLabel, namespace: string) => { - dispatch({ type: 'WORKFLOW_COLUMN.ADD', payload: { label, namespace } }); -}; - -export const removeColumn = ( - label: AnyWorkflowHeaderLabel, - namespace: string, -) => { - dispatch({ type: 'WORKFLOW_COLUMN.REMOVE', payload: { label, namespace } }); -}; - -export const moveColumn = (from: number, to: number, namespace: string) => { - dispatch({ type: 'WORKFLOW_COLUMN.MOVE', payload: { from, to, namespace } }); -}; - -export const pinColumn = (label: AnyWorkflowHeaderLabel, namespace: string) => { - dispatch({ type: 'WORKFLOW_COLUMN.PIN', payload: { label, namespace } }); -}; From c8c435c0a06af27b8eaf94818b4e7c71e7c2be90 Mon Sep 17 00:00:00 2001 From: Laura Whitaker Date: Fri, 16 Aug 2024 20:46:28 -0600 Subject: [PATCH 04/13] Add back schedule name search input --- src/lib/pages/schedules.svelte | 65 ++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/src/lib/pages/schedules.svelte b/src/lib/pages/schedules.svelte index 5ed34932d..d73c1bbf7 100644 --- a/src/lib/pages/schedules.svelte +++ b/src/lib/pages/schedules.svelte @@ -1,4 +1,6 @@ {#key [namespace, query, refresh]} - {#if query} + {#if error} + + + {error} + + + {:else if query} => { return (pageSize = 100, token = '') => { @@ -45,6 +46,7 @@ export const fetchPaginatedSchedules = async ( ...(query ? { query } : {}), }, request, + onError, }).then(({ schedules, nextPageToken }) => { return { items: schedules, From 3c5396684a345fc1f357b5661619265756f6f6bd Mon Sep 17 00:00:00 2001 From: Laura Whitaker Date: Tue, 20 Aug 2024 10:14:56 -0600 Subject: [PATCH 10/13] Remove schedule name search input --- src/lib/pages/schedules.svelte | 38 +--------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/src/lib/pages/schedules.svelte b/src/lib/pages/schedules.svelte index e1e05c96f..c14d6cfe9 100644 --- a/src/lib/pages/schedules.svelte +++ b/src/lib/pages/schedules.svelte @@ -1,6 +1,4 @@
{translate('common.status')}{translate('schedules.name')}{translate('common.workflow-type')}{translate('schedules.recent-runs')}{translate('schedules.upcoming-runs')}{translate('schedules.schedule-spec')}{label}