diff --git a/package.json b/package.json index 5fdac739a..f5b72f7f5 100644 --- a/package.json +++ b/package.json @@ -118,13 +118,13 @@ "@sveltejs/adapter-vercel": "^1.0.5", "@sveltejs/kit": "1.15.4", "@sveltejs/vite-plugin-svelte": "^2.0.2", - "@temporalio/activity": "^1.8.4", - "@temporalio/client": "^1.8.4", - "@temporalio/common": "^1.8.4", - "@temporalio/proto": "^1.8.4", - "@temporalio/testing": "^1.8.4", - "@temporalio/worker": "^1.8.4", - "@temporalio/workflow": "^1.8.4", + "@temporalio/activity": "^1.8.6", + "@temporalio/client": "^1.8.6", + "@temporalio/common": "^1.8.6", + "@temporalio/proto": "^1.8.6", + "@temporalio/testing": "^1.8.6", + "@temporalio/worker": "^1.8.6", + "@temporalio/workflow": "^1.8.6", "@types/base-64": "^1.0.0", "@types/cors": "^2.8.13", "@types/express": "^4.17.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d047242cf..19e6e2eb6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,26 +131,26 @@ devDependencies: specifier: ^2.0.2 version: 2.0.2(svelte@3.55.1)(vite@4.0.5) '@temporalio/activity': - specifier: ^1.8.4 - version: 1.8.4 + specifier: ^1.8.6 + version: 1.8.6 '@temporalio/client': - specifier: ^1.8.4 - version: 1.8.4 + specifier: ^1.8.6 + version: 1.8.6 '@temporalio/common': - specifier: ^1.8.4 - version: 1.8.4 + specifier: ^1.8.6 + version: 1.8.6 '@temporalio/proto': - specifier: ^1.8.4 - version: 1.8.4 + specifier: ^1.8.6 + version: 1.8.6 '@temporalio/testing': - specifier: ^1.8.4 - version: 1.8.4(esbuild@0.13.15) + specifier: ^1.8.6 + version: 1.8.6(esbuild@0.13.15) '@temporalio/worker': - specifier: ^1.8.4 - version: 1.8.4(esbuild@0.13.15) + specifier: ^1.8.6 + version: 1.8.6(esbuild@0.13.15) '@temporalio/workflow': - specifier: ^1.8.4 - version: 1.8.4 + specifier: ^1.8.6 + version: 1.8.6 '@types/base-64': specifier: ^1.0.0 version: 1.0.0 @@ -1844,64 +1844,64 @@ packages: '@swc/core-win32-x64-msvc': 1.3.35 dev: true - /@temporalio/activity@1.8.4: - resolution: {integrity: sha512-B28cD4ncq51R9HAQVmTMTl/qOZQLuqfRQ4fDTS5thDssHEkV4ciamoPYLyD+57yriAGoN+aWi+r5aqyfLdTqdQ==} + /@temporalio/activity@1.8.6: + resolution: {integrity: sha512-xTPIWIR1mL51Ub45nWpix+ATzJz/8ZAp0cCp3qw+xnKD8pwd4z9L0b30lr3co5kaPlWhSesdhypQ7VwWEf7ZHA==} dependencies: - '@temporalio/common': 1.8.4 + '@temporalio/common': 1.8.6 abort-controller: 3.0.0 dev: true - /@temporalio/client@1.8.4: - resolution: {integrity: sha512-0QnEk0gNia98jS68m0euhswpkRvHd/1E5OkKMSIuIMfI4ODRSDO6uKm9ZPJLdwFVPOKanKLlqk6B6Qt5BqFUaA==} + /@temporalio/client@1.8.6: + resolution: {integrity: sha512-lcDy9YeNrHU1thGpfsvxxHOLj8VKEbEthkAWQfyZghvvjQ3K7NHS7qRzYfB9ZbYnaus+ZiodSqnKTyqW9gsJ6A==} dependencies: '@grpc/grpc-js': 1.7.3 - '@temporalio/common': 1.8.4 - '@temporalio/proto': 1.8.4 + '@temporalio/common': 1.8.6 + '@temporalio/proto': 1.8.6 abort-controller: 3.0.0 long: 5.2.1 uuid: 8.3.2 dev: true - /@temporalio/common@1.8.4: - resolution: {integrity: sha512-jZhOc/tAfqAMbAnZ53WDVUwXUvot1TyvNgtuLeDmIvWnwfw/Lk5v106YoqFscdwDZ/pFbPrrbfxbvqOGdfDq4A==} + /@temporalio/common@1.8.6: + resolution: {integrity: sha512-7Cu1ymGkgklJM5xoYgJbppM/K/Dmq6Lb9bQnJiSvp8sb/RxXz6DQbiWypl9iDmaocUyFzDDaB/brA6abIq9xOQ==} dependencies: '@opentelemetry/api': 1.4.1 - '@temporalio/proto': 1.8.4 + '@temporalio/proto': 1.8.6 long: 5.2.1 ms: 3.0.0-canary.1 proto3-json-serializer: 1.1.0 - protobufjs: 7.2.2 + protobufjs: 7.2.5 dev: true - /@temporalio/core-bridge@1.8.4: - resolution: {integrity: sha512-iDmAKDzIPbXcUxn+5EADytTl9Ck72MKV/avldneSvyBvXjMAPxFhdpjEcRpvpLvdFCvogeSEy0juXstHyXD81A==} + /@temporalio/core-bridge@1.8.6: + resolution: {integrity: sha512-YkoG03qG7gqBWOupuhK1kt9A//ossKW2lOY9T22ZOTv2JfVZNgQPm0N0Kcusw0s4YULdiSGlLjalvRo4YTDoyQ==} requiresBuild: true dependencies: '@opentelemetry/api': 1.4.1 - '@temporalio/common': 1.8.4 + '@temporalio/common': 1.8.6 arg: 5.0.2 cargo-cp-artifact: 0.1.7 which: 2.0.2 dev: true - /@temporalio/proto@1.8.4: - resolution: {integrity: sha512-r1QSZbLl1gKXelwhr3F+ccIk+BnhCIGuYvWz3Jgo1PVqhVf698dw4hGnLjvaXA8x3QF55ysmjucYvjz9Pul51w==} + /@temporalio/proto@1.8.6: + resolution: {integrity: sha512-32ADOGSGBRinikP/XRTmyPXlIRaWjg5oAn64zj5NJRESrQy6ifpXb8bYVoPK01K9NIvcCL8FewCCWRKxxNvKZg==} dependencies: long: 5.2.1 - protobufjs: 7.2.2 + protobufjs: 7.2.5 dev: true - /@temporalio/testing@1.8.4(esbuild@0.13.15): - resolution: {integrity: sha512-cUOG/W8+fMxoz0WtUnSuHMlfAb6urDzWAk7qV02FhrM+syHG9s9pKDKvbQTr76nB1iplAYgUCwzh1kJzs5veAA==} + /@temporalio/testing@1.8.6(esbuild@0.13.15): + resolution: {integrity: sha512-sbSMfn18PjYsdHJRiDnfpNI33KK6BJvmud5C/uhZdxjVk6C2KLc2mt3lTZ6YGQ+K9ICiwztxWvxYNC/X2dfGBg==} dependencies: '@grpc/grpc-js': 1.7.3 - '@temporalio/activity': 1.8.4 - '@temporalio/client': 1.8.4 - '@temporalio/common': 1.8.4 - '@temporalio/core-bridge': 1.8.4 - '@temporalio/proto': 1.8.4 - '@temporalio/worker': 1.8.4(esbuild@0.13.15) - '@temporalio/workflow': 1.8.4 + '@temporalio/activity': 1.8.6 + '@temporalio/client': 1.8.6 + '@temporalio/common': 1.8.6 + '@temporalio/core-bridge': 1.8.6 + '@temporalio/proto': 1.8.6 + '@temporalio/worker': 1.8.6(esbuild@0.13.15) + '@temporalio/workflow': 1.8.6 abort-controller: 3.0.0 transitivePeerDependencies: - esbuild @@ -1909,18 +1909,18 @@ packages: - webpack-cli dev: true - /@temporalio/worker@1.8.4(esbuild@0.13.15): - resolution: {integrity: sha512-lYuN0a+4wABS3w2T4Bc0s8zMXL2oEic0Sa6TJ4ZA+UAjRIc2YNh7C+0pl8+WDLwhHMAC33CPoNg7xLZen5gdKA==} + /@temporalio/worker@1.8.6(esbuild@0.13.15): + resolution: {integrity: sha512-1T7TTkJwFJxrtAjRzHWpEoMlSrFw4b45aPx5oD8gpJCzkytHtjYjFLyPIuNOiFwsTzOJzffKnl2YIGinnn8emA==} engines: {node: '>= 14.18.0'} dependencies: '@opentelemetry/api': 1.4.1 '@swc/core': 1.3.35 - '@temporalio/activity': 1.8.4 - '@temporalio/client': 1.8.4 - '@temporalio/common': 1.8.4 - '@temporalio/core-bridge': 1.8.4 - '@temporalio/proto': 1.8.4 - '@temporalio/workflow': 1.8.4 + '@temporalio/activity': 1.8.6 + '@temporalio/client': 1.8.6 + '@temporalio/common': 1.8.6 + '@temporalio/core-bridge': 1.8.6 + '@temporalio/proto': 1.8.6 + '@temporalio/workflow': 1.8.6 abort-controller: 3.0.0 cargo-cp-artifact: 0.1.7 heap-js: 2.2.0 @@ -1937,11 +1937,11 @@ packages: - webpack-cli dev: true - /@temporalio/workflow@1.8.4: - resolution: {integrity: sha512-LtX+fxgHyvwEGKao9y7iBgaKEc8KVL0rWw7U1+hwITUo5LXBWpQqip8aOkDZ0+n64r8jvnDQ7W8QOPXaWLcD5Q==} + /@temporalio/workflow@1.8.6: + resolution: {integrity: sha512-ltVIqjNyJ3HvkhdA8RvcID6ylKZK8Fggw4mOSNnpPXkhGiCxA0oXuidaZK+MW9lTJzbm5EDiOvt3YctchCWK0Q==} dependencies: - '@temporalio/common': 1.8.4 - '@temporalio/proto': 1.8.4 + '@temporalio/common': 1.8.6 + '@temporalio/proto': 1.8.6 dev: true /@tootallnate/once@2.0.0: @@ -8205,7 +8205,7 @@ packages: resolution: {integrity: sha512-SjXwUWe/vANGs/mJJTbw5++7U67nwsymg7qsoPtw6GiXqw3kUy8ByojrlEdVE2efxAdKreX8WkDafxvYW95ZQg==} engines: {node: '>=12.0.0'} dependencies: - protobufjs: 7.2.2 + protobufjs: 7.2.5 dev: true /protobufjs@6.11.4: @@ -8247,6 +8247,25 @@ packages: long: 5.2.1 dev: true + /protobufjs@7.2.5: + resolution: {integrity: sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 16.18.11 + long: 5.2.1 + dev: true + /proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} diff --git a/server/proto/api/temporal/api/workflowservice/v1/service.proto b/server/proto/api/temporal/api/workflowservice/v1/service.proto index 5952e51ae..f6f7480b1 100644 --- a/server/proto/api/temporal/api/workflowservice/v1/service.proto +++ b/server/proto/api/temporal/api/workflowservice/v1/service.proto @@ -398,7 +398,7 @@ service WorkflowService { // GetSystemInfo returns information about the system. rpc GetSystemInfo(GetSystemInfoRequest) returns (GetSystemInfoResponse) { option (google.api.http) = { - get: "/api/v1/systemInfo" + get: "/api/v1/system-info" }; } diff --git a/src/lib/components/workflow-status.svelte b/src/lib/components/workflow-status.svelte index 8e9f58dc8..8d36937b4 100644 --- a/src/lib/components/workflow-status.svelte +++ b/src/lib/components/workflow-status.svelte @@ -1,6 +1,7 @@ -
- +
+ + {#if loading} + + {:else if count} + {count.toLocaleString()} + {/if} {label[status]} {#if status === 'Running'} {/if}
+ + diff --git a/src/lib/components/workflow/workflow-count-all.svelte b/src/lib/components/workflow/workflow-count-all.svelte new file mode 100644 index 000000000..ba541902a --- /dev/null +++ b/src/lib/components/workflow/workflow-count-all.svelte @@ -0,0 +1,17 @@ + + +
+

+ {#if $loading || $updating} + + {:else if count >= 0} + {count.toLocaleString()} {translate('workflows').toLocaleLowerCase()} + {/if} +

+
diff --git a/src/lib/components/workflow/workflow-count-status.svelte b/src/lib/components/workflow/workflow-count-status.svelte new file mode 100644 index 000000000..d23fbde22 --- /dev/null +++ b/src/lib/components/workflow/workflow-count-status.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/workflow/workflow-count.svelte b/src/lib/components/workflow/workflow-count.svelte deleted file mode 100644 index c1c51fe18..000000000 --- a/src/lib/components/workflow/workflow-count.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
onStatusClick(status)} - on:keypress={() => onStatusClick(status)} -> -

{count.toLocaleString()}

- -
- - diff --git a/src/lib/components/workflow/workflow-counts.svelte b/src/lib/components/workflow/workflow-counts.svelte index 8c85a6e32..c65be5a4c 100644 --- a/src/lib/components/workflow/workflow-counts.svelte +++ b/src/lib/components/workflow/workflow-counts.svelte @@ -1,90 +1,44 @@ -
- {#each workflowStatuses as status} - filter.value === status)} - /> +
+ + {#each statusGroups as { count, status } (status)} + {/each}
diff --git a/src/lib/components/workflow/workflows-summary-configurable-table/tests/__snapshots__/table-body-cell.test.ts.snap b/src/lib/components/workflow/workflows-summary-configurable-table/tests/__snapshots__/table-body-cell.test.ts.snap index 230042b79..050c84102 100644 --- a/src/lib/components/workflow/workflows-summary-configurable-table/tests/__snapshots__/table-body-cell.test.ts.snap +++ b/src/lib/components/workflow/workflows-summary-configurable-table/tests/__snapshots__/table-body-cell.test.ts.snap @@ -18,7 +18,7 @@ exports[`Table_body_cell$ > Start renders 1`] = `" State Transitions renders 1`] = `"12"`; -exports[`Table_body_cell$ > Status renders 1`] = `"
Running
"`; +exports[`Table_body_cell$ > Status renders 1`] = `"
Running
"`; exports[`Table_body_cell$ > Task Queue renders 1`] = `"task-queue"`; diff --git a/src/lib/components/workflow/workflows-summary-configurable-table/tests/__snapshots__/table-header-cell.test.ts.snap b/src/lib/components/workflow/workflows-summary-configurable-table/tests/__snapshots__/table-header-cell.test.ts.snap index 0bed8ad25..005cb1d06 100644 --- a/src/lib/components/workflow/workflows-summary-configurable-table/tests/__snapshots__/table-header-cell.test.ts.snap +++ b/src/lib/components/workflow/workflows-summary-configurable-table/tests/__snapshots__/table-header-cell.test.ts.snap @@ -18,7 +18,7 @@ exports[`Table_header_cell$ > Start renders 1`] = `" State Transitions renders 1`] = `"State Transitions"`; -exports[`Table_header_cell$ > Status renders 1`] = `"
"`; +exports[`Table_header_cell$ > Status renders 1`] = `"
"`; exports[`Table_header_cell$ > Task Queue renders 1`] = `"Task Queue"`; diff --git a/src/lib/pages/workflows-with-new-search.svelte b/src/lib/pages/workflows-with-new-search.svelte index e2139a018..5af630f16 100644 --- a/src/lib/pages/workflows-with-new-search.svelte +++ b/src/lib/pages/workflows-with-new-search.svelte @@ -56,6 +56,7 @@ import WorkflowDateTimeFilter from '$lib/components/workflow/dropdown-filter/workflow-datetime-filter.svelte'; import WorkflowFilterSearch from '$lib/components/workflow/filter-search/index.svelte'; import WorkflowAdvancedSearch from '$lib/components/workflow/workflow-advanced-search.svelte'; + import WorkflowCounts from '$lib/components/workflow/workflow-counts.svelte'; import WorkflowsSummaryConfigurableTable from '$lib/components/workflow/workflows-summary-configurable-table.svelte'; import IconButton from '$lib/holocene/icon-button.svelte'; import LabsModeGuard from '$lib/holocene/labs-mode-guard.svelte'; @@ -69,6 +70,7 @@ } from '$lib/services/batch-service'; import { supportsAdvancedVisibility } from '$lib/stores/advanced-visibility'; import { persistedTimeFilter, workflowFilters } from '$lib/stores/filters'; + import { groupByCountEnabled } from '$lib/stores/group-by-enabled'; import { labsMode } from '$lib/stores/labs-mode'; import { lastUsedNamespace } from '$lib/stores/namespaces'; import { searchAttributes } from '$lib/stores/search-attributes'; @@ -209,50 +211,53 @@ on:confirm={cancelWorkflows} /> -
-
-

