Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ server/ui/assets
go.work
go.work.sum
.claude/*
!.claude/skills/
!.claude/skills/
.sisyphus/*
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.59.1-0.20251219210004-6b4f0602460a
go.temporal.io/api v1.60.0
golang.org/x/net v0.40.0
golang.org/x/oauth2 v0.30.0
google.golang.org/grpc v1.66.1
Expand Down
4 changes: 2 additions & 2 deletions server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
go.temporal.io/api v1.59.1-0.20251219210004-6b4f0602460a h1:ujf/X4+peZuQywOKhl67QlSRzYCmEM0KFDGVYX58hCg=
go.temporal.io/api v1.59.1-0.20251219210004-6b4f0602460a/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM=
go.temporal.io/api v1.60.0 h1:SlRkizt3PXu/J62NWlUNLldHtJhUxfsBRuF4T0KYkgY=
go.temporal.io/api v1.60.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script lang="ts">
import { page } from '$app/state';

import TableEmptyState from '$lib/components/activity/activities-summary-configurable-table/table-empty-state.svelte';
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 { fetchPaginatedActivities } from '$lib/services/standalone-activities';
import { activityCount, activityRefresh } from '$lib/stores/activities';
import { configurableTableColumns } from '$lib/stores/configurable-table-columns';

import TableBodyCell from './activities-summary-configurable-table/table-body-cell.svelte';
import TableHeaderCell from './activities-summary-configurable-table/table-header-cell.svelte';
import TableHeaderRow from './activities-summary-configurable-table/table-header-row.svelte';
import TableRow from './activities-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]?.activities ?? [],
);
const query = $derived(page.url.searchParams.get('query'));

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

{#key [namespace, query, $activityRefresh]}
<PaginatedTable
total={$activityCount.count}
{onFetch}
let:visibleItems
aria-label={translate('activities.standalone-activities')}
pageSizeSelectLabel={translate('common.per-page')}
nextButtonLabel={translate('common.next')}
previousButtonLabel={translate('common.previous')}
emptyStateMessage={translate('activities.empty-state-title')}
maxHeight="var(--panel-h)"
>
<caption class="sr-only" slot="caption">
{translate('activities.standalone-activities')}
</caption>
<TableHeaderRow slot="headers">
<th></th>
{#each columns as column}
<TableHeaderCell {column} />
{/each}
</TableHeaderRow>
{#each visibleItems as activity}
<TableRow {activity}>
{#each columns as column}
<TableBodyCell {activity} {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="activities-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,68 @@
<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 { activityFilters } from '$lib/stores/filters';
import {
SEARCH_ATTRIBUTE_TYPE,
type SearchAttributeType,
} from '$lib/types/workflows';
import { 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 = $activityFilters.find((f) => f.attribute === attribute);
const getOtherFilters = () =>
$activityFilters.filter((f) => f.attribute !== attribute);

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

updateQueryParamsFromFilter(page.url, $activityFilters);
};
</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-activities')}
show={filterOrCopyButtonsVisible}
content={value}
onFilter={onRowFilterClick}
filtered={$activityFilters.some(
(filter) => filter.attribute === attribute && filter.value === value,
)}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<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 { ActivityExecutionInfo } from '$lib/types/activity-execution';
import { formatDistance } from '$lib/utilities/format-time';
import { toActivityStatus } from '$lib/utilities/get-activity-status-and-count';
import { routeForStandaloneActivityDetails } from '$lib/utilities/route-for';

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

type Props = {
column: ConfigurableTableHeader;
activity: ActivityExecutionInfo;
};
let { column, activity }: 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 = ['Activity ID', 'Activity Type', 'Task Queue'];
</script>

{#if filterableLabels.includes(label)}
<td
class="activities-summary-table-body-cell filterable"
data-testid="activities-summary-table-body-cell"
onmouseover={showFilterOrCopy}
onfocus={showFilterOrCopy}
onfocusin={showFilterOrCopy}
onfocusout={handleFocusOut}
onmouseleave={hideFilterOrCopy}
onblur={hideFilterOrCopy}
>
{#if label === 'Activity ID'}
<FilterableTableCell
{filterOrCopyButtonsVisible}
attribute="ActivityId"
value={activity.activityId}
href={routeForStandaloneActivityDetails({
namespace,
activityId: activity.activityId,
})}
/>
{:else if label === 'Activity Type'}
<FilterableTableCell
{filterOrCopyButtonsVisible}
attribute="ActivityType"
value={activity.activityType?.name ?? ''}
/>
{:else if label === 'Task Queue'}
<FilterableTableCell
{filterOrCopyButtonsVisible}
attribute="TaskQueue"
value={activity.taskQueue ?? ''}
/>
{/if}
</td>
{:else}
<td
class="activities-summary-table-body-cell"
data-testid="activities-summary-table-body-cell"
>
{#if label === 'Status'}
<WorkflowStatus status={toActivityStatus(activity.status)} />
{:else if label === 'Run ID'}
{activity.runId ?? ''}
{:else if label === 'Start Time'}
<Timestamp dateTime={activity.lastStartedTime || activity.scheduleTime} />
{:else if label === 'Execution Time'}
<Timestamp dateTime={activity.lastStartedTime} />
{:else if label === 'Close Time'}
<Timestamp dateTime={activity.closeTime} />
{:else if label === 'Execution Duration'}
{#if activity.executionDuration}
{formatDistance({
start: activity.lastStartedTime || activity.scheduleTime,
end: activity.closeTime,
includeMillisecondsForUnderSecond: true,
})}
{/if}
{:else if label === 'State Transitions'}
{activity.stateTransitionCount ?? ''}
{/if}
</td>
{/if}

<style lang="postcss">
.activities-summary-table-body-cell {
@apply h-8 whitespace-nowrap;

&.filterable {
@apply relative pr-24;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script lang="ts">
import { page } from '$app/state';

import NoWorkflows from '$lib/components/workflow/workflows-summary-configurable-table/empty-states/no-workflows.svelte';
import Alert from '$lib/holocene/alert.svelte';
import { translate } from '$lib/i18n/translate';
import { activityError } from '$lib/stores/activities';
import noResultsImages from '$lib/vendor/empty-state.svg';

let query = $derived(page.url.searchParams.get('query'));
</script>

{#if query}
<div
class="flex h-full w-full flex-col items-center justify-center p-4"
aria-live="polite"
>
<div class="text-center">
<h2>
{#if $activityError}
{translate('activities.activity-query-error-state')}
{:else}
{translate('activities.empty-state-title')}
{/if}
</h2>
<p class="text-secondary">
{#if $activityError}
{$activityError}
{:else}
{translate('activities.empty-state-description')}
{/if}
</p>
<NoWorkflows class="m-auto mt-8 text-subtle" />
</div>
</div>
{:else}
<div
class="h-full w-full overflow-hidden xl:flex xl:flex-row"
aria-live="polite"
>
<div
class="surface-primary flex w-auto min-w-[280px] flex-col gap-4 p-8 xl:min-w-[520px] xl:flex-1"
>
<h2>
{translate('activities.empty-state-title')}
</h2>
{#if $activityError}
<Alert
intent="warning"
icon="warning"
title={translate('common.error-occurred')}
style="overflow-wrap: anywhere"
>
{$activityError}
</Alert>
{:else}
<p>
{translate('activities.empty-state-description')}
</p>
{/if}
</div>
<div class="flex h-full flex-col">
<div class="bg-off-white dark:bg-[#0f1725]">
<img src={noResultsImages} alt="" class="w-full" />
</div>
<div class="flex-1 bg-[#818cf8]"></div>
</div>
</div>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts">
import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns';

interface Props {
column: ConfigurableTableHeader;
}

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

const label = $derived(column.label);
</script>

<th data-testid="activities-summary-table-header-cell-{label}">
{label}
</th>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
children?: Snippet;
}

let { children }: Props = $props();
</script>

<tr>
{@render children?.()}
</tr>
Loading