Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/labstack/echo/v4 v4.13.4
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v2 v2.3.0
go.temporal.io/api v1.62.8
go.temporal.io/api v1.62.12
golang.org/x/net v0.54.0
golang.org/x/oauth2 v0.34.0
google.golang.org/grpc v1.79.3
Expand Down
4 changes: 2 additions & 2 deletions server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.temporal.io/api v1.62.8 h1:g8RAZmdebYODoNa2GLA4M4TsXNe1096WV3n26C4+fdw=
go.temporal.io/api v1.62.8/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM=
go.temporal.io/api v1.62.12 h1:627rVnItegQmrszg1bH4vfyc/1uNo5qCereCNkvZefw=
go.temporal.io/api v1.62.12/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script lang="ts">
import { page } from '$app/state';

import Button from '$lib/holocene/button.svelte';
import Icon from '$lib/holocene/icon/icon.svelte';
import PaginatedTable from '$lib/holocene/table/paginated-table/api-paginated.svelte';
import Tooltip from '$lib/holocene/tooltip.svelte';
import { translate } from '$lib/i18n/translate';
import { fetchPaginatedNexusOperations } from '$lib/services/standalone-nexus-operations';
import { configurableTableColumns } from '$lib/stores/configurable-table-columns';
import {
nexusOperationCount,
nexusOperationRefresh,
} from '$lib/stores/nexus-operations';

import TableBodyCell from './nexus-operations-summary-configurable-table/table-body-cell.svelte';
import TableEmptyState from './nexus-operations-summary-configurable-table/table-empty-state.svelte';
import TableHeaderCell from './nexus-operations-summary-configurable-table/table-header-cell.svelte';
import TableHeaderRow from './nexus-operations-summary-configurable-table/table-header-row.svelte';
import TableRow from './nexus-operations-summary-configurable-table/table-row.svelte';

interface Props {
onClickConfigure: () => void;
}

let { onClickConfigure }: Props = $props();

const namespace = $derived(page.params.namespace);
const columns = $derived(
$configurableTableColumns?.[namespace]?.['nexus-operations'] ?? [],
);
const query = $derived(page.url.searchParams.get('query') ?? '');

const onFetch = $derived(() =>
fetchPaginatedNexusOperations(namespace, query),
);
</script>

{#key [namespace, query, $nexusOperationRefresh]}
<PaginatedTable
total={$nexusOperationCount.count}
{onFetch}
let:visibleItems
aria-label={translate(
'standalone-nexus-operations.standalone-nexus-operations',
)}
pageSizeSelectLabel={translate('common.per-page')}
nextButtonLabel={translate('common.next')}
previousButtonLabel={translate('common.previous')}
emptyStateMessage={translate(
'standalone-nexus-operations.empty-state-title',
)}
maxHeight="var(--panel-h)"
>
<caption class="sr-only" slot="caption">
{translate('standalone-nexus-operations.nexus-operations-table')}
</caption>
<TableHeaderRow slot="headers">
<th></th>
{#each columns as column (column.label)}
<TableHeaderCell {column} />
{/each}
</TableHeaderRow>
{#each visibleItems as operation (operation.operationId + '-' + operation.runId)}
<TableRow {operation}>
{#each columns as column (column.label)}
<TableBodyCell {operation} {column} />
{/each}
</TableRow>
{/each}
<svelte:fragment slot="empty">
<TableEmptyState />
</svelte:fragment>
<svelte:fragment slot="actions-end-additional">
<Tooltip text="Configure Columns" top>
<Button
on:click={onClickConfigure}
data-testid="nexus-operations-summary-table-configuration-button"
size="xs"
variant="ghost"
>
<Icon name="settings" />
</Button>
</Tooltip>
</svelte:fragment>
</PaginatedTable>
{/key}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script lang="ts">
import { page } from '$app/state';

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 { nexusOperationFilters } from '$lib/stores/filters';
import {
SEARCH_ATTRIBUTE_TYPE,
type SearchAttributeType,
} from '$lib/types/workflows';
import {
createFilter,
updateQueryParamsFromFilter,
} from '$lib/utilities/query/to-list-workflow-filters';

type Props = {
attribute: string;
filterOrCopyButtonsVisible: boolean;
value: string;
href?: string;
type?: SearchAttributeType;
};
let {
attribute,
filterOrCopyButtonsVisible = false,
value,
href,
type = SEARCH_ATTRIBUTE_TYPE.KEYWORD,
}: Props = $props();

const onRowFilterClick = () => {
const filter = $nexusOperationFilters.find(
(f) => f.attribute === attribute,
);
const getOtherFilters = () =>
$nexusOperationFilters.filter((f) => f.attribute !== attribute);

if (!filter || filter.value !== value) {
const newFilter: SearchAttributeFilter = createFilter({
attribute,
type,
value,
conditional: '=',
});
$nexusOperationFilters = [...getOtherFilters(), newFilter];
} else {
$nexusOperationFilters = [...getOtherFilters()];
}

updateQueryParamsFromFilter(page.url, $nexusOperationFilters);
};
</script>

{#if href}
<Link {href}>{value}</Link>
{:else}
{value}
{/if}
<FilterOrCopyButtons
copyIconTitle={translate('common.copy-icon-title')}
copySuccessIconTitle={translate('common.copy-success-icon-title')}
filterIconTitle={translate('common.filter-nexus-operations')}
show={filterOrCopyButtonsVisible}
content={value}
onFilter={onRowFilterClick}
filtered={$nexusOperationFilters.some(
(filter) => filter.attribute === attribute && filter.value === value,
)}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script lang="ts">
import { page } from '$app/state';

import Timestamp from '$lib/components/timestamp.svelte';
import WorkflowStatus from '$lib/components/workflow-status.svelte';
import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns';
import type { NexusOperationExecutionListInfo } from '$lib/types/nexus-operation-execution';
import { formatDistance } from '$lib/utilities/format-time';
import { toNexusOperationStatus } from '$lib/utilities/get-nexus-operation-status-and-count';
import { routeForStandaloneNexusOperationDetails } from '$lib/utilities/route-for';

import FilterableTableCell from './filterable-table-cell.svelte';

type Props = {
column: ConfigurableTableHeader;
operation: NexusOperationExecutionListInfo;
};
let { column, operation }: Props = $props();

const { label } = $derived(column);
const namespace = $derived(page.params.namespace);

let filterOrCopyButtonsVisible = $state(false);
const showFilterOrCopy = () => (filterOrCopyButtonsVisible = true);
const hideFilterOrCopy = () => (filterOrCopyButtonsVisible = false);
const handleFocusOut = (e: FocusEvent) => {
const nextTarget = e.relatedTarget as HTMLElement;
if (
nextTarget &&
!['filter-button', 'copy-button'].includes(nextTarget.id)
) {
hideFilterOrCopy();
}
};

const filterableLabels = ['Operation ID', 'Endpoint', 'Service', 'Operation'];
</script>

{#if filterableLabels.includes(label)}
<td
class="relative h-8 whitespace-nowrap pr-24"
data-testid="nexus-operations-summary-table-body-cell"
onmouseover={showFilterOrCopy}
onfocus={showFilterOrCopy}
onfocusin={showFilterOrCopy}
onfocusout={handleFocusOut}
onmouseleave={hideFilterOrCopy}
onblur={hideFilterOrCopy}
>
{#if label === 'Operation ID'}
<FilterableTableCell
{filterOrCopyButtonsVisible}
attribute="OperationId"
value={operation.operationId}
href={routeForStandaloneNexusOperationDetails({
namespace,
operationId: operation.operationId,
})}
/>
{:else if label === 'Endpoint'}
<FilterableTableCell
{filterOrCopyButtonsVisible}
attribute="Endpoint"
value={operation.endpoint ?? ''}
/>
{:else if label === 'Service'}
<FilterableTableCell
{filterOrCopyButtonsVisible}
attribute="Service"
value={operation.service ?? ''}
/>
{:else if label === 'Operation'}
<FilterableTableCell
{filterOrCopyButtonsVisible}
attribute="Operation"
value={operation.operation ?? ''}
/>
{/if}
</td>
{:else}
<td
class="h-8 whitespace-nowrap"
data-testid="nexus-operations-summary-table-body-cell"
>
{#if label === 'Status'}
<WorkflowStatus status={toNexusOperationStatus(operation.status)} />
{:else if label === 'Run ID'}
{operation.runId ?? ''}
{:else if label === 'Schedule Time'}
<Timestamp dateTime={operation.scheduleTime} />
{:else if label === 'Close Time'}
<Timestamp dateTime={operation.closeTime} />
{:else if label === 'Execution Duration'}
{#if operation.executionDuration}
{formatDistance({
start: operation.scheduleTime,
end: operation.closeTime,
includeMillisecondsForUnderSecond: true,
})}
{/if}
{:else if label === 'State Transitions'}
{operation.stateTransitionCount ?? ''}
{/if}
</td>
{/if}
Loading