- -

+
+
+
+

+ +

+
+ {#if $workflowCount?.totalCount >= 0 && $supportsAdvancedVisibility && !$groupByCountEnabled} +

+ {#if $loading || $updating} + + {:else if query} + + {:else} + + {/if} +

+ {/if} +
+
- {#if $workflowCount?.totalCount >= 0 && $supportsAdvancedVisibility} -

- {#if $loading || $updating} - - {:else if query} - - {:else} - - {/if} -

- {/if} - {#if !$loading && !$updating && $workflows.length > 0} -
- exportWorkflows($workflows)} - >{translate('download-json')} - {/if} + exportWorkflows($workflows)} + >{translate('download-json')} +
-
- -
+ {#if $groupByCountEnabled} + + {/if}
+
diff --git a/src/lib/services/cluster-service.ts b/src/lib/services/cluster-service.ts index b7e05e2cd..dfa447326 100644 --- a/src/lib/services/cluster-service.ts +++ b/src/lib/services/cluster-service.ts @@ -1,5 +1,4 @@ -import { cluster } from '$lib/stores/cluster'; -import type { GetClusterInfoResponse } from '$lib/types'; +import type { GetClusterInfoResponse, GetSystemInfoResponse } from '$lib/types'; import type { Settings } from '$lib/types/global'; import { requestFromAPI } from '$lib/utilities/request-from-api'; import { routeForApi } from '$lib/utilities/route-for-api'; @@ -14,7 +13,20 @@ export const fetchCluster = async ( return await requestFromAPI(route, { request, }).then((clusterInformation) => { - cluster.set(clusterInformation); return clusterInformation; }); }; + +export const fetchSystemInfo = async ( + settings: Settings, + request = fetch, +): Promise => { + if (settings.runtimeEnvironment.isCloud) return; + + const route = routeForApi('systemInfo'); + return await requestFromAPI(route, { + request, + }).then((systemInformation) => { + return systemInformation; + }); +}; diff --git a/src/lib/services/settings-service.ts b/src/lib/services/settings-service.ts index 71889146a..9ade90711 100644 --- a/src/lib/services/settings-service.ts +++ b/src/lib/services/settings-service.ts @@ -1,6 +1,5 @@ import { BROWSER } from 'esm-env'; -import { settings } from '$lib/stores/settings'; import type { SettingsResponse } from '$lib/types'; import type { Settings } from '$lib/types/global'; import { getApiOrigin } from '$lib/utilities/get-api-origin'; @@ -62,7 +61,5 @@ export const fetchSettings = async (request = fetch): Promise => { version: settingsResponse?.Version, }; - settings.set(settingsInformation); - return settingsInformation; }; diff --git a/src/lib/services/workflow-counts.ts b/src/lib/services/workflow-counts.ts new file mode 100644 index 000000000..e7925aa69 --- /dev/null +++ b/src/lib/services/workflow-counts.ts @@ -0,0 +1,81 @@ +import { noop } from 'svelte/internal'; + +import type { WorkflowFilter } from '$lib/models/workflow-filters'; +import type { CountWorkflowExecutionsResponse } from '$lib/types/workflows'; +import { toListWorkflowQueryFromFilters } from '$lib/utilities/query/filter-workflow-query'; +import { combineFilters } from '$lib/utilities/query/to-list-workflow-filters'; +import { requestFromAPI } from '$lib/utilities/request-from-api'; +import { routeForApi } from '$lib/utilities/route-for-api'; + +export const fetchWorkflowCount = async ( + namespace: string, + query: string, + request = fetch, +): Promise<{ totalCount: number; count: number }> => { + let totalCount = 0; + let count = 0; + try { + const countRoute = routeForApi('workflows.count', { namespace }); + if (!query) { + const totalCountResult = await requestFromAPI<{ count: string }>( + countRoute, + { + params: {}, + onError: noop, + handleError: noop, + request, + }, + ); + totalCount = parseInt(totalCountResult?.count); + } else { + const countPromise = requestFromAPI<{ count: string }>(countRoute, { + params: { query }, + onError: noop, + handleError: noop, + request, + }); + const totalCountPromise = requestFromAPI<{ count: string }>(countRoute, { + params: { query: '' }, + onError: noop, + handleError: noop, + request, + }); + const [countResult, totalCountResult] = await Promise.all([ + countPromise, + totalCountPromise, + ]); + count = parseInt(countResult?.count ?? '0'); + totalCount = parseInt(totalCountResult?.count); + } + } catch (e) { + // Don't fail the workflows call due to count + } + + return { count, totalCount }; +}; + +type WorkflowCountByExecutionStatusOptions = { + namespace: string; + filters: WorkflowFilter[]; +}; + +export const fetchWorkflowCountByExecutionStatus = async ({ + namespace, + filters, +}: WorkflowCountByExecutionStatusOptions): Promise => { + try { + const groupByClause = 'GROUP BY ExecutionStatus'; + const countRoute = routeForApi('workflows.count', { + namespace, + }); + const query = toListWorkflowQueryFromFilters(combineFilters(filters)); + const { count, groups } = + await requestFromAPI(countRoute, { + params: { query: `${query} ${groupByClause}` }, + notifyOnError: false, + }); + return { count, groups }; + } catch (e) { + return { count: '0', groups: [] }; + } +}; diff --git a/src/lib/services/workflow-service.ts b/src/lib/services/workflow-service.ts index a05785f4f..cba87c175 100644 --- a/src/lib/services/workflow-service.ts +++ b/src/lib/services/workflow-service.ts @@ -1,4 +1,3 @@ -import { noop } from 'svelte/internal'; import { get } from 'svelte/store'; import { v4 } from 'uuid'; @@ -88,53 +87,6 @@ export type FetchWorkflow = | typeof fetchAllWorkflows | typeof fetchAllArchivedWorkflows; -export const fetchWorkflowCount = async ( - namespace: string, - query: string, - request = fetch, -): Promise<{ totalCount: number; count: number }> => { - let totalCount = 0; - let count = 0; - try { - const countRoute = routeForApi('workflows.count', { namespace }); - if (!query) { - const totalCountResult = await requestFromAPI<{ count: string }>( - countRoute, - { - params: {}, - onError: noop, - handleError: noop, - request, - }, - ); - totalCount = parseInt(totalCountResult?.count); - } else { - const countPromise = requestFromAPI<{ count: string }>(countRoute, { - params: { query }, - onError: noop, - handleError: noop, - request, - }); - const totalCountPromise = requestFromAPI<{ count: string }>(countRoute, { - params: { query: '' }, - onError: noop, - handleError: noop, - request, - }); - const [countResult, totalCountResult] = await Promise.all([ - countPromise, - totalCountPromise, - ]); - count = parseInt(countResult?.count ?? '0'); - totalCount = parseInt(totalCountResult?.count); - } - } catch (e) { - // Don't fail the workflows call due to count - } - - return { count, totalCount }; -}; - export const fetchAllWorkflows = async ( namespace: string, parameters: ValidWorkflowParameters, diff --git a/src/lib/stores/bulk-actions.test.ts b/src/lib/stores/bulk-actions.test.ts index abf60b5d4..aa4a8f2f1 100644 --- a/src/lib/stores/bulk-actions.test.ts +++ b/src/lib/stores/bulk-actions.test.ts @@ -1,10 +1,8 @@ import { get, type writable as writableFunc } from 'svelte/store'; -import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { describe, expect, test, vi } from 'vitest'; import { supportsBulkActions } from './bulk-actions'; -import { cluster } from './cluster'; -import { settings } from './settings'; const mockedPageStore = await vi.hoisted(async () => { const { writable } = await vi.importActual<{ @@ -24,16 +22,15 @@ vi.mock('$app/stores', () => ({ describe('supportsBulkActions store', () => { describe('for Cloud', () => { - beforeEach(() => { - mockedPageStore.mockSetSubscribeValue({ - data: { settings: { runtimeEnvironment: { isCloud: true } } }, - }); - }); - test('returns true when batch actions are enabled, and visibility store is elasticsearch regardless of server version', () => { - cluster.set({ serverVersion: '1.0.0', visibilityStore: 'elasticsearch' }); - settings.set({ - batchActionsDisabled: false, + mockedPageStore.mockSetSubscribeValue({ + data: { + settings: { + runtimeEnvironment: { isCloud: true }, + batchActionsDisabled: false, + }, + cluster: { serverVersion: '1.0.0', visibilityStore: 'elasticsearch' }, + }, }); expect(get(supportsBulkActions)).toBe(true); @@ -41,50 +38,67 @@ describe('supportsBulkActions store', () => { }); describe('for Local', () => { - beforeEach(() => { - mockedPageStore.mockSetSubscribeValue({ - data: { settings: { runtimeEnvironment: { isCloud: false } } }, - }); - }); test('returns true when version is newer than 1.18.0, advanced visibility is supported, and batch actions are enabled', () => { - cluster.set({ - serverVersion: '1.19.0', - visibilityStore: 'elasticsearch', - }); - settings.set({ - batchActionsDisabled: false, + mockedPageStore.mockSetSubscribeValue({ + data: { + settings: { + runtimeEnvironment: { isCloud: false }, + batchActionsDisabled: false, + }, + cluster: { + serverVersion: '1.19.0', + visibilityStore: 'elasticsearch', + }, + }, }); expect(get(supportsBulkActions)).toBe(true); }); test('returns false when version is older, even if visibility store is elasticsearch and batch actions are enabled', () => { - cluster.set({ - serverVersion: '1.17.0', - visibilityStore: 'elasticsearch', - }); - settings.set({ - batchActionsDisabled: false, + mockedPageStore.mockSetSubscribeValue({ + data: { + settings: { + runtimeEnvironment: { isCloud: false }, + batchActionsDisabled: false, + }, + cluster: { + serverVersion: '1.17.0', + visibilityStore: 'elasticsearch', + }, + }, }); expect(get(supportsBulkActions)).toBe(false); }); test('returns false when advanced visibility store is not elasticsearch, even if version is newer and batch actions are enabled', () => { - cluster.set({ serverVersion: '1.19.0', visibilityStore: 'mysql' }); - settings.set({ - batchActionsDisabled: false, + mockedPageStore.mockSetSubscribeValue({ + data: { + settings: { + runtimeEnvironment: { isCloud: false }, + batchActionsDisabled: false, + }, + cluster: { serverVersion: '1.19.0', visibilityStore: 'mysql' }, + }, }); expect(get(supportsBulkActions)).toBe(false); }); test('returns false when batch actions are not enabled, even if version is newer and advanced visibility is supported', () => { - cluster.set({ - serverVersion: '1.19.0', - visibilityStore: 'elasticsearch', + mockedPageStore.mockSetSubscribeValue({ + data: { + settings: { + runtimeEnvironment: { isCloud: false }, + batchActionsDisabled: true, + }, + cluster: { + serverVersion: '1.19.0', + visibilityStore: 'elasticsearch', + }, + }, }); - settings.set({ batchActionsDisabled: true }); expect(get(supportsBulkActions)).toBe(false); }); diff --git a/src/lib/stores/cluster.ts b/src/lib/stores/cluster.ts index 8cadb61f9..297683249 100644 --- a/src/lib/stores/cluster.ts +++ b/src/lib/stores/cluster.ts @@ -1,5 +1,7 @@ -import { writable } from 'svelte/store'; +import { derived } from 'svelte/store'; -import type { GetClusterInfoResponse } from '$lib/types'; +import { page } from '$app/stores'; -export const cluster = writable({}); +export const cluster = derived([page], ([$page]) => { + return $page.data?.cluster; +}); diff --git a/src/lib/stores/group-by-enabled.ts b/src/lib/stores/group-by-enabled.ts new file mode 100644 index 000000000..173756a7f --- /dev/null +++ b/src/lib/stores/group-by-enabled.ts @@ -0,0 +1,9 @@ +import { derived } from 'svelte/store'; + +import { page } from '$app/stores'; + +export const groupByCountEnabled = derived([page], ([$page]) => { + return Boolean( + $page.data?.systemInfo?.capabilities?.countGroupByExecutionStatus, + ); +}); diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts index 90bd0f49d..15242ad54 100644 --- a/src/lib/stores/settings.ts +++ b/src/lib/stores/settings.ts @@ -1,33 +1,5 @@ -import { writable } from 'svelte/store'; +import { derived } from 'svelte/store'; -import type { Settings } from '$lib/types/global'; +import { page } from '$app/stores'; -export const settings = writable({ - auth: { - enabled: false, - options: null, - }, - baseUrl: '', - codec: { - endpoint: '', - passAccessToken: false, - includeCredentials: false, - }, - defaultNamespace: null, - disableWriteActions: false, - batchActionsDisabled: false, - workflowTerminateDisabled: false, - workflowCancelDisabled: false, - workflowSignalDisabled: false, - workflowResetDisabled: false, - hideWorkflowQueryErrors: false, - showTemporalSystemNamespace: false, - notifyOnNewVersion: false, - feedbackURL: '', - runtimeEnvironment: { - isCloud: false, - isLocal: true, - envOverride: true, - }, - version: '', -}); +export const settings = derived([page], ([$page]) => $page.data.settings); diff --git a/src/lib/stores/workflows.ts b/src/lib/stores/workflows.ts index 9a5c2a323..abce870bc 100644 --- a/src/lib/stores/workflows.ts +++ b/src/lib/stores/workflows.ts @@ -1,17 +1,16 @@ import type { StartStopNotifier } from 'svelte/store'; -import { derived, readable, writable } from 'svelte/store'; +import { derived, get, readable, writable } from 'svelte/store'; import { page } from '$app/stores'; import { translate } from '$lib/i18n/translate'; -import { - fetchAllWorkflows, - fetchWorkflowCount, -} from '$lib/services/workflow-service'; +import { fetchWorkflowCount } from '$lib/services/workflow-counts'; +import { fetchAllWorkflows } from '$lib/services/workflow-service'; import type { FilterParameters, WorkflowExecution } from '$lib/types/workflows'; import { withLoading } from '$lib/utilities/stores/with-loading'; import { supportsAdvancedVisibility } from './advanced-visibility'; +import { groupByCountEnabled } from './group-by-enabled'; export const refresh = writable(0); export const hideWorkflowQueryErrors = derived( @@ -64,7 +63,7 @@ const updateWorkflows: StartStopNotifier = (set) => { }); set(workflows); - if (supportsAdvancedVisibility) { + if (supportsAdvancedVisibility && !get(groupByCountEnabled)) { const workflowCount = await fetchWorkflowCount(namespace, query); setCounts(workflowCount); } diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts index daebd3dc4..0c2e9f96c 100644 --- a/src/lib/types/api.ts +++ b/src/lib/types/api.ts @@ -25,6 +25,7 @@ export type NamespaceAPIRoutePath = 'namespace'; export type TaskQueueAPIRoutePath = 'task-queue' | 'task-queue.compatibility'; export type ParameterlessAPIRoutePath = + | 'systemInfo' | 'cluster' | 'settings' | 'user' diff --git a/src/lib/types/global.ts b/src/lib/types/global.ts index 94015b46e..3942f2df5 100644 --- a/src/lib/types/global.ts +++ b/src/lib/types/global.ts @@ -106,6 +106,7 @@ export type User = { [key: string]: any; }; export type ClusterInformation = import('$lib/types').GetClusterInfoResponse; +export type SystemInformation = import('$lib/types').GetSystemInfoResponse; export type SelectOptionValue = number | string | boolean; diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 907cf39db..077f82075 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -10,6 +10,8 @@ export type ListNamespacesResponse = temporal.api.workflowservice.v1.IListNamespacesResponse; export type GetClusterInfoResponse = temporal.api.workflowservice.v1.IGetClusterInfoResponse; +export type GetSystemInfoResponse = + temporal.api.workflowservice.v1.IGetSystemInfoResponse; export type GetWorkflowExecutionHistoryResponse = temporal.api.workflowservice.v1.IGetWorkflowExecutionHistoryResponse; export type GetSearchAttributesResponse = diff --git a/src/lib/types/workflows.ts b/src/lib/types/workflows.ts index 41280ef05..4d7c69dff 100644 --- a/src/lib/types/workflows.ts +++ b/src/lib/types/workflows.ts @@ -1,4 +1,4 @@ -import type { WorkflowVersionTimpstamp } from '$lib/types'; +import type { Payloads, WorkflowVersionTimpstamp } from '$lib/types'; import type { Payload, @@ -30,6 +30,11 @@ export type ListWorkflowExecutionsResponse = Replace< Optional<{ executions: WorkflowExecutionInfo[] }> >; +export type CountWorkflowExecutionsResponse = { + count: string; + groups: { count: string; groupValues: Payloads }[]; +}; + export type WorkflowExecutionConfig = Replace< import('$lib/types').WorkflowExecutionConfig, { defaultWorkflowTaskTimeout: Duration } diff --git a/src/lib/utilities/route-for-api.ts b/src/lib/utilities/route-for-api.ts index c47fd81d5..f5315bf31 100644 --- a/src/lib/utilities/route-for-api.ts +++ b/src/lib/utilities/route-for-api.ts @@ -100,6 +100,7 @@ export function pathForApi( const routes: { [K in APIRoutePath]: string } = { cluster: '/cluster', + systemInfo: '/system-info', 'events.ascending': `/namespaces/${parameters?.namespace}/workflows/${parameters?.workflowId}/runs/${parameters?.runId}/events`, 'events.descending': `/namespaces/${parameters?.namespace}/workflows/${parameters?.workflowId}/runs/${parameters?.runId}/events/reverse`, namespaces: '/namespaces', diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 8f5564a72..d33367ada 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -2,8 +2,6 @@ import { afterNavigate, goto } from '$app/navigation'; import { page, updated } from '$app/stores'; - import type { PageData } from './$types'; - import DataEncoderSettings from '$lib/components/data-encoder-settings.svelte'; import SideNavigation from '$lib/components/side-nav.svelte'; import SkipNavigation from '$lib/components/skip-nav.svelte'; @@ -28,8 +26,6 @@ import type { DescribeNamespaceResponse as Namespace } from '$types'; - export let data: PageData; - let namespaceList: NamespaceListItem[]; $: isCloud = $page.data?.settings?.runtimeEnvironment?.isCloud; diff --git a/src/routes/(app)/+layout.ts b/src/routes/(app)/+layout.ts index 5ae99d1a4..23595c788 100644 --- a/src/routes/(app)/+layout.ts +++ b/src/routes/(app)/+layout.ts @@ -2,11 +2,11 @@ import { redirect } from '@sveltejs/kit'; import type { LayoutData, LayoutLoad } from './$types'; -import { fetchCluster } from '$lib/services/cluster-service'; +import { fetchCluster, fetchSystemInfo } from '$lib/services/cluster-service'; import { fetchNamespaces } from '$lib/services/namespaces-service'; import { fetchSettings } from '$lib/services/settings-service'; import { getAuthUser, setAuthUser } from '$lib/stores/auth-user'; -import type { GetClusterInfoResponse } from '$lib/types'; +import type { GetClusterInfoResponse, GetSystemInfoResponse } from '$lib/types'; import type { Settings } from '$lib/types/global'; import { cleanAuthUserCookie, @@ -37,10 +37,15 @@ export const load: LayoutLoad = async function ({ fetchNamespaces(settings, fetch); const cluster: GetClusterInfoResponse = await fetchCluster(settings, fetch); + const systemInfo: GetSystemInfoResponse = await fetchSystemInfo( + settings, + fetch, + ); return { user, settings, cluster, + systemInfo, }; }; diff --git a/tests/integration/workflows-count.spec.ts b/tests/integration/workflows-count.spec.ts new file mode 100644 index 000000000..d4f0fe8ae --- /dev/null +++ b/tests/integration/workflows-count.spec.ts @@ -0,0 +1,96 @@ +import { expect, test } from '@playwright/test'; + +import { + mockClusterApi, + mockSystemInfoApi, + mockWorkflowApis, + mockWorkflowsApis, + mockWorkflowsGroupByCountApi, + waitForWorkflowsApis, +} from '~/test-utilities/mock-apis'; + +test.describe('Workflows List with Counts when countGroupByExecutionStatus is disabled', () => { + test.beforeEach(async ({ page }) => { + await mockWorkflowsApis(page); + await mockWorkflowApis(page); + + await mockClusterApi(page, { + visibilityStore: 'elasticsearch', + persistenceStore: 'postgres,elasticsearch', + }); + + await mockSystemInfoApi(page, { + capabilities: { + countGroupByExecutionStatus: false, + }, + }); + + page.goto('/namespaces/default/workflows'); + + await waitForWorkflowsApis(page); + }); + + test.describe('Shows total count and result count', () => { + test('Results of workflows ', async ({ page }) => { + await page.waitForSelector( + '[data-testid="workflow-count"][data-loaded="true"]', + ); + + await expect(page.getByTestId('workflow-count')).toHaveText( + '15 workflows', + ); + + await page.fill('#manual-search', 'WorkflowType="ImportantWorkflowType"'); + await page.click('[data-testid="manual-search-button"]'); + + await expect(page).toHaveURL( + /WorkflowType%3D%22ImportantWorkflowType%22/, + ); + + await page.getByTestId('workflow-type-filter-button').click(); + const workflowTypeValue = await page.inputValue('#workflow-type'); + expect(workflowTypeValue).toBe('ImportantWorkflowType'); + await page.waitForSelector( + '[data-testid="workflow-count"][data-loaded="true"]', + ); + + await expect(page.getByTestId('workflow-count')).toHaveText( + 'Results 15 of 15 workflows', + ); + }); + }); +}); + +test.describe('Workflows List with Counts when countGroupByExecutionStatus is enabled', () => { + test.beforeEach(async ({ page }) => { + await mockWorkflowsApis(page); + await mockWorkflowApis(page); + + await mockClusterApi(page, { + visibilityStore: 'elasticsearch', + persistenceStore: 'postgres,elasticsearch', + }); + + page.goto('/namespaces/default/workflows'); + + await mockWorkflowsGroupByCountApi(page); + await waitForWorkflowsApis(page); + }); + + test.describe('Shows only result count', () => { + test('Counts of workflows ', async ({ page }) => { + await page.waitForSelector( + '[data-testid="workflow-count"][data-loaded="true"]', + ); + await expect(page.getByTestId('workflow-count')).toHaveText( + '31,230 workflows', + ); + await expect(page.getByTestId('workflow-status-Running')).toHaveText( + '6 Running', + ); + await expect(page.getByTestId('workflow-status-Completed')).toHaveText( + '21,652 Completed', + ); + }); + }); +}); diff --git a/tests/integration/workflows-list-with-advanced-visibility.spec.ts b/tests/integration/workflows-list-with-advanced-visibility.spec.ts index 1d57ab3a5..f335025b2 100644 --- a/tests/integration/workflows-list-with-advanced-visibility.spec.ts +++ b/tests/integration/workflows-list-with-advanced-visibility.spec.ts @@ -39,10 +39,6 @@ test.describe('Workflows List with Advanced Visibility', () => { await page.waitForSelector( '[data-testid="workflow-count"][data-loaded="true"]', ); - - await expect(page.getByTestId('workflow-count')).toHaveText( - 'Results 15 of 15 workflows', - ); }); }); @@ -158,10 +154,6 @@ test.describe('Workflows List with Advanced Visibility', () => { await page.waitForSelector( '[data-testid="workflow-count"][data-loaded="true"]', ); - - await expect(page.getByTestId('workflow-count')).toHaveText( - 'Results 15 of 15 workflows', - ); }); test('keeps workflow datetime filter after navigating away and back to workflow list', async ({ diff --git a/tests/test-utilities/mock-apis.ts b/tests/test-utilities/mock-apis.ts index b69fb44c4..078778f1a 100644 --- a/tests/test-utilities/mock-apis.ts +++ b/tests/test-utilities/mock-apis.ts @@ -14,6 +14,7 @@ import { mockNamespaceApi } from './mocks/namespace'; import { mockNamespacesApi, NAMESPACES_API } from './mocks/namespaces'; import { mockSearchAttributesApi } from './mocks/search-attributes'; import { mockSettingsApi, SETTINGS_API } from './mocks/settings'; +import { mockSystemInfoApi } from './mocks/system-info'; import { mockTaskQueuesApi, TASK_QUEUES_API } from './mocks/task-queues'; import { mockWorkflowApi, WORKFLOW_API } from './mocks/workflow'; import { mockWorkflowsApi, WORKFLOWS_API } from './mocks/workflows'; @@ -26,6 +27,8 @@ export { mockClusterApi, CLUSTER_API } from './mocks/cluster'; export { mockNamespaceApi, NAMESPACE_API } from './mocks/namespace'; export { mockNamespacesApi, NAMESPACES_API } from './mocks/namespaces'; export { mockSettingsApi, SETTINGS_API } from './mocks/settings'; +export { mockSystemInfoApi } from './mocks/system-info'; + export { mockSearchAttributesApi, SEARCH_ATTRIBUTES_API, @@ -34,6 +37,7 @@ export { mockWorkflowsApi, WORKFLOWS_API } from './mocks/workflows'; export { mockWorkflowApi, WORKFLOW_API } from './mocks/workflow'; export { mockWorkflowsCountApi, + mockWorkflowsGroupByCountApi, WORKFLOWS_COUNT_API, } from './mocks/workflows-count'; export { @@ -50,6 +54,7 @@ export const mockGlobalApis = (page: Page) => { mockClusterApi(page), mockNamespacesApi(page), mockSettingsApi(page), + mockSystemInfoApi(page), ]); }; diff --git a/tests/test-utilities/mocks/system-info.ts b/tests/test-utilities/mocks/system-info.ts new file mode 100644 index 000000000..a2ac1039a --- /dev/null +++ b/tests/test-utilities/mocks/system-info.ts @@ -0,0 +1,29 @@ +import type { Page } from '@playwright/test'; + +import { GetSystemInfoResponse } from '$src/lib/types'; + +export const SYSTEM_INFO_API = '**/api/v1/system-info**'; + +const defaultSystemInfo = { + serverVersion: '1.22.0', + capabilities: { + signalAndQueryHeader: true, + internalErrorDifferentiation: true, + activityFailureIncludeHeartbeat: true, + supportsSchedules: true, + encodedFailureAttributes: true, + buildIdBasedVersioning: true, + upsertMemo: true, + eagerWorkflowStart: true, + sdkMetadata: true, + countGroupByExecutionStatus: true, + }, +}; +export const mockSystemInfoApi = async ( + page: Page, + systemInfo: Partial = {}, +) => { + await page.route(SYSTEM_INFO_API, async (route) => { + route.fulfill({ json: { ...defaultSystemInfo, ...systemInfo } }); + }); +}; diff --git a/tests/test-utilities/mocks/workflows-count.ts b/tests/test-utilities/mocks/workflows-count.ts index 77dc1b268..a4f99fc05 100644 --- a/tests/test-utilities/mocks/workflows-count.ts +++ b/tests/test-utilities/mocks/workflows-count.ts @@ -7,3 +7,87 @@ export const mockWorkflowsCountApi = (page: Page) => { return route.fulfill({ json: { count: '15' } }); }); }; + +const groupByCountResponse = { + count: '31230', + groups: [ + { + groupValues: [ + { + metadata: { + encoding: 'anNvbi9wbGFpbg==', + type: 'S2V5d29yZA==', + }, + data: 'IlJ1bm5pbmci', + }, + ], + count: '6', + }, + { + groupValues: [ + { + metadata: { + encoding: 'anNvbi9wbGFpbg==', + type: 'S2V5d29yZA==', + }, + data: 'IkNvbXBsZXRlZCI=', + }, + ], + count: '21652', + }, + { + groupValues: [ + { + metadata: { + encoding: 'anNvbi9wbGFpbg==', + type: 'S2V5d29yZA==', + }, + data: 'IkZhaWxlZCI=', + }, + ], + count: '1932', + }, + { + groupValues: [ + { + metadata: { + encoding: 'anNvbi9wbGFpbg==', + type: 'S2V5d29yZA==', + }, + data: 'IkNhbmNlbGVkIg==', + }, + ], + count: '6215', + }, + { + groupValues: [ + { + metadata: { + encoding: 'anNvbi9wbGFpbg==', + type: 'S2V5d29yZA==', + }, + data: 'IlRlcm1pbmF0ZWQi', + }, + ], + count: '917', + }, + { + groupValues: [ + { + metadata: { + encoding: 'anNvbi9wbGFpbg==', + type: 'S2V5d29yZA==', + }, + data: 'IlRpbWVkT3V0Ig==', + }, + ], + count: '508', + }, + ], +}; + +export const mockWorkflowsGroupByCountApi = (page: Page) => { + return page.route(WORKFLOWS_COUNT_API, (route) => { + return route.fulfill({ json: groupByCountResponse }); + }); +};