From 566cfca8b2485fccd5e8a8ced7033b80b628db27 Mon Sep 17 00:00:00 2001 From: David Juhasz Date: Wed, 2 Oct 2024 11:14:51 -0700 Subject: [PATCH] Add total package count and improved pager Fixes #988: The package list shows the total number of packages found, before paging. Other changes: - Show text indicating which packages are currently displayed as well as the total number of results, e.g. "Showing 1 - 20 of 228" - Show the pager only when there are more results than the page limit, e.g. if 27 results are returned an the page limit is 20 - Include individual page links for up to 7 pages - Show "first" and "last" page links when more than 7 pages of results are returned - Show an ellipsis before or after the page links to indicated there are more pages than the page links show - Hide page links and ellipses on narrow screens --- dashboard/src/client.ts | 2 - .../.openapi-generator/FILES | 4 +- .../src/openapi-generator/apis/PackageApi.ts | 89 ++++++- .../src/openapi-generator/apis/UploadApi.ts | 91 ------- dashboard/src/openapi-generator/apis/index.ts | 1 - .../models/EnduroPackagePreservationAction.ts | 22 +- .../models/EnduroPackagePreservationTask.ts | 22 +- ...{ListResponseBody.ts => EnduroPackages.ts} | 35 ++- .../openapi-generator/models/EnduroPage.ts | 84 ++++++ .../models/EnduroStoredPackage.ts | 241 +++++++++--------- .../src/openapi-generator/models/index.ts | 3 +- dashboard/src/pages/packages/index.vue | 93 ++++++- .../src/stores/__tests__/package.test.ts | 169 ++++++++++++ dashboard/src/stores/package.ts | 89 +++++-- hack/genpkgs/main.go | 2 +- 15 files changed, 650 insertions(+), 297 deletions(-) delete mode 100644 dashboard/src/openapi-generator/apis/UploadApi.ts rename dashboard/src/openapi-generator/models/{ListResponseBody.ts => EnduroPackages.ts} (59%) create mode 100644 dashboard/src/openapi-generator/models/EnduroPage.ts diff --git a/dashboard/src/client.ts b/dashboard/src/client.ts index d086f9a1e..e3192992e 100644 --- a/dashboard/src/client.ts +++ b/dashboard/src/client.ts @@ -7,7 +7,6 @@ import { usePackageStore } from "./stores/package"; export interface Client { package: api.PackageApi; storage: api.StorageApi; - upload: api.UploadApi; connectPackageMonitor: () => void; } @@ -79,7 +78,6 @@ function createClient(): Client { return { package: new api.PackageApi(config), storage: new api.StorageApi(config), - upload: new api.UploadApi(config), connectPackageMonitor, }; } diff --git a/dashboard/src/openapi-generator/.openapi-generator/FILES b/dashboard/src/openapi-generator/.openapi-generator/FILES index 0ccd66d85..7958eac1f 100644 --- a/dashboard/src/openapi-generator/.openapi-generator/FILES +++ b/dashboard/src/openapi-generator/.openapi-generator/FILES @@ -2,7 +2,6 @@ apis/PackageApi.ts apis/StorageApi.ts apis/SwaggerApi.ts -apis/UploadApi.ts apis/index.ts index.ts models/AddLocationRequestBody.ts @@ -13,8 +12,9 @@ models/CreateRequestBody.ts models/EnduroPackagePreservationAction.ts models/EnduroPackagePreservationActions.ts models/EnduroPackagePreservationTask.ts +models/EnduroPackages.ts +models/EnduroPage.ts models/EnduroStoredPackage.ts -models/ListResponseBody.ts models/Location.ts models/LocationNotFound.ts models/LocationResponse.ts diff --git a/dashboard/src/openapi-generator/apis/PackageApi.ts b/dashboard/src/openapi-generator/apis/PackageApi.ts index ace67ce84..25251e47a 100644 --- a/dashboard/src/openapi-generator/apis/PackageApi.ts +++ b/dashboard/src/openapi-generator/apis/PackageApi.ts @@ -17,8 +17,8 @@ import * as runtime from '../runtime'; import type { ConfirmRequestBody, EnduroPackagePreservationActions, + EnduroPackages, EnduroStoredPackage, - ListResponseBody, MonitorEvent, MoveStatusResult, PackageNotFound, @@ -28,10 +28,10 @@ import { ConfirmRequestBodyToJSON, EnduroPackagePreservationActionsFromJSON, EnduroPackagePreservationActionsToJSON, + EnduroPackagesFromJSON, + EnduroPackagesToJSON, EnduroStoredPackageFromJSON, EnduroStoredPackageToJSON, - ListResponseBodyFromJSON, - ListResponseBodyToJSON, MonitorEventFromJSON, MonitorEventToJSON, MoveStatusResultFromJSON, @@ -52,7 +52,8 @@ export interface PackageListRequest { latestCreatedTime?: Date; locationId?: string; status?: PackageListStatusEnum; - cursor?: string; + limit?: number; + offset?: number; } export interface PackageMonitorRequest { @@ -80,6 +81,10 @@ export interface PackageShowRequest { id: number; } +export interface PackageUploadRequest { + contentType?: string; +} + /** * PackageApi - interface * @@ -113,18 +118,19 @@ export interface PackageApiInterface { * @param {Date} [latestCreatedTime] * @param {string} [locationId] Identifier of storage location * @param {'new' | 'in progress' | 'done' | 'error' | 'unknown' | 'queued' | 'abandoned' | 'pending'} [status] - * @param {string} [cursor] Pagination cursor + * @param {number} [limit] Limit number of results to return + * @param {number} [offset] Offset from the beginning of the found set * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof PackageApiInterface */ - packageListRaw(requestParameters: PackageListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + packageListRaw(requestParameters: PackageListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; /** * List all stored packages * list package */ - packageList(requestParameters: PackageListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + packageList(requestParameters: PackageListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; /** * Obtain access to the /monitor WebSocket @@ -238,6 +244,22 @@ export interface PackageApiInterface { */ packageShow(requestParameters: PackageShowRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + /** + * Upload a package to trigger an ingest workflow + * @summary upload package + * @param {string} [contentType] Content-Type header, must define value for multipart boundary. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PackageApiInterface + */ + packageUploadRaw(requestParameters: PackageUploadRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Upload a package to trigger an ingest workflow + * upload package + */ + packageUpload(requestParameters: PackageUploadRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + } /** @@ -295,7 +317,7 @@ export class PackageApi extends runtime.BaseAPI implements PackageApiInterface { * List all stored packages * list package */ - async packageListRaw(requestParameters: PackageListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + async packageListRaw(requestParameters: PackageListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { const queryParameters: any = {}; if (requestParameters.name !== undefined) { @@ -322,8 +344,12 @@ export class PackageApi extends runtime.BaseAPI implements PackageApiInterface { queryParameters['status'] = requestParameters.status; } - if (requestParameters.cursor !== undefined) { - queryParameters['cursor'] = requestParameters.cursor; + if (requestParameters.limit !== undefined) { + queryParameters['limit'] = requestParameters.limit; + } + + if (requestParameters.offset !== undefined) { + queryParameters['offset'] = requestParameters.offset; } const headerParameters: runtime.HTTPHeaders = {}; @@ -343,14 +369,14 @@ export class PackageApi extends runtime.BaseAPI implements PackageApiInterface { query: queryParameters, }, initOverrides); - return new runtime.JSONApiResponse(response, (jsonValue) => ListResponseBodyFromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => EnduroPackagesFromJSON(jsonValue)); } /** * List all stored packages * list package */ - async packageList(requestParameters: PackageListRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + async packageList(requestParameters: PackageListRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { const response = await this.packageListRaw(requestParameters, initOverrides); return await response.value(); } @@ -622,6 +648,45 @@ export class PackageApi extends runtime.BaseAPI implements PackageApiInterface { return await response.value(); } + /** + * Upload a package to trigger an ingest workflow + * upload package + */ + async packageUploadRaw(requestParameters: PackageUploadRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (requestParameters.contentType !== undefined && requestParameters.contentType !== null) { + headerParameters['Content-Type'] = String(requestParameters.contentType); + } + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("jwt_header_Authorization", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + const response = await this.request({ + path: `/package/upload`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * Upload a package to trigger an ingest workflow + * upload package + */ + async packageUpload(requestParameters: PackageUploadRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.packageUploadRaw(requestParameters, initOverrides); + } + } /** diff --git a/dashboard/src/openapi-generator/apis/UploadApi.ts b/dashboard/src/openapi-generator/apis/UploadApi.ts deleted file mode 100644 index 4808c52bf..000000000 --- a/dashboard/src/openapi-generator/apis/UploadApi.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Enduro API - * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - * - * The version of the OpenAPI document: 0.0.1 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -import * as runtime from '../runtime'; - -export interface UploadUploadRequest { - contentType?: string; -} - -/** - * UploadApi - interface - * - * @export - * @interface UploadApiInterface - */ -export interface UploadApiInterface { - /** - * Upload a package to trigger an ingest workflow - * @summary upload upload - * @param {string} [contentType] Content-Type header, must define value for multipart boundary. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof UploadApiInterface - */ - uploadUploadRaw(requestParameters: UploadUploadRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; - - /** - * Upload a package to trigger an ingest workflow - * upload upload - */ - uploadUpload(requestParameters: UploadUploadRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; - -} - -/** - * - */ -export class UploadApi extends runtime.BaseAPI implements UploadApiInterface { - - /** - * Upload a package to trigger an ingest workflow - * upload upload - */ - async uploadUploadRaw(requestParameters: UploadUploadRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - if (requestParameters.contentType !== undefined && requestParameters.contentType !== null) { - headerParameters['Content-Type'] = String(requestParameters.contentType); - } - - if (this.configuration && this.configuration.accessToken) { - const token = this.configuration.accessToken; - const tokenString = await token("jwt_header_Authorization", []); - - if (tokenString) { - headerParameters["Authorization"] = `Bearer ${tokenString}`; - } - } - const response = await this.request({ - path: `/upload/upload`, - method: 'POST', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.VoidApiResponse(response); - } - - /** - * Upload a package to trigger an ingest workflow - * upload upload - */ - async uploadUpload(requestParameters: UploadUploadRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - await this.uploadUploadRaw(requestParameters, initOverrides); - } - -} diff --git a/dashboard/src/openapi-generator/apis/index.ts b/dashboard/src/openapi-generator/apis/index.ts index d5f890a63..530db6280 100644 --- a/dashboard/src/openapi-generator/apis/index.ts +++ b/dashboard/src/openapi-generator/apis/index.ts @@ -3,4 +3,3 @@ export * from './PackageApi'; export * from './StorageApi'; export * from './SwaggerApi'; -export * from './UploadApi'; diff --git a/dashboard/src/openapi-generator/models/EnduroPackagePreservationAction.ts b/dashboard/src/openapi-generator/models/EnduroPackagePreservationAction.ts index 8660ae61d..4d7e4a72a 100644 --- a/dashboard/src/openapi-generator/models/EnduroPackagePreservationAction.ts +++ b/dashboard/src/openapi-generator/models/EnduroPackagePreservationAction.ts @@ -5,7 +5,7 @@ * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 0.0.1 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -27,49 +27,49 @@ import { */ export interface EnduroPackagePreservationAction { /** - * + * * @type {Date} * @memberof EnduroPackagePreservationAction */ completedAt?: Date; /** - * + * * @type {number} * @memberof EnduroPackagePreservationAction */ id: number; /** - * + * * @type {number} * @memberof EnduroPackagePreservationAction */ packageId?: number; /** - * + * * @type {Date} * @memberof EnduroPackagePreservationAction */ startedAt: Date; /** - * + * * @type {string} * @memberof EnduroPackagePreservationAction */ status: EnduroPackagePreservationActionStatusEnum; /** - * + * * @type {Array} * @memberof EnduroPackagePreservationAction */ tasks?: Array; /** - * + * * @type {string} * @memberof EnduroPackagePreservationAction */ type: EnduroPackagePreservationActionTypeEnum; /** - * + * * @type {string} * @memberof EnduroPackagePreservationAction */ @@ -125,7 +125,7 @@ export function EnduroPackagePreservationActionFromJSONTyped(json: any, ignoreDi return json; } return { - + 'completedAt': !exists(json, 'completed_at') ? undefined : (new Date(json['completed_at'])), 'id': json['id'], 'packageId': !exists(json, 'package_id') ? undefined : json['package_id'], @@ -145,7 +145,7 @@ export function EnduroPackagePreservationActionToJSON(value?: EnduroPackagePrese return null; } return { - + 'completed_at': value.completedAt === undefined ? undefined : (value.completedAt.toISOString()), 'id': value.id, 'package_id': value.packageId, diff --git a/dashboard/src/openapi-generator/models/EnduroPackagePreservationTask.ts b/dashboard/src/openapi-generator/models/EnduroPackagePreservationTask.ts index a2fbd662f..30a995226 100644 --- a/dashboard/src/openapi-generator/models/EnduroPackagePreservationTask.ts +++ b/dashboard/src/openapi-generator/models/EnduroPackagePreservationTask.ts @@ -5,7 +5,7 @@ * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 0.0.1 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech @@ -20,49 +20,49 @@ import { exists, mapValues } from '../runtime'; */ export interface EnduroPackagePreservationTask { /** - * + * * @type {Date} * @memberof EnduroPackagePreservationTask */ completedAt?: Date; /** - * + * * @type {number} * @memberof EnduroPackagePreservationTask */ id: number; /** - * + * * @type {string} * @memberof EnduroPackagePreservationTask */ name: string; /** - * + * * @type {string} * @memberof EnduroPackagePreservationTask */ note?: string; /** - * + * * @type {number} * @memberof EnduroPackagePreservationTask */ preservationActionId?: number; /** - * + * * @type {Date} * @memberof EnduroPackagePreservationTask */ startedAt: Date; /** - * + * * @type {string} * @memberof EnduroPackagePreservationTask */ status: EnduroPackagePreservationTaskStatusEnum; /** - * + * * @type {string} * @memberof EnduroPackagePreservationTask */ @@ -107,7 +107,7 @@ export function EnduroPackagePreservationTaskFromJSONTyped(json: any, ignoreDisc return json; } return { - + 'completedAt': !exists(json, 'completed_at') ? undefined : (new Date(json['completed_at'])), 'id': json['id'], 'name': json['name'], @@ -127,7 +127,7 @@ export function EnduroPackagePreservationTaskToJSON(value?: EnduroPackagePreserv return null; } return { - + 'completed_at': value.completedAt === undefined ? undefined : (value.completedAt.toISOString()), 'id': value.id, 'name': value.name, diff --git a/dashboard/src/openapi-generator/models/ListResponseBody.ts b/dashboard/src/openapi-generator/models/EnduroPackages.ts similarity index 59% rename from dashboard/src/openapi-generator/models/ListResponseBody.ts rename to dashboard/src/openapi-generator/models/EnduroPackages.ts index c69e2c424..7ce4cd289 100644 --- a/dashboard/src/openapi-generator/models/ListResponseBody.ts +++ b/dashboard/src/openapi-generator/models/EnduroPackages.ts @@ -13,6 +13,12 @@ */ import { exists, mapValues } from '../runtime'; +import type { EnduroPage } from './EnduroPage'; +import { + EnduroPageFromJSON, + EnduroPageFromJSONTyped, + EnduroPageToJSON, +} from './EnduroPage'; import type { EnduroStoredPackage } from './EnduroStoredPackage'; import { EnduroStoredPackageFromJSON, @@ -23,49 +29,50 @@ import { /** * * @export - * @interface ListResponseBody + * @interface EnduroPackages */ -export interface ListResponseBody { +export interface EnduroPackages { /** * * @type {Array} - * @memberof ListResponseBody + * @memberof EnduroPackages */ items: Array; /** * - * @type {string} - * @memberof ListResponseBody + * @type {EnduroPage} + * @memberof EnduroPackages */ - nextCursor?: string; + page: EnduroPage; } /** - * Check if a given object implements the ListResponseBody interface. + * Check if a given object implements the EnduroPackages interface. */ -export function instanceOfListResponseBody(value: object): boolean { +export function instanceOfEnduroPackages(value: object): boolean { let isInstance = true; isInstance = isInstance && "items" in value; + isInstance = isInstance && "page" in value; return isInstance; } -export function ListResponseBodyFromJSON(json: any): ListResponseBody { - return ListResponseBodyFromJSONTyped(json, false); +export function EnduroPackagesFromJSON(json: any): EnduroPackages { + return EnduroPackagesFromJSONTyped(json, false); } -export function ListResponseBodyFromJSONTyped(json: any, ignoreDiscriminator: boolean): ListResponseBody { +export function EnduroPackagesFromJSONTyped(json: any, ignoreDiscriminator: boolean): EnduroPackages { if ((json === undefined) || (json === null)) { return json; } return { 'items': ((json['items'] as Array).map(EnduroStoredPackageFromJSON)), - 'nextCursor': !exists(json, 'next_cursor') ? undefined : json['next_cursor'], + 'page': EnduroPageFromJSON(json['page']), }; } -export function ListResponseBodyToJSON(value?: ListResponseBody | null): any { +export function EnduroPackagesToJSON(value?: EnduroPackages | null): any { if (value === undefined) { return undefined; } @@ -75,7 +82,7 @@ export function ListResponseBodyToJSON(value?: ListResponseBody | null): any { return { 'items': ((value.items as Array).map(EnduroStoredPackageToJSON)), - 'next_cursor': value.nextCursor, + 'page': EnduroPageToJSON(value.page), }; } diff --git a/dashboard/src/openapi-generator/models/EnduroPage.ts b/dashboard/src/openapi-generator/models/EnduroPage.ts new file mode 100644 index 000000000..9ea8af524 --- /dev/null +++ b/dashboard/src/openapi-generator/models/EnduroPage.ts @@ -0,0 +1,84 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Enduro API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.0.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * Page represents a subset of search results. + * @export + * @interface EnduroPage + */ +export interface EnduroPage { + /** + * Maximum items per page + * @type {number} + * @memberof EnduroPage + */ + limit: number; + /** + * Offset from first result to start of page + * @type {number} + * @memberof EnduroPage + */ + offset: number; + /** + * Total result count before paging + * @type {number} + * @memberof EnduroPage + */ + total: number; +} + +/** + * Check if a given object implements the EnduroPage interface. + */ +export function instanceOfEnduroPage(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "limit" in value; + isInstance = isInstance && "offset" in value; + isInstance = isInstance && "total" in value; + + return isInstance; +} + +export function EnduroPageFromJSON(json: any): EnduroPage { + return EnduroPageFromJSONTyped(json, false); +} + +export function EnduroPageFromJSONTyped(json: any, ignoreDiscriminator: boolean): EnduroPage { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'limit': json['limit'], + 'offset': json['offset'], + 'total': json['total'], + }; +} + +export function EnduroPageToJSON(value?: EnduroPage | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'limit': value.limit, + 'offset': value.offset, + 'total': value.total, + }; +} + diff --git a/dashboard/src/openapi-generator/models/EnduroStoredPackage.ts b/dashboard/src/openapi-generator/models/EnduroStoredPackage.ts index 7ff661fe8..a042647d1 100644 --- a/dashboard/src/openapi-generator/models/EnduroStoredPackage.ts +++ b/dashboard/src/openapi-generator/models/EnduroStoredPackage.ts @@ -12,146 +12,155 @@ * Do not edit the class manually. */ -import { exists, mapValues } from '../runtime'; +import { exists, mapValues } from "../runtime"; /** * StoredPackage describes a package retrieved by the service. * @export * @interface EnduroStoredPackage */ export interface EnduroStoredPackage { - /** - * Identifier of AIP - * @type {string} - * @memberof EnduroStoredPackage - */ - aipId?: string; - /** - * Completion datetime - * @type {Date} - * @memberof EnduroStoredPackage - */ - completedAt?: Date; - /** - * Creation datetime - * @type {Date} - * @memberof EnduroStoredPackage - */ - createdAt: Date; - /** - * Identifier of package - * @type {number} - * @memberof EnduroStoredPackage - */ - id: number; - /** - * Identifier of storage location - * @type {string} - * @memberof EnduroStoredPackage - */ - locationId?: string; - /** - * Name of the package - * @type {string} - * @memberof EnduroStoredPackage - */ - name?: string; - /** - * Identifier of latest processing workflow run - * @type {string} - * @memberof EnduroStoredPackage - */ - runId?: string; - /** - * Start datetime - * @type {Date} - * @memberof EnduroStoredPackage - */ - startedAt?: Date; - /** - * Status of the package - * @type {string} - * @memberof EnduroStoredPackage - */ - status: EnduroStoredPackageStatusEnum; - /** - * Identifier of processing workflow - * @type {string} - * @memberof EnduroStoredPackage - */ - workflowId?: string; + /** + * Identifier of AIP + * @type {string} + * @memberof EnduroStoredPackage + */ + aipId?: string; + /** + * Completion datetime + * @type {Date} + * @memberof EnduroStoredPackage + */ + completedAt?: Date; + /** + * Creation datetime + * @type {Date} + * @memberof EnduroStoredPackage + */ + createdAt: Date; + /** + * Identifier of package + * @type {number} + * @memberof EnduroStoredPackage + */ + id: number; + /** + * Identifier of storage location + * @type {string} + * @memberof EnduroStoredPackage + */ + locationId?: string; + /** + * Name of the package + * @type {string} + * @memberof EnduroStoredPackage + */ + name?: string; + /** + * Identifier of latest processing workflow run + * @type {string} + * @memberof EnduroStoredPackage + */ + runId?: string; + /** + * Start datetime + * @type {Date} + * @memberof EnduroStoredPackage + */ + startedAt?: Date; + /** + * Status of the package + * @type {string} + * @memberof EnduroStoredPackage + */ + status: EnduroStoredPackageStatusEnum; + /** + * Identifier of processing workflow + * @type {string} + * @memberof EnduroStoredPackage + */ + workflowId?: string; } - /** * @export */ export const EnduroStoredPackageStatusEnum = { - New: 'new', - InProgress: 'in progress', - Done: 'done', - Error: 'error', - Unknown: 'unknown', - Queued: 'queued', - Abandoned: 'abandoned', - Pending: 'pending' + New: "new", + InProgress: "in progress", + Done: "done", + Error: "error", + Unknown: "unknown", + Queued: "queued", + Abandoned: "abandoned", + Pending: "pending", } as const; -export type EnduroStoredPackageStatusEnum = typeof EnduroStoredPackageStatusEnum[keyof typeof EnduroStoredPackageStatusEnum]; - +export type EnduroStoredPackageStatusEnum = + (typeof EnduroStoredPackageStatusEnum)[keyof typeof EnduroStoredPackageStatusEnum]; /** * Check if a given object implements the EnduroStoredPackage interface. */ export function instanceOfEnduroStoredPackage(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "createdAt" in value; - isInstance = isInstance && "id" in value; - isInstance = isInstance && "status" in value; + let isInstance = true; + isInstance = isInstance && "createdAt" in value; + isInstance = isInstance && "id" in value; + isInstance = isInstance && "status" in value; - return isInstance; + return isInstance; } export function EnduroStoredPackageFromJSON(json: any): EnduroStoredPackage { - return EnduroStoredPackageFromJSONTyped(json, false); + return EnduroStoredPackageFromJSONTyped(json, false); } -export function EnduroStoredPackageFromJSONTyped(json: any, ignoreDiscriminator: boolean): EnduroStoredPackage { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'aipId': !exists(json, 'aip_id') ? undefined : json['aip_id'], - 'completedAt': !exists(json, 'completed_at') ? undefined : (new Date(json['completed_at'])), - 'createdAt': (new Date(json['created_at'])), - 'id': json['id'], - 'locationId': !exists(json, 'location_id') ? undefined : json['location_id'], - 'name': !exists(json, 'name') ? undefined : json['name'], - 'runId': !exists(json, 'run_id') ? undefined : json['run_id'], - 'startedAt': !exists(json, 'started_at') ? undefined : (new Date(json['started_at'])), - 'status': json['status'], - 'workflowId': !exists(json, 'workflow_id') ? undefined : json['workflow_id'], - }; +export function EnduroStoredPackageFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): EnduroStoredPackage { + if (json === undefined || json === null) { + return json; + } + return { + aipId: !exists(json, "aip_id") ? undefined : json["aip_id"], + completedAt: !exists(json, "completed_at") + ? undefined + : new Date(json["completed_at"]), + createdAt: new Date(json["created_at"]), + id: json["id"], + locationId: !exists(json, "location_id") ? undefined : json["location_id"], + name: !exists(json, "name") ? undefined : json["name"], + runId: !exists(json, "run_id") ? undefined : json["run_id"], + startedAt: !exists(json, "started_at") + ? undefined + : new Date(json["started_at"]), + status: json["status"], + workflowId: !exists(json, "workflow_id") ? undefined : json["workflow_id"], + }; } -export function EnduroStoredPackageToJSON(value?: EnduroStoredPackage | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'aip_id': value.aipId, - 'completed_at': value.completedAt === undefined ? undefined : (value.completedAt.toISOString()), - 'created_at': (value.createdAt.toISOString()), - 'id': value.id, - 'location_id': value.locationId, - 'name': value.name, - 'run_id': value.runId, - 'started_at': value.startedAt === undefined ? undefined : (value.startedAt.toISOString()), - 'status': value.status, - 'workflow_id': value.workflowId, - }; +export function EnduroStoredPackageToJSON( + value?: EnduroStoredPackage | null, +): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + aip_id: value.aipId, + completed_at: + value.completedAt === undefined + ? undefined + : value.completedAt.toISOString(), + created_at: value.createdAt.toISOString(), + id: value.id, + location_id: value.locationId, + name: value.name, + run_id: value.runId, + started_at: + value.startedAt === undefined ? undefined : value.startedAt.toISOString(), + status: value.status, + workflow_id: value.workflowId, + }; } - diff --git a/dashboard/src/openapi-generator/models/index.ts b/dashboard/src/openapi-generator/models/index.ts index 7ac55ef00..0668523ca 100644 --- a/dashboard/src/openapi-generator/models/index.ts +++ b/dashboard/src/openapi-generator/models/index.ts @@ -8,8 +8,9 @@ export * from './CreateRequestBody'; export * from './EnduroPackagePreservationAction'; export * from './EnduroPackagePreservationActions'; export * from './EnduroPackagePreservationTask'; +export * from './EnduroPackages'; +export * from './EnduroPage'; export * from './EnduroStoredPackage'; -export * from './ListResponseBody'; export * from './Location'; export * from './LocationNotFound'; export * from './LocationResponse'; diff --git a/dashboard/src/pages/packages/index.vue b/dashboard/src/pages/packages/index.vue index 337670584..3c4e082e2 100644 --- a/dashboard/src/pages/packages/index.vue +++ b/dashboard/src/pages/packages/index.vue @@ -11,6 +11,10 @@ import Tooltip from "bootstrap/js/dist/tooltip"; import { onMounted } from "vue"; import IconInfoFill from "~icons/akar-icons/info-fill"; import IconBundleLine from "~icons/clarity/bundle-line"; +import IconSkipEndFill from "~icons/bi/skip-end-fill"; +import IconSkipStartFill from "~icons/bi/skip-start-fill"; +import IconCaretRightFill from "~icons/bi/caret-right-fill"; +import IconCaretLeftFill from "~icons/bi/caret-left-fill"; const authStore = useAuthStore(); const layoutStore = useLayoutStore(); @@ -19,7 +23,7 @@ layoutStore.updateBreadcrumb([{ text: "Packages" }]); const packageStore = usePackageStore(); const { execute, error } = useAsyncState(() => { - return packageStore.fetchPackages(); + return packageStore.fetchPackages(1); }, null); const el = $ref(null); @@ -43,8 +47,9 @@ const toggleLegend = () => {
- Showing {{ packageStore.packages.length }} / - {{ packageStore.packages.length }} + Showing {{ packageStore.page.offset + 1 }} - + {{ packageStore.lastResultOnPage }} of + {{ packageStore.page.total }}
@@ -103,9 +108,22 @@ const toggleLegend = () => { - diff --git a/dashboard/src/stores/__tests__/package.test.ts b/dashboard/src/stores/__tests__/package.test.ts index a3f1bfb12..2f318831f 100644 --- a/dashboard/src/stores/__tests__/package.test.ts +++ b/dashboard/src/stores/__tests__/package.test.ts @@ -2,12 +2,138 @@ import { usePackageStore } from "@/stores/package"; import { setActivePinia, createPinia } from "pinia"; import { expect, describe, it, beforeEach } from "vitest"; import { api } from "@/client"; +import type { Pager } from "@/stores/package"; describe("usePackageStore", () => { beforeEach(() => { setActivePinia(createPinia()); }); + it("isPending", () => { + const packageStore = usePackageStore(); + const now = new Date(); + + expect(packageStore.isPending).toEqual(false); + + packageStore.$patch((state) => { + state.current = { + createdAt: now, + id: 1, + status: api.EnduroStoredPackageStatusEnum.Pending, + }; + }); + expect(packageStore.isPending).toEqual(true); + }); + + it("isDone", () => { + const packageStore = usePackageStore(); + const now = new Date(); + + expect(packageStore.isDone).toEqual(false); + + packageStore.$patch((state) => { + state.current = { + createdAt: now, + id: 1, + status: api.EnduroStoredPackageStatusEnum.Done, + }; + }); + expect(packageStore.isDone).toEqual(true); + }); + + it("isMovable", () => { + const packageStore = usePackageStore(); + const now = new Date(); + + expect(packageStore.isMovable).toEqual(false); + + packageStore.$patch((state) => { + state.current = { + createdAt: now, + id: 1, + status: api.EnduroStoredPackageStatusEnum.Done, + }; + }); + expect(packageStore.isMovable).toEqual(true); + }); + + it("isMoving", () => { + const packageStore = usePackageStore(); + + expect(packageStore.isMoving).toEqual(false); + + packageStore.$patch((state) => { + state.locationChanging = true; + }); + expect(packageStore.isMoving).toEqual(true); + }); + + it("isRejected", () => { + const packageStore = usePackageStore(); + const now = new Date(); + + expect(packageStore.isRejected).toEqual(false); + + packageStore.$patch((state) => { + state.current = { + createdAt: now, + id: 1, + status: api.EnduroStoredPackageStatusEnum.Done, + }; + }); + expect(packageStore.isRejected).toEqual(true); + }); + + it("hasNextPage", () => { + const packageStore = usePackageStore(); + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 0, total: 20 }; + }); + expect(packageStore.hasNextPage).toEqual(false); + + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 20, total: 40 }; + }); + expect(packageStore.hasNextPage).toEqual(false); + + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 0, total: 21 }; + }); + expect(packageStore.hasNextPage).toEqual(true); + }); + + it("hasPrevPage", () => { + const packageStore = usePackageStore(); + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 0, total: 40 }; + }); + expect(packageStore.hasPrevPage).toEqual(false); + + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 20, total: 40 }; + }); + expect(packageStore.hasPrevPage).toEqual(true); + }); + + it("returns lastResultOnPage", () => { + const packageStore = usePackageStore(); + + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 0, total: 40 }; + }); + expect(packageStore.lastResultOnPage).toEqual(20); + + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 0, total: 7 }; + }); + expect(packageStore.lastResultOnPage).toEqual(7); + + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 20, total: 35 }; + }); + expect(packageStore.lastResultOnPage).toEqual(35); + }); + it("getActionById finds actions", () => { const packageStore = usePackageStore(); const now = new Date(); @@ -137,4 +263,47 @@ describe("usePackageStore", () => { }); expect(packageStore.getTaskById(2, 5)).toBeUndefined(); }); + + it("updates the pager", () => { + const packageStore = usePackageStore(); + + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 60, total: 125 }; + }); + packageStore.updatePager; + expect(packageStore.pager).toEqual({ + maxPages: 7, + current: 4, + first: 1, + last: 7, + total: 7, + pages: [1, 2, 3, 4, 5, 6, 7], + }); + + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 160, total: 573 }; + }); + packageStore.updatePager; + expect(packageStore.pager).toEqual({ + maxPages: 7, + current: 9, + first: 6, + last: 12, + total: 29, + pages: [6, 7, 8, 9, 10, 11, 12], + }); + + packageStore.$patch((state) => { + state.page = { limit: 20, offset: 540, total: 573 }; + }); + packageStore.updatePager; + expect(packageStore.pager).toEqual({ + maxPages: 7, + current: 28, + first: 23, + last: 29, + total: 29, + pages: [23, 24, 25, 26, 27, 28, 29], + }); + }); }); diff --git a/dashboard/src/stores/package.ts b/dashboard/src/stores/package.ts index c30464dc6..b5b4898c7 100644 --- a/dashboard/src/stores/package.ts +++ b/dashboard/src/stores/package.ts @@ -6,6 +6,17 @@ import { defineStore, acceptHMRUpdate } from "pinia"; import { ref } from "vue"; import { mapKeys, snakeCase } from "lodash-es"; +export interface Pager { + // maxPages is the maximum number of page links to show in the pager. + readonly maxPages: number; + + current: number; + first: number; + last: number; + total: number; + pages: Array; +} + export const usePackageStore = defineStore("package", { state: () => ({ // Package currently displayed. @@ -23,14 +34,11 @@ export const usePackageStore = defineStore("package", { // A list of packages shown during searches. packages: [] as Array, - // Cursor for this page of packages. - cursor: 0, - - // Cursor for next page of packages. - nextCursor: 0, + // Page is a subset of the total package list. + page: { limit: 20 } as api.EnduroPage, - // A list of previous page cursors. - prevCursors: [] as Array, + // Pager contains a list of pages numbers to show in the pager. + pager: { maxPages: 7 } as Pager, // User-interface interactions between components. ui: { @@ -54,10 +62,40 @@ export const usePackageStore = defineStore("package", { return this.isDone && this.current?.locationId === undefined; }, hasNextPage(): boolean { - return this.nextCursor != 0; + return this.page.offset + this.page.limit < this.page.total; }, hasPrevPage(): boolean { - return this.prevCursors.length > 0; + return this.page.offset > 0; + }, + lastResultOnPage(): number { + let i = this.page.offset + this.page.limit; + if (i > this.page.total) { + i = this.page.total; + } + return i; + }, + updatePager(): void { + let pgr = this.pager; + pgr.total = Math.ceil(this.page.total / this.page.limit); + pgr.current = Math.floor(this.page.offset / this.page.limit) + 1; + + let first = 1; + let count = pgr.total < pgr.maxPages ? pgr.total : pgr.maxPages; + let half = Math.floor(pgr.maxPages / 2); + if (pgr.current > half + 1) { + if (pgr.total - pgr.current < half) { + first = pgr.total - count + 1; + } else { + first = pgr.current - half; + } + } + pgr.first = first; + pgr.last = first + count - 1; + + pgr.pages = new Array(count); + for (var i = 0; i < count; i++) { + pgr.pages[i] = i + first; + } }, getActionById: (state) => { return ( @@ -122,15 +160,17 @@ export const usePackageStore = defineStore("package", { }), ]); }, - async fetchPackages() { + async fetchPackages(page: number) { const resp = await client.package.packageList({ - cursor: this.cursor > 0 ? this.cursor.toString() : undefined, + offset: page > 1 ? (page - 1) * this.page.limit : undefined, + limit: this.page?.limit || undefined, }); this.packages = resp.items; - this.nextCursor = Number(resp.nextCursor); + this.page = resp.page; + this.updatePager; }, - async fetchPackagesDebounced() { - return this.fetchPackages(); + async fetchPackagesDebounced(page: number) { + return this.fetchPackages(page); }, async move(locationId: string) { if (!this.current) return; @@ -180,18 +220,13 @@ export const usePackageStore = defineStore("package", { }); }, nextPage() { - if (this.nextCursor == 0) { - return; + if (this.hasNextPage) { + this.fetchPackages(this.pager.current + 1); } - this.prevCursors.push(this.cursor); - this.cursor = this.nextCursor; - this.fetchPackages(); }, prevPage() { - let prev = this.prevCursors.pop(); - if (prev !== undefined) { - this.cursor = prev; - this.fetchPackages(); + if (this.hasPrevPage) { + this.fetchPackages(this.pager.current - 1); } }, }, @@ -231,13 +266,13 @@ function handleMonitorPing(data: any) { function handlePackageCreated(data: any) { const event = api.PackageCreatedEventFromJSON(data); const store = usePackageStore(); - store.fetchPackagesDebounced(); + store.fetchPackagesDebounced(1); } function handlePackageUpdated(data: any) { const event = api.PackageUpdatedEventFromJSON(data); const store = usePackageStore(); - store.fetchPackagesDebounced(); + store.fetchPackagesDebounced(1); if (store.$state.current?.id != event.id) return; Object.assign(store.$state.current, event.item); } @@ -245,7 +280,7 @@ function handlePackageUpdated(data: any) { function handlePackageStatusUpdated(data: any) { const event = api.PackageStatusUpdatedEventFromJSON(data); const store = usePackageStore(); - store.fetchPackagesDebounced(); + store.fetchPackagesDebounced(1); if (store.$state.current?.id != event.id) return; store.$state.current.status = event.status; } @@ -253,7 +288,7 @@ function handlePackageStatusUpdated(data: any) { function handlePackageLocationUpdated(data: any) { const event = api.PackageLocationUpdatedEventFromJSON(data); const store = usePackageStore(); - store.fetchPackagesDebounced(); + store.fetchPackagesDebounced(1); store.$patch((state) => { if (state.current?.id != event.id) return; state.current.locationId = event.locationId; diff --git a/hack/genpkgs/main.go b/hack/genpkgs/main.go index 15203fe26..2b76001b9 100644 --- a/hack/genpkgs/main.go +++ b/hack/genpkgs/main.go @@ -12,7 +12,7 @@ import ( const ( // How many records we want to generate. - datasetSize = 10000 + datasetSize = 1000 // Size of each batch that we write to the CSV. batchSize = 100