diff --git a/src/lib/components/event/event-link.svelte b/src/lib/components/event/event-link.svelte index b79ccceed..b24a4d786 100644 --- a/src/lib/components/event/event-link.svelte +++ b/src/lib/components/event/event-link.svelte @@ -1,26 +1,32 @@
-

Link

-
+

+ {label} +

+
- - {link.workflowEvent.workflowId} + + {value}
diff --git a/src/lib/components/event/event-links-expanded.svelte b/src/lib/components/event/event-links-expanded.svelte index 0233f34b7..06dc69850 100644 --- a/src/lib/components/event/event-links-expanded.svelte +++ b/src/lib/components/event/event-links-expanded.svelte @@ -1,33 +1,29 @@ {#each links as link} {#if link?.workflowEvent} -
-

Link

- - - {link.workflowEvent.workflowId} - - +
+ +
+
+
{/if} {/each} - - diff --git a/src/lib/components/event/event-summary-row.svelte b/src/lib/components/event/event-summary-row.svelte index cc082fd51..7889e4a86 100644 --- a/src/lib/components/event/event-summary-row.svelte +++ b/src/lib/components/event/event-summary-row.svelte @@ -46,6 +46,8 @@ export let active = false; export let onRowClick: () => void = noop; + let row; + $: selectedId = isEventGroup(event) ? Array.from(event.events.keys()).pop() : event.id; @@ -57,6 +59,7 @@ workflow, run, }); + $: expanded = expandAll; $: attributes = formatAttributes(event); @@ -78,6 +81,9 @@ : ''; const onLinkClick = () => { + if (isHashRow($page.url.hash) && expanded) { + history.replaceState(null, '', location.pathname + location.search); + } expanded = !expanded; onRowClick(); }; @@ -112,9 +118,29 @@ isEventGroup(event) && !event.isPending && event.eventList.find(isActivityTaskStartedEvent)?.attributes?.attempt; + + const scrollToId = (hash: string) => { + if (isHashRow(hash)) { + expanded = true; + setTimeout(() => { + row?.scrollIntoView({ behavior: 'smooth' }); + }, 500); + } + }; + + const isHashRow = (hash: string) => { + if (hash) { + const id = hash.slice(1); + return compact ? group?.eventIds.has(id) : event?.id === id; + } + return false; + }; + + $: scrollToId($page.url.hash); import Alert from '$lib/holocene/alert.svelte'; + import Badge from '$lib/holocene/badge.svelte'; import CodeBlock from '$lib/holocene/code-block.svelte'; import { translate } from '$lib/i18n/translate'; + import { fullEventHistory } from '$lib/stores/events'; import { timeFormat } from '$lib/stores/time-format'; import type { CallbackInfo } from '$lib/types'; import { formatDate } from '$lib/utilities/format-date'; + import { routeForNamespace } from '$lib/utilities/route-for'; + + import EventLink from '../event/event-link.svelte'; export let callback: CallbackInfo; @@ -12,6 +17,9 @@ $: nextTime = formatDate(callback.nextAttemptScheduleTime, $timeFormat); $: failure = callback?.lastAttemptFailure?.message; + $: initialEvent = $fullEventHistory[0]; + $: link = initialEvent?.links[0]; + const titles = { Standby: translate('nexus.callback.standby'), Scheduled: translate('nexus.callback.scheduled'), @@ -25,32 +33,42 @@
-
-

- {translate('common.url')} - {callback.callback.nexus.url} -

-
+ {#if link} + + + {/if}
-

- {translate('common.state')} {callback.state} +

+ {translate('common.state')} + {callback.state}

{#if callback.attempt} -

+

{translate('common.attempt')} - {callback.attempt} + + {callback.attempt} +

{/if} {#if callback.lastAttemptCompleteTime} -

+

{translate('nexus.last-attempt-completed-time')} - {completedTime} + + {completedTime} +

{/if} {#if callback.nextAttemptScheduleTime} -

+

{translate('nexus.next-attempt-scheduled-time')} - {nextTime} + + {nextTime} +

{/if}
diff --git a/src/lib/holocene/table/paginated-table/paginated.svelte b/src/lib/holocene/table/paginated-table/paginated.svelte index 7ca01fe8c..4bc3b2876 100644 --- a/src/lib/holocene/table/paginated-table/paginated.svelte +++ b/src/lib/holocene/table/paginated-table/paginated.svelte @@ -32,7 +32,8 @@ $: url = $page.url; $: perPageParam = url.searchParams.get(perPageKey) ?? pageSizeOptions[0]; $: currentPageParam = url.searchParams.get(currentPageKey) ?? '1'; - $: store = pagination(items, perPageParam, currentPageParam); + $: hash = $page.url.hash; + $: store = pagination(items, perPageParam, currentPageParam, hash); // keep the 'page-size' url search param within the supported options $: { @@ -83,7 +84,7 @@ }; $: { - if (currentPageParam) store.jumpToPage(currentPageParam); + if (currentPageParam && !hash) store.jumpToPage(currentPageParam); if (perPageParam) store.adjustPageSize(perPageParam); } diff --git a/src/lib/i18n/locales/en/common.ts b/src/lib/i18n/locales/en/common.ts index cc0e14549..6d0169b7c 100644 --- a/src/lib/i18n/locales/en/common.ts +++ b/src/lib/i18n/locales/en/common.ts @@ -181,4 +181,6 @@ export const Strings = { message: 'Message', 'upload-json': 'Upload JSON', 'input-valid-json': 'Input must be valid JSON', + link: 'Link', + 'link-namespace': 'Link Namespace', } as const; diff --git a/src/lib/stores/pagination.test.ts b/src/lib/stores/pagination.test.ts index 4b5a749cc..164dae6ad 100644 --- a/src/lib/stores/pagination.test.ts +++ b/src/lib/stores/pagination.test.ts @@ -9,11 +9,15 @@ import { getStartingIndexForPage, getTotalPages, getValidPage, + hasId, pagination, perPageFromSearchParameter, } from './pagination'; const oneHundredResolutions = new Array(100).fill(null).map((_, i) => i); +const oneHundredItems = new Array(100) + .fill(null) + .map((_, i) => ({ id: i.toString() })); describe('pagination', () => { it('should have a pageSize', () => { @@ -542,3 +546,53 @@ describe('perPageFromSearchParameter', () => { expect(perPageFromSearchParameter({} as any)).toBe(100); }); }); + +describe('getStartingIndexForPage', () => { + it('should return 0 for the first page', () => { + expect(getStartingIndexForPage(1, 20, oneHundredResolutions)).toBe(0); + }); + + it('should return the first index of the second page for the something on the second page', () => { + expect(getStartingIndexForPage(2, 20, oneHundredResolutions)).toBe(20); + }); + + it('should return the first index of the last page for the something out of bounds', () => { + expect(getStartingIndexForPage(100, 20, oneHundredResolutions)).toBe(80); + }); + + it('should return 0 for the something out of bounds if the total number of items is less than itemsPerPage', () => { + expect(getStartingIndexForPage(3, 101, oneHundredResolutions)).toBe(0); + }); + + it('should return 0 if given a negative number for the page', () => { + expect(getStartingIndexForPage(-10, 20, oneHundredResolutions)).toBe(0); + }); + + it('should return 0 if given NaN', () => { + expect(getStartingIndexForPage(NaN, 20, oneHundredResolutions)).toBe(0); + }); +}); + +describe('hash included in pagination store', () => { + it('should return true if object has id', () => { + expect(hasId({ id: '1234', name: 'cats' })).toBe(true); + }); + + it('should return false if object does not have id', () => { + expect(hasId({ name: 'cats', startedId: 'asdf' })).toBe(false); + }); + + it('should not adjust page when hash is included and on first page', () => { + const store = pagination(oneHundredItems, 50, 0, '#23'); + const { currentPage } = get(store); + + expect(currentPage).toBe(1); + }); + + it('should adjust page when hash is included and is on next page', () => { + const store = pagination(oneHundredItems, 50, 0, '#87'); + const { currentPage } = get(store); + + expect(currentPage).toBe(2); + }); +}); diff --git a/src/lib/stores/pagination.ts b/src/lib/stores/pagination.ts index e3586a89f..c9c6442a1 100644 --- a/src/lib/stores/pagination.ts +++ b/src/lib/stores/pagination.ts @@ -151,6 +151,12 @@ export const outOfBounds = ( return false; }; +export const hasId = (item: unknown): item is { id: string } => { + return ( + typeof item === 'object' && Object.prototype.hasOwnProperty.call(item, 'id') + ); +}; + /** * Creates a Svelte store for viewing pages of a larger data set. */ @@ -158,9 +164,20 @@ export const pagination = ( items: Readonly = [], perPage: number | string = defaultItemsPerPage, startingIndex: string | number = 0, + hash: string = '', ): PaginationStore => { perPage = perPageFromSearchParameter(perPage); + const hashId = hash?.slice(1); + if (hashId) { + const itemIndex = items.findIndex( + (item: unknown) => hasId(item) && item?.id === hashId, + ); + if (itemIndex !== -1) { + startingIndex = itemIndex; + } + } + const start = getNearestStartingIndex( toNumber(startingIndex), perPage, diff --git a/src/lib/utilities/route-for.ts b/src/lib/utilities/route-for.ts index 254fdb9d4..59f1dfa12 100644 --- a/src/lib/utilities/route-for.ts +++ b/src/lib/utilities/route-for.ts @@ -13,7 +13,7 @@ type RouteParameters = { run: string; view?: EventView | string; queryParams?: Record; - eventId: string; + eventId?: string; scheduleId: string; queue: string; schedule: string; @@ -38,7 +38,7 @@ export type ScheduleParameters = Pick< >; export type EventHistoryParameters = Pick< RouteParameters, - 'namespace' | 'workflow' | 'run' | 'view' | 'queryParams' + 'namespace' | 'workflow' | 'run' | 'eventId' | 'view' | 'queryParams' >; export type EventParameters = Pick< RouteParameters, @@ -164,7 +164,7 @@ export const routeForEventHistory = ({ ...parameters }: EventHistoryParameters): string => { const eventHistoryPath = `${routeForWorkflow(parameters)}/history`; - return toURL(`${eventHistoryPath}`, queryParams); + return toURL(`${eventHistoryPath}`, queryParams, parameters?.eventId); }; export const routeForEventHistoryEvent = ({ diff --git a/src/lib/utilities/to-url.test.ts b/src/lib/utilities/to-url.test.ts index a7b408a26..cf6db8a34 100644 --- a/src/lib/utilities/to-url.test.ts +++ b/src/lib/utilities/to-url.test.ts @@ -7,6 +7,10 @@ describe('toURL', () => { expect(toURL('/workflows')).toBe('/workflows'); }); + it('should take a string and hash for the URL and return it', () => { + expect(toURL('/workflows', undefined, '123')).toBe('/workflows#123'); + }); + it('should turn the query params into a query string', () => { const params = new URLSearchParams({ a: 'hello' }); expect(toURL('/workflows', params)).toBe('/workflows?a=hello'); @@ -16,4 +20,14 @@ describe('toURL', () => { const params = { a: 'hello' }; expect(toURL('/workflows', params)).toBe('/workflows?a=hello'); }); + + it('should turn an object into a query string and include it with a hash', () => { + const params = { a: 'hello' }; + expect(toURL('/workflows', params, '123')).toBe('/workflows?a=hello#123'); + }); + + it('should turn the query params into a query string with a hash', () => { + const params = new URLSearchParams({ a: 'hello' }); + expect(toURL('/workflows', params, '1')).toBe('/workflows?a=hello#1'); + }); }); diff --git a/src/lib/utilities/to-url.ts b/src/lib/utilities/to-url.ts index 986584fb5..77e9cf195 100644 --- a/src/lib/utilities/to-url.ts +++ b/src/lib/utilities/to-url.ts @@ -1,9 +1,11 @@ export const toURL = ( url: string, params?: URLSearchParams | Record, + hash?: string, ): string => { const isURLSearchParams = params instanceof URLSearchParams; if (params && !isURLSearchParams) params = new URLSearchParams(params); - if (params) return `${url}?${params}`; + if (params) url = `${url}?${params}`; + if (hash) url = `${url}#${hash}`; return url; };