-
-
- {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;
};