diff --git a/src/lib/components/schedule/schedules-list/schedules-table-row.svelte b/src/lib/components/schedule/schedules-list/schedules-table-row.svelte
index 3629418f07..9926e5cb71 100644
--- a/src/lib/components/schedule/schedules-list/schedules-table-row.svelte
+++ b/src/lib/components/schedule/schedules-list/schedules-table-row.svelte
@@ -4,17 +4,19 @@
import { goto } from '$app/navigation';
import { page } from '$app/state';
+ import SearchAttributeValue from '$lib/components/table/search-attribute-value.svelte';
import Timestamp from '$lib/components/timestamp.svelte';
import WorkflowStatusBadge from '$lib/components/workflow/workflow-status-badge.svelte';
import Link from '$lib/holocene/link.svelte';
import TableCellWithFilterOrCopyButtons from '$lib/holocene/table/table-cell-with-filter-or-copy-buttons.svelte';
import { translate } from '$lib/i18n/translate';
import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns';
+ import { customSearchAttributes } from '$lib/stores/search-attributes';
import {
COLUMN_WIDTH_CLAMP_CLASSES,
columnWidthStyle,
} from '$lib/utilities/column-width';
- import { parsePayloadAttributes } from '$lib/utilities/decode-payload';
+ import { parseSearchAttributes } from '$lib/utilities/decode-payload';
import {
routeForSchedule,
routeForWorkflow,
@@ -36,9 +38,8 @@
const status = $derived(schedule?.info?.paused ? 'Paused' : 'Running');
const spec = $derived(schedule?.info?.spec);
- const searchAttributes = $derived(schedule?.searchAttributes ?? {});
- const decodedAttributes = $derived(
- parsePayloadAttributes({ searchAttributes }),
+ const decodedSearchAttributes = $derived(
+ parseSearchAttributes(schedule?.searchAttributes ?? {}).indexedFields ?? {},
);
const sortRecentActions = (recentActions: ScheduleActionResult[]) => {
@@ -142,7 +143,12 @@
{:else}
- {decodedAttributes?.searchAttributes?.indexedFields?.[label] ?? ''}
+ {#if decodedSearchAttributes[label] !== undefined}
+
+ {/if}
|
{/if}
{/each}
diff --git a/src/lib/components/standalone-activities/activities-summary-configurable-table.svelte b/src/lib/components/standalone-activities/activities-summary-configurable-table.svelte
index 784ba1f61f..3ac7433812 100644
--- a/src/lib/components/standalone-activities/activities-summary-configurable-table.svelte
+++ b/src/lib/components/standalone-activities/activities-summary-configurable-table.svelte
@@ -23,7 +23,7 @@
resizeColumn,
TABLE_TYPE,
} from '$lib/stores/configurable-table-columns';
- import type { ActivityExecutionInfo } from '$lib/types/activity-execution';
+ import type { ActivityExecutionListInfo } from '$lib/types/activity-execution';
import {
getBatchSelectionTargets,
getPageSelectionStatus,
@@ -58,7 +58,7 @@
standaloneActivityBulkActionsEnabled(page) && $supportsAdvancedVisibility,
);
- let visiblePaginatedItems: ActivityExecutionInfo[] = $state([]);
+ let visiblePaginatedItems: ActivityExecutionListInfo[] = $state([]);
const pageSelectionStatus: PageSelectionStatus = $derived(
getPageSelectionStatus(
@@ -72,7 +72,7 @@
const handleSelectPage = (
isSelected: boolean,
- activities: ActivityExecutionInfo[],
+ activities: ActivityExecutionListInfo[],
) => {
selectActivities(isSelected, activities);
prevClickedIndex = null;
diff --git a/src/lib/components/standalone-activities/activities-summary-configurable-table/batch-actions.svelte b/src/lib/components/standalone-activities/activities-summary-configurable-table/batch-actions.svelte
index bf7db83c3a..8653749a07 100644
--- a/src/lib/components/standalone-activities/activities-summary-configurable-table/batch-actions.svelte
+++ b/src/lib/components/standalone-activities/activities-summary-configurable-table/batch-actions.svelte
@@ -11,11 +11,11 @@
type ActivityBatchOperationContext,
} from '$lib/pages/standalone-activities.svelte';
import { activitiesQuery, activityCount } from '$lib/stores/activities';
- import type { ActivityExecutionInfo } from '$lib/types/activity-execution';
+ import type { ActivityExecutionListInfo } from '$lib/types/activity-execution';
import { standaloneActivityBulkActionsEnabled } from '$lib/utilities/standalone-activities-commands-disabled';
type Props = {
- activities: ActivityExecutionInfo[];
+ activities: ActivityExecutionListInfo[];
};
let { activities }: Props = $props();
diff --git a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-body-cell.svelte b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-body-cell.svelte
index f535a9111f..2a2d366a1c 100644
--- a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-body-cell.svelte
+++ b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-body-cell.svelte
@@ -5,9 +5,14 @@
import { page } from '$app/state';
import ActivityStatusBadge from '$lib/components/standalone-activities/activity-status-badge.svelte';
+ import SearchAttributeValue from '$lib/components/table/search-attribute-value.svelte';
import Timestamp from '$lib/components/timestamp.svelte';
import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns';
- import type { ActivityExecutionInfo } from '$lib/types/activity-execution';
+ import {
+ customSearchAttributes,
+ isFilterableCustomSearchAttribute,
+ } from '$lib/stores/search-attributes';
+ import type { ActivityExecutionListInfo } from '$lib/types/activity-execution';
import {
COLUMN_WIDTH_CLAMP_CLASSES,
columnWidthStyle,
@@ -21,7 +26,7 @@
type Props = {
column: ConfigurableTableHeader;
- activity: ActivityExecutionInfo;
+ activity: ActivityExecutionListInfo;
};
let { column, activity }: Props = $props();
@@ -30,6 +35,17 @@
const filterableLabels = ['Activity ID', 'Run ID', 'Type', 'Task Queue'];
+ const customSearchAttributeValue = $derived(
+ activity.searchAttributes?.indexedFields?.[label],
+ );
+ const isCustomKeywordOrTextAttribute = $derived(
+ isFilterableCustomSearchAttribute(
+ $customSearchAttributes,
+ label,
+ customSearchAttributeValue,
+ ),
+ );
+
const className = $derived(
twMerge(
'h-8 whitespace-nowrap',
@@ -43,7 +59,7 @@
{#snippet renderFilterableTableCell(
filterableCellProps: Pick<
ComponentProps,
- 'attribute' | 'value' | 'href'
+ 'attribute' | 'value' | 'href' | 'type'
>,
)}
{/snippet}
-{#if filterableLabels.includes(label)}
+{#if filterableLabels.includes(label) || isCustomKeywordOrTextAttribute}
{#if label === 'Activity ID'}
{@render renderFilterableTableCell({
attribute: 'ActivityId',
@@ -85,6 +101,12 @@
attribute: 'TaskQueue',
value: activity.taskQueue ?? '',
})}
+ {:else if isCustomKeywordOrTextAttribute}
+ {@render renderFilterableTableCell({
+ attribute: label,
+ value: customSearchAttributeValue ?? '',
+ type: $customSearchAttributes[label],
+ })}
{/if}
{:else}
@@ -105,6 +127,11 @@
{activity.stateTransitionCount ?? ''}
{:else if label === 'Execution Time'}
+ {:else if label in $customSearchAttributes && customSearchAttributeValue !== undefined}
+
{/if}
|
{/if}
diff --git a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-header-row.svelte b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-header-row.svelte
index 62eed514c3..9d3b50d34a 100644
--- a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-header-row.svelte
+++ b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-header-row.svelte
@@ -7,20 +7,20 @@
ACTIVITY_BATCH_OPERATION_CONTEXT,
type ActivityBatchOperationContext,
} from '$lib/pages/standalone-activities.svelte';
- import type { ActivityExecutionInfo } from '$lib/types/activity-execution';
+ import type { ActivityExecutionListInfo } from '$lib/types/activity-execution';
import type { PageSelectionStatus } from '$lib/utilities/batch-selection';
import BatchActions from './batch-actions.svelte';
type Props = {
- activities?: ActivityExecutionInfo[];
+ activities?: ActivityExecutionListInfo[];
empty?: boolean;
columnsCount?: number;
showBatchActions?: boolean;
pageSelectionStatus?: PageSelectionStatus;
onSelectPage?: (
selected: boolean,
- activities: ActivityExecutionInfo[],
+ activities: ActivityExecutionListInfo[],
) => void;
children?: Snippet;
};
diff --git a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-row.svelte b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-row.svelte
index 3ce3d83e3c..5cc0e8aec8 100644
--- a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-row.svelte
+++ b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-row.svelte
@@ -11,11 +11,11 @@
type ActivityBatchOperationContext,
activityKey,
} from '$lib/pages/standalone-activities.svelte';
- import type { ActivityExecutionInfo } from '$lib/types/activity-execution';
+ import type { ActivityExecutionListInfo } from '$lib/types/activity-execution';
import { standaloneActivityWriteActionsDisabled } from '$lib/utilities/standalone-activities-commands-disabled';
interface Props {
- activity?: ActivityExecutionInfo;
+ activity?: ActivityExecutionListInfo;
empty?: boolean;
showBatchActions?: boolean;
onClickBatchSelect?: (e: MouseEvent) => void;
diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte
index c031b3e4c7..7dba143ede 100644
--- a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte
+++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte
@@ -3,8 +3,13 @@
import { page } from '$app/state';
+ import SearchAttributeValue from '$lib/components/table/search-attribute-value.svelte';
import Timestamp from '$lib/components/timestamp.svelte';
import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns';
+ import {
+ customSearchAttributes,
+ isFilterableCustomSearchAttribute,
+ } from '$lib/stores/search-attributes';
import type { NexusOperationExecutionListInfo } from '$lib/types/nexus-operation-execution';
import {
COLUMN_WIDTH_CLAMP_CLASSES,
@@ -51,9 +56,20 @@
'Service',
'Operation',
];
+
+ const customSearchAttributeValue = $derived(
+ operation.searchAttributes?.indexedFields?.[label],
+ );
+ const isCustomKeywordOrTextAttribute = $derived(
+ isFilterableCustomSearchAttribute(
+ $customSearchAttributes,
+ label,
+ customSearchAttributeValue,
+ ),
+ );
-{#if filterableLabels.includes(label)}
+{#if filterableLabels.includes(label) || isCustomKeywordOrTextAttribute}
|
+ {:else if isCustomKeywordOrTextAttribute}
+
{/if}
{:else}
@@ -129,6 +152,11 @@
{/if}
{:else if label === 'State Transitions'}
{operation.stateTransitionCount ?? ''}
+ {:else if label in $customSearchAttributes && customSearchAttributeValue !== undefined}
+
{/if}
{/if}
diff --git a/src/lib/components/table/search-attribute-value.svelte b/src/lib/components/table/search-attribute-value.svelte
new file mode 100644
index 0000000000..dfb9fdc8c7
--- /dev/null
+++ b/src/lib/components/table/search-attribute-value.svelte
@@ -0,0 +1,46 @@
+
+
+{#if value != null}
+ {#if type === SEARCH_ATTRIBUTE_TYPE.DATETIME && typeof value === 'string'}
+
+ {:else if type === SEARCH_ATTRIBUTE_TYPE.BOOL}
+
+ {:else}
+
+ {truncate ? truncateValue(displayValue) : displayValue}
+
+ {/if}
+{/if}
diff --git a/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte b/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte
index 4b8a4b773d..6ba266bb16 100644
--- a/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte
+++ b/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte
@@ -4,20 +4,17 @@
import { page } from '$app/state';
+ import SearchAttributeValue from '$lib/components/table/search-attribute-value.svelte';
import Timestamp from '$lib/components/timestamp.svelte';
import WorkflowStatusBadge from '$lib/components/workflow/workflow-status-badge.svelte';
import Tooltip from '$lib/holocene/tooltip.svelte';
- import { Badge } from '$lib/io/badge';
import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns';
import {
customSearchAttributes,
- isCustomSearchAttribute,
+ isFilterableCustomSearchAttribute,
workflowIncludesSearchAttribute,
} from '$lib/stores/search-attributes';
- import {
- SEARCH_ATTRIBUTE_TYPE,
- type WorkflowExecution,
- } from '$lib/types/workflows';
+ import type { WorkflowExecution } from '$lib/types/workflows';
import {
COLUMN_WIDTH_CLAMP_CLASSES,
columnWidthStyle,
@@ -54,10 +51,11 @@
const { label, width } = $derived(column);
const namespace = $derived(page.params.namespace);
const isCustomKeywordOrTextAttribute = $derived(
- isCustomSearchAttribute(label) &&
- ($customSearchAttributes[label] === SEARCH_ATTRIBUTE_TYPE.KEYWORD ||
- $customSearchAttributes[label] === SEARCH_ATTRIBUTE_TYPE.TEXT) &&
- typeof workflow.searchAttributes?.indexedFields?.[label] === 'string',
+ isFilterableCustomSearchAttribute(
+ $customSearchAttributes,
+ label,
+ workflow.searchAttributes?.indexedFields?.[label],
+ ),
);
const filterableLabels = [
@@ -245,23 +243,12 @@
{/if}
{:else if label === 'Change Version'}
{workflow.searchAttributes?.indexedFields?.TemporalChangeVersion}
- {:else if isCustomSearchAttribute(label) && workflowIncludesSearchAttribute(workflow, label)}
- {@const content = workflow.searchAttributes?.indexedFields?.[label]}
- {#if $customSearchAttributes[label] === SEARCH_ATTRIBUTE_TYPE.DATETIME && typeof content === 'string'}
-
- {:else if $customSearchAttributes[label] === SEARCH_ATTRIBUTE_TYPE.BOOL}
-
- {:else}
-
- {truncate ? truncateValue(content) : content}
-
- {/if}
+ {:else if label in $customSearchAttributes && workflowIncludesSearchAttribute(workflow, label)}
+
{/if}
{/if}
diff --git a/src/lib/models/workflow-execution.ts b/src/lib/models/workflow-execution.ts
index c8453ae4f6..8545b92b65 100644
--- a/src/lib/models/workflow-execution.ts
+++ b/src/lib/models/workflow-execution.ts
@@ -1,5 +1,3 @@
-import { mapValues } from 'es-toolkit';
-
import type {
Callbacks,
PendingActivity,
@@ -10,14 +8,12 @@ import type {
} from '$lib/types/events';
import type { Callback } from '$lib/types/nexus';
import type {
- DecodedWorkflowSearchAttributes,
ListWorkflowExecutionsResponse,
MostRecentWorkflowVersionStamp,
WorkflowExecution,
WorkflowExecutionAPIResponse,
- WorkflowSearchAttributes,
} from '$lib/types/workflows';
-import { parseRawPayloadToJSON } from '$lib/utilities/decode-payload';
+import { parseSearchAttributes } from '$lib/utilities/decode-payload';
import {
toCallbackStateReadable,
toPendingActivityStateReadable,
@@ -67,19 +63,6 @@ const toCallbacks = (callbacks?: Callbacks): Callback[] => {
});
};
-const toSearchAttributes = (
- apiSearchAttributes: WorkflowSearchAttributes,
-): DecodedWorkflowSearchAttributes => {
- if (!apiSearchAttributes || !apiSearchAttributes.indexedFields) return {};
- const decoded = mapValues(apiSearchAttributes.indexedFields, (payload) =>
- parseRawPayloadToJSON(payload),
- ) as Record;
-
- return {
- indexedFields: decoded,
- };
-};
-
const getStartDelay = ({
executionTime,
startTime,
@@ -100,7 +83,7 @@ export const toWorkflowExecution = (
response?: WorkflowExecutionAPIResponse,
): WorkflowExecution => {
const info = response?.workflowExecutionInfo;
- const searchAttributes = toSearchAttributes(info?.searchAttributes ?? {});
+ const searchAttributes = parseSearchAttributes(info?.searchAttributes ?? {});
const memo = info?.memo ?? {};
const name = info?.type?.name ?? '';
const id = info?.execution?.workflowId ?? '';
diff --git a/src/lib/pages/standalone-activities.svelte b/src/lib/pages/standalone-activities.svelte
index 42dddbc91e..660098ec73 100644
--- a/src/lib/pages/standalone-activities.svelte
+++ b/src/lib/pages/standalone-activities.svelte
@@ -1,27 +1,27 @@
@@ -120,9 +120,9 @@
selectItems: selectActivities,
handleSelectAll,
reset: resetSelection,
- } = createBatchSelection(activityKey);
+ } = createBatchSelection(activityKey);
- const isRunning = (activity: ActivityExecutionInfo) =>
+ const isRunning = (activity: ActivityExecutionListInfo) =>
activity.status === 'ACTIVITY_EXECUTION_STATUS_RUNNING';
const cancelableActivities = derivedStore(selectedActivities, (activities) =>
diff --git a/src/lib/pages/standalone-activity-workers.svelte b/src/lib/pages/standalone-activity-workers.svelte
index 61ccb11d1a..a1bd3ea80e 100644
--- a/src/lib/pages/standalone-activity-workers.svelte
+++ b/src/lib/pages/standalone-activity-workers.svelte
@@ -1,12 +1,9 @@
({
+}: ActivityExecutionListInfo): Execution => ({
type: ExecutionType.EXECUTION_TYPE_ACTIVITY,
businessId: activityId ?? '',
runId: runId ?? '',
diff --git a/src/lib/services/standalone-activities.test.ts b/src/lib/services/standalone-activities.test.ts
deleted file mode 100644
index 9c3110394b..0000000000
--- a/src/lib/services/standalone-activities.test.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import type { StandaloneActivityFormData } from '$lib/components/standalone-activities/start-standalone-activity-form/types';
-
-import {
- toActivityCallbacks,
- toStartActivityExecutionRequest,
-} from './standalone-activities';
-
-const baseFormData: StandaloneActivityFormData = {
- identity: 'test',
- namespace: 'default',
- activityId: 'activity-1',
- taskQueue: 'task-queue',
- activityType: 'MyActivity',
- startToCloseTimeout: '',
- scheduleToCloseTimeout: '',
- scheduleToStartTimeout: '',
- input: '',
- encoding: 'json/plain',
- messageType: '',
- searchAttributes: [],
- summary: '',
- details: '',
- initialInterval: '',
- backoffCoefficient: '',
- maximumInterval: '',
- maximumAttempts: '',
-};
-
-describe('toStartActivityExecutionRequest', () => {
- it('omits retry policy numbers left empty', async () => {
- const request = await toStartActivityExecutionRequest(baseFormData);
-
- expect(request.retryPolicy).toEqual({});
- });
-
- it('coerces populated retry policy numbers to numbers', async () => {
- const request = await toStartActivityExecutionRequest({
- ...baseFormData,
- backoffCoefficient: '2',
- maximumAttempts: '5',
- });
-
- expect(request.retryPolicy?.backoffCoefficient).toBe(2);
- expect(request.retryPolicy?.maximumAttempts).toBe(5);
- });
-
- it('omits timeouts left empty and forwards provided ones', async () => {
- const empty = await toStartActivityExecutionRequest(baseFormData);
-
- expect(empty.startToCloseTimeout).toBeUndefined();
- expect(empty.scheduleToCloseTimeout).toBeUndefined();
-
- const provided = await toStartActivityExecutionRequest({
- ...baseFormData,
- startToCloseTimeout: '30s',
- });
-
- expect(provided.startToCloseTimeout).toBe('30s');
- });
-});
-
-describe('toActivityCallbacks', () => {
- const activityCallbacks = [
- {
- trigger: {},
- info: {
- callback: {
- nexus: { url: 'http://callback' },
- links: [
- {
- workflowEvent: {
- namespace: 'caller-ns',
- workflowId: 'caller-wf',
- runId: 'caller-run',
- eventRef: { eventId: '7' },
- },
- },
- ],
- },
- state: 'CALLBACK_STATE_SUCCEEDED',
- attempt: 2,
- },
- },
- ] as unknown as Parameters[0];
-
- it('extracts nested info and makes state readable', () => {
- const [callback] = toActivityCallbacks(activityCallbacks);
-
- expect(callback.state).toBe('Succeeded');
- expect(callback.attempt).toBe(2);
- expect(callback.callback?.links).toHaveLength(1);
- expect(callback.callback?.nexus?.url).toBe('http://callback');
- });
-
- it('skips callbacks without info and handles empty input', () => {
- expect(toActivityCallbacks(undefined)).toEqual([]);
- expect(
- toActivityCallbacks([{}] as unknown as Parameters<
- typeof toActivityCallbacks
- >[0]),
- ).toEqual([]);
- });
-});
diff --git a/src/lib/services/standalone-activities.ts b/src/lib/services/standalone-activities.ts
index a51fa9b34b..030a07ee20 100644
--- a/src/lib/services/standalone-activities.ts
+++ b/src/lib/services/standalone-activities.ts
@@ -20,11 +20,15 @@ import type { ActivityOptions, Payload, SearchAttribute } from '$lib/types';
import type {
ActivityExecution,
ActivityExecutionInfo,
+ ActivityExecutionListInfo,
StartActivityExecutionRequest,
} from '$lib/types/activity-execution';
import type { Callback } from '$lib/types/nexus';
import { activityOptionsUpdateMask } from '$lib/utilities/activity-options-update-mask';
-import { decodePayloadAndParseDataToJSON } from '$lib/utilities/decode-payload';
+import {
+ decodePayloadAndParseDataToJSON,
+ parseSearchAttributes,
+} from '$lib/utilities/decode-payload';
import {
encodePayloads,
setSearchAttributes,
@@ -101,10 +105,17 @@ export interface StartStandaloneActivityResponse {
started: boolean;
}
+const toActivityExecutionListInfo = (
+ activity: ActivityExecutionInfo,
+): ActivityExecutionListInfo => ({
+ ...activity,
+ searchAttributes: parseSearchAttributes(activity.searchAttributes),
+});
+
export type PaginatedActivitiesPromise = (
pageSize?: number,
token?: string,
-) => Promise<{ items: ActivityExecutionInfo[]; nextPageToken: string }>;
+) => Promise<{ items: ActivityExecutionListInfo[]; nextPageToken: string }>;
export const fetchPaginatedActivities = async (
namespace: string,
@@ -134,7 +145,7 @@ export const fetchPaginatedActivities = async (
}).then((response) => {
const { executions = [], nextPageToken = '' } = response || {};
return {
- items: executions,
+ items: executions.map(toActivityExecutionListInfo),
nextPageToken: nextPageToken ? String(nextPageToken) : '',
};
});
diff --git a/src/lib/services/standalone-nexus-operations.ts b/src/lib/services/standalone-nexus-operations.ts
index b0d7fa9efe..e924ad08cc 100644
--- a/src/lib/services/standalone-nexus-operations.ts
+++ b/src/lib/services/standalone-nexus-operations.ts
@@ -15,9 +15,13 @@ import type {
NexusOperationExecution,
NexusOperationExecutionInfo,
NexusOperationExecutionListInfo,
+ NexusOperationExecutionListInfoAPIResponse,
StartNexusOperationRequest,
} from '$lib/types/nexus-operation-execution';
-import { decodePayloadAndParseDataToJSON } from '$lib/utilities/decode-payload';
+import {
+ decodePayloadAndParseDataToJSON,
+ parseSearchAttributes,
+} from '$lib/utilities/decode-payload';
import {
encodePayloads,
setSearchAttributes,
@@ -30,7 +34,7 @@ import {
import { routeForApi } from '$lib/utilities/route-for-api';
export type ListNexusOperationsResponse = {
- operations: NexusOperationExecutionListInfo[];
+ operations: NexusOperationExecutionListInfoAPIResponse[];
nextPageToken: string;
};
@@ -53,6 +57,13 @@ const emptyNexusOperationExecution: NexusOperationExecution = {
info: emptyNexusOperationExecutionInfo,
};
+const toNexusOperationExecutionListInfo = (
+ operation: NexusOperationExecutionListInfoAPIResponse,
+): NexusOperationExecutionListInfo => ({
+ ...operation,
+ searchAttributes: parseSearchAttributes(operation.searchAttributes ?? {}),
+});
+
export type PaginatedNexusOperationsPromise = (
pageSize?: number,
token?: string,
@@ -90,7 +101,7 @@ export const fetchPaginatedNexusOperations = async (
}).then((response) => {
const { operations = [], nextPageToken = '' } = response || {};
return {
- items: operations,
+ items: operations.map(toNexusOperationExecutionListInfo),
nextPageToken: nextPageToken ? String(nextPageToken) : '',
};
});
diff --git a/src/lib/stores/search-attributes.test.ts b/src/lib/stores/search-attributes.test.ts
index 998318d52f..819676c762 100644
--- a/src/lib/stores/search-attributes.test.ts
+++ b/src/lib/stores/search-attributes.test.ts
@@ -1,30 +1,11 @@
import { describe, expect, test } from 'vitest';
import {
- allSearchAttributes,
- isCustomSearchAttribute,
+ isFilterableCustomSearchAttribute,
workflowIncludesSearchAttribute,
} from './search-attributes';
describe('search attributes store', () => {
- describe('isCustomSearchAttribute', () => {
- test('returns false when the key is not a custom search attribute', () => {
- allSearchAttributes.set({
- systemAttributes: { WorkflowType: 'Keyword' },
- customAttributes: {},
- });
- expect(isCustomSearchAttribute('CustomBool')).toBe(false);
- });
-
- test('returns true when the key is a custom search attribute', () => {
- allSearchAttributes.set({
- systemAttributes: { WorkflowType: 'Keyword' },
- customAttributes: { CustomBool: 'Bool' },
- });
- expect(isCustomSearchAttribute('CustomBool')).toBe(true);
- });
- });
-
describe('workflowIncludesSearchAttribute', () => {
test('returns true when the search attribute is defined on the workflow', () => {
const mockWorkflow = {
@@ -79,4 +60,50 @@ describe('search attributes store', () => {
);
});
});
+
+ describe('isFilterableCustomSearchAttribute', () => {
+ const customAttributes = {
+ CustomKeyword: 'Keyword',
+ CustomText: 'Text',
+ CustomInt: 'Int',
+ } as const;
+
+ test('returns true for a string Keyword value', () => {
+ expect(
+ isFilterableCustomSearchAttribute(
+ customAttributes,
+ 'CustomKeyword',
+ 'value',
+ ),
+ ).toBe(true);
+ });
+
+ test('returns true for a string Text value', () => {
+ expect(
+ isFilterableCustomSearchAttribute(customAttributes, 'CustomText', 'a'),
+ ).toBe(true);
+ });
+
+ test('returns false for a non-string value', () => {
+ expect(
+ isFilterableCustomSearchAttribute(
+ customAttributes,
+ 'CustomKeyword',
+ undefined,
+ ),
+ ).toBe(false);
+ });
+
+ test('returns false for other types', () => {
+ expect(
+ isFilterableCustomSearchAttribute(customAttributes, 'CustomInt', '1'),
+ ).toBe(false);
+ });
+
+ test('returns false for unknown attributes', () => {
+ expect(
+ isFilterableCustomSearchAttribute(customAttributes, 'Unknown', 'a'),
+ ).toBe(false);
+ });
+ });
});
diff --git a/src/lib/stores/search-attributes.ts b/src/lib/stores/search-attributes.ts
index 59531f5834..d8eded04af 100644
--- a/src/lib/stores/search-attributes.ts
+++ b/src/lib/stores/search-attributes.ts
@@ -1,4 +1,4 @@
-import { derived, get, readable, type Readable, writable } from 'svelte/store';
+import { derived, readable, type Readable, writable } from 'svelte/store';
import { z } from 'zod/v3';
@@ -85,10 +85,14 @@ export const customSearchAttributeOptions: Readable<
}));
});
-export const isCustomSearchAttribute = (key: string) => {
- const customSearchAttrs = get(customSearchAttributes);
- return key in customSearchAttrs;
-};
+export const isFilterableCustomSearchAttribute = (
+ customAttributes: SearchAttributes,
+ attribute: string,
+ value: unknown,
+): value is string =>
+ typeof value === 'string' &&
+ (customAttributes[attribute] === SEARCH_ATTRIBUTE_TYPE.KEYWORD ||
+ customAttributes[attribute] === SEARCH_ATTRIBUTE_TYPE.TEXT);
export const workflowIncludesSearchAttribute = (
workflow: WorkflowExecution,
diff --git a/src/lib/types/activity-execution.ts b/src/lib/types/activity-execution.ts
index 8a3a008bc1..7652716475 100644
--- a/src/lib/types/activity-execution.ts
+++ b/src/lib/types/activity-execution.ts
@@ -3,7 +3,10 @@ import type { temporal } from '@temporalio/proto';
import type { Callback } from '$lib/types/nexus';
import type { Failure, Payloads } from '.';
-import type { WorkflowSearchAttributes } from './workflows';
+import type {
+ DecodedWorkflowSearchAttributes,
+ WorkflowSearchAttributes,
+} from './workflows';
// Enum values arrive over REST/JSON as their SCREAMING_SNAKE names, so these are
// string unions of the proto enum keys (not the numeric proto enum). Keying off
@@ -99,6 +102,13 @@ export interface ActivityExecutionInfo extends Omit<
| null;
}
+export type ActivityExecutionListInfo = Omit<
+ ActivityExecutionInfo,
+ 'searchAttributes'
+> & {
+ searchAttributes: DecodedWorkflowSearchAttributes;
+};
+
export interface ActivityExecution {
runId: string;
info: ActivityExecutionInfo;
diff --git a/src/lib/types/nexus-operation-execution.ts b/src/lib/types/nexus-operation-execution.ts
index 2b174fb0e8..72a561c57a 100644
--- a/src/lib/types/nexus-operation-execution.ts
+++ b/src/lib/types/nexus-operation-execution.ts
@@ -6,7 +6,10 @@ import type {
NexusOperationIdReusePolicy,
StartNexusOperationExecutionRequest,
} from '.';
-import type { WorkflowSearchAttributes } from './workflows';
+import type {
+ DecodedWorkflowSearchAttributes,
+ WorkflowSearchAttributes,
+} from './workflows';
export type NexusOperationExecutionStatus =
keyof typeof temporal.api.enums.v1.NexusOperationExecutionStatus;
@@ -82,9 +85,16 @@ export interface NexusOperationExecutionListInfo extends Omit<
closeTime: string;
stateTransitionCount: string;
executionDuration: string;
- searchAttributes: WorkflowSearchAttributes;
+ searchAttributes: DecodedWorkflowSearchAttributes;
}
+export type NexusOperationExecutionListInfoAPIResponse = Omit<
+ NexusOperationExecutionListInfo,
+ 'searchAttributes'
+> & {
+ searchAttributes?: WorkflowSearchAttributes;
+};
+
export interface NexusOperationExecution extends Omit<
DescribeNexusOperationResponse,
'info' | 'longPollToken'
diff --git a/src/lib/utilities/decode-payload.test.ts b/src/lib/utilities/decode-payload.test.ts
index ddbd60e52d..67f9f0c9a4 100644
--- a/src/lib/utilities/decode-payload.test.ts
+++ b/src/lib/utilities/decode-payload.test.ts
@@ -7,6 +7,7 @@ import {
decodeEventAttributes,
parsePayloadAttributes,
parseRawPayloadToJSON,
+ parseSearchAttributes,
} from './decode-payload';
import {
dataConvertedWorkflowStartedEvent,
@@ -245,6 +246,38 @@ describe('parseRawPayloadToJSON with returnDataOnly = false', () => {
});
});
+describe('parseSearchAttributes', () => {
+ it('parses indexed field payloads', () => {
+ expect(
+ parseSearchAttributes({
+ indexedFields: {
+ CustomKeyword: {
+ metadata: { encoding: btoa('json/plain') },
+ data: btoa('"keyword-value"'),
+ },
+ CustomKeywordList: {
+ metadata: { encoding: btoa('json/plain') },
+ data: btoa('["a","b"]'),
+ },
+ },
+ }),
+ ).toEqual({
+ indexedFields: {
+ CustomKeyword: 'keyword-value',
+ CustomKeywordList: ['a', 'b'],
+ },
+ });
+ });
+
+ it('returns an empty object when there are no indexed fields', () => {
+ expect(parseSearchAttributes({})).toEqual({});
+ });
+
+ it('returns an empty object when indexed fields are null', () => {
+ expect(parseSearchAttributes({ indexedFields: null })).toEqual({});
+ });
+});
+
describe('parsePayloadAttributes', () => {
it('Should parsePayloadAttributes searchAttributes with indexedFields', () => {
const payload = {
diff --git a/src/lib/utilities/decode-payload.ts b/src/lib/utilities/decode-payload.ts
index c7c00cc2a6..4799664a34 100644
--- a/src/lib/utilities/decode-payload.ts
+++ b/src/lib/utilities/decode-payload.ts
@@ -1,9 +1,15 @@
+import { mapValues } from 'es-toolkit';
+
import { decodePayloadsWithCodec as callCodecEndpoint } from '$lib/services/data-encoder';
import type { DownloadEventHistorySetting } from '$lib/stores/events';
import { schemaForMessageType } from '$lib/system-nexus-endpoints';
-import type { Memo, Payload, Payloads } from '$lib/types';
+import type { Memo, Payload, Payloads, SearchAttribute } from '$lib/types';
import type { EventAttribute, WorkflowEvent } from '$lib/types/events';
import type { Optional, Replace } from '$lib/types/global';
+import type {
+ DecodedWorkflowSearchAttributes,
+ WorkflowSearchAttributes,
+} from '$lib/types/workflows';
import { atob } from './atob';
import {
@@ -162,6 +168,19 @@ export function parseRawPayloadToJSON(
return payload;
}
+export const parseSearchAttributes = (
+ apiSearchAttributes: WorkflowSearchAttributes | SearchAttribute,
+): DecodedWorkflowSearchAttributes => {
+ if (!apiSearchAttributes || !apiSearchAttributes.indexedFields) return {};
+ const decoded = mapValues(apiSearchAttributes.indexedFields, (payload) =>
+ parseRawPayloadToJSON(payload),
+ ) as Record;
+
+ return {
+ indexedFields: decoded,
+ };
+};
+
/**
* Phase 1 — synchronous, no network.
*
diff --git a/src/lib/utilities/delayed-activities.ts b/src/lib/utilities/delayed-activities.ts
index 8e2bba98f3..dd62c538c0 100644
--- a/src/lib/utilities/delayed-activities.ts
+++ b/src/lib/utilities/delayed-activities.ts
@@ -1,6 +1,8 @@
import type { ActivityExecutionInfo } from '$lib/types/activity-execution';
import { isFuture } from '$lib/utilities/format-date';
-export const isActivityDelayed = (activity: ActivityExecutionInfo): boolean => {
+export const isActivityDelayed = (
+ activity: Pick,
+): boolean => {
return !!activity.executionTime && isFuture(activity.executionTime);
};