From 15a8d6bc112319f14b7a60e0a92a958956efa1ef Mon Sep 17 00:00:00 2001 From: Tomasz Pluskiewicz Date: Mon, 30 Dec 2024 15:03:02 +0100 Subject: [PATCH] feat(ui): pager for shared dimensions --- .../lib/domain/shared-dimensions.ts | 24 ++++++---- .../lib/shapes/dimensions-query-shape.ttl | 22 ++++++++- apis/shared-dimensions/package.json | 2 +- packages/core/namespace.ts | 4 +- packages/model/Project.ts | 2 +- ui/src/store/modules/projects.ts | 18 +++++++- ui/src/store/modules/sharedDimensions.ts | 7 ++- ui/src/store/serializers.ts | 36 +++++++-------- ui/src/views/SharedDimensions.vue | 46 +++++++++++++++++-- yarn.lock | 8 ++-- 10 files changed, 125 insertions(+), 44 deletions(-) diff --git a/apis/shared-dimensions/lib/domain/shared-dimensions.ts b/apis/shared-dimensions/lib/domain/shared-dimensions.ts index 7ea5f54ea..1a29248a1 100644 --- a/apis/shared-dimensions/lib/domain/shared-dimensions.ts +++ b/apis/shared-dimensions/lib/domain/shared-dimensions.ts @@ -1,8 +1,8 @@ import path from 'path' -import type { Quad, Stream, Term, Literal } from '@rdfjs/types' +import type { Quad, Stream, Term, Literal, NamedNode } from '@rdfjs/types' import { hydra, rdf, schema, sh } from '@tpluscode/rdf-ns-builders' import $rdf from 'rdf-ext' -import { toRdf } from 'rdf-literal' +import { toRdf, fromRdf } from 'rdf-literal' import { fromFile } from 'rdf-utils-fs' import clownface from 'clownface' import { isResource } from 'is-graph-pointer' @@ -21,10 +21,11 @@ interface GetSharedDimensions { includeDeprecated?: Literal } -export async function getSharedDimensions(client: StreamClient, { freetextQuery = '', limit = 10, offset = 0, includeDeprecated }: GetSharedDimensions = {}): Promise> { +export async function getSharedDimensions(client: StreamClient, { freetextQuery = '', limit = 10, offset = 0, includeDeprecated }: GetSharedDimensions = {}): Promise>> { const { constructQuery } = await shapeToQuery() - const shape = await loadShape('dimensions-query-shape') + const memberQueryShape = await loadShape('dimensions-query-shape', md.MembersQueryShape) + const totalQueryShape = await loadShape('dimensions-query-shape', md.CountQueryShape) const { MANAGED_DIMENSIONS_BASE } = env const variables = new Map(Object.entries({ @@ -35,9 +36,10 @@ export async function getSharedDimensions(client: StreamClient, { freetextQuery includeDeprecated, orderBy: schema.name, })) - await rewriteTemplates(shape, variables) + await rewriteTemplates(memberQueryShape, variables) + await rewriteTemplates(totalQueryShape, variables) - const dataset = await $rdf.dataset().import(await client.query.construct(constructQuery(shape))) + const dataset = await $rdf.dataset().import(await client.query.construct(constructQuery(memberQueryShape))) clownface({ dataset }) .has(rdf.type, schema.DefinedTermSet) .forEach(termSet => { @@ -45,9 +47,13 @@ export async function getSharedDimensions(client: StreamClient, { freetextQuery termSet.addOut(md.terms, $rdf.namedNode(`${MANAGED_DIMENSIONS_BASE}dimension/_terms?dimension=${termSet.value}`)) }) + const totalItems = clownface({ + dataset: await $rdf.dataset().import(await client.query.construct(constructQuery(totalQueryShape))), + }).has(hydra.totalItems).out(hydra.totalItems).term as Literal + return { members: dataset, - totalItems: dataset.match(null, rdf.type, schema.DefinedTermSet).length, + totalItems: fromRdf(totalItems), } } @@ -97,12 +103,12 @@ export async function getSharedTerms({ s }) as any } -async function loadShape(shape: string) { +async function loadShape(shape: string, shapeType: NamedNode = sh.NodeShape) { const dataset = await $rdf.dataset().import(fromFile(path.resolve(__dirname, `../shapes/${shape}.ttl`))) const ptr = clownface({ dataset, - }).has(rdf.type, sh.NodeShape) + }).has(rdf.type, shapeType) if (!isResource(ptr)) { throw new Error('Expected a single blank node or named node') diff --git a/apis/shared-dimensions/lib/shapes/dimensions-query-shape.ttl b/apis/shared-dimensions/lib/shapes/dimensions-query-shape.ttl index 78a7b6dee..244bea78b 100644 --- a/apis/shared-dimensions/lib/shapes/dimensions-query-shape.ttl +++ b/apis/shared-dimensions/lib/shapes/dimensions-query-shape.ttl @@ -1,3 +1,4 @@ +PREFIX hydra: PREFIX xsd: @prefix sh: . @prefix rdf: . @@ -8,7 +9,26 @@ PREFIX sparql: prefix md: [ - a sh:NodeShape ; + a sh:NodeShape, md:CountQueryShape ; + sh:rule + [ + sh:subject hydra:Collection ; + sh:predicate hydra:totalItems ; + sh:object + [ + sh:count + [ + sh:distinct + [ + sh:filterShape _:FilterShape ; + ] ; + ] + ] ; + ] ; +] . + +[ + a sh:NodeShape, md:MembersQueryShape ; sh:target [ a s2q:NodeExpressionTarget ; diff --git a/apis/shared-dimensions/package.json b/apis/shared-dimensions/package.json index ebee17398..2e16344e1 100644 --- a/apis/shared-dimensions/package.json +++ b/apis/shared-dimensions/package.json @@ -7,7 +7,7 @@ "@cube-creator/core": "1.0.0", "@cube-creator/express": "0.0.0", "@hydrofoil/labyrinth": "^0.4.2", - "@hydrofoil/shape-to-query": "^0.13.2", + "@hydrofoil/shape-to-query": "^0.13.4", "@rdfine/hydra": "^0.8.2", "@rdfine/rdfs": "^0.6.4", "@rdfine/schema": "^0.6.3", diff --git a/packages/core/namespace.ts b/packages/core/namespace.ts index 39ca5c27c..bea62c4e0 100644 --- a/packages/core/namespace.ts +++ b/packages/core/namespace.ts @@ -115,7 +115,9 @@ type SharedDimensionsTerms = 'Hierarchies' | 'Hierarchy' | 'Entrypoint' | - 'FreeTextSearchConstraintComponent' + 'FreeTextSearchConstraintComponent' | + 'MembersQueryShape' | + 'CountQueryShape' prefixes.view = 'https://cube.link/view/' diff --git a/packages/model/Project.ts b/packages/model/Project.ts index 7906e89ad..f08902027 100644 --- a/packages/model/Project.ts +++ b/packages/model/Project.ts @@ -42,8 +42,8 @@ export interface ImportProject extends Project { export type CubeProject = CsvProject | ImportProject +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ProjectsCollection extends Collection { - searchParams: GraphPointer } export const isCsvProject = (project: CsvProject | ImportProject): project is CsvProject => { diff --git a/ui/src/store/modules/projects.ts b/ui/src/store/modules/projects.ts index fce0eaee1..8e44d5979 100644 --- a/ui/src/store/modules/projects.ts +++ b/ui/src/store/modules/projects.ts @@ -2,7 +2,7 @@ import { ActionTree, MutationTree, GetterTree } from 'vuex' import { api } from '@/api' import { RootState } from '../types' import * as ns from '@cube-creator/core/namespace' -import { Project, ProjectsCollection } from '@cube-creator/model' +import { CubeProject, Project, ProjectsCollection } from '@cube-creator/model' import { serializeProjectDetails, serializeCollection } from '../serializers' import { RdfResource } from 'alcaeus' @@ -53,7 +53,7 @@ const actions: ActionTree = { const mutations: MutationTree = { storeCollection (state, collection) { - state.collection = collection ? serializeCollection(collection) : null + state.collection = collection ? serializeCollection(collection, sortProject) : null }, storeProjectDetails (state, { project, details }) { @@ -64,6 +64,20 @@ const mutations: MutationTree = { }, } +function sortProject (a: CubeProject, b: CubeProject) { + const aPlannedUpdate = a.plannedNextUpdate?.toISOString() + if (!aPlannedUpdate) { + return 1 + } + + const bPlannedUpdate = b.plannedNextUpdate?.toISOString() + if (!bPlannedUpdate) { + return -1 + } + + return aPlannedUpdate.localeCompare(bPlannedUpdate) || a.label.localeCompare(b.label) +} + export default { namespaced: true, state: initialState, diff --git a/ui/src/store/modules/sharedDimensions.ts b/ui/src/store/modules/sharedDimensions.ts index 210b3e31e..260f30a45 100644 --- a/ui/src/store/modules/sharedDimensions.ts +++ b/ui/src/store/modules/sharedDimensions.ts @@ -4,6 +4,7 @@ import { RootState } from '../types' import { cc, md } from '@cube-creator/core/namespace' import { Collection, RdfResource } from 'alcaeus' import { serializeCollection } from '@/store/serializers' +import { schema } from '@tpluscode/rdf-ns-builders' export interface SharedDimensionsState { entrypoint: null | RdfResource @@ -63,10 +64,14 @@ const mutations: MutationTree = { }, storeCollection (state, collection) { - state.collection = collection ? serializeCollection(collection) : null + state.collection = collection ? serializeCollection(collection, sortByName) : null }, } +function sortByName (l: RdfResource, r: RdfResource) { + return l.pointer.out(schema.name).value?.localeCompare(r.pointer.out(schema.name).value || '') || 0 +} + export default { namespaced: true, state: initialState, diff --git a/ui/src/store/serializers.ts b/ui/src/store/serializers.ts index 689426caa..fc534ed10 100644 --- a/ui/src/store/serializers.ts +++ b/ui/src/store/serializers.ts @@ -10,40 +10,38 @@ import { DimensionMetadata, DimensionMetadataCollection, JobCollection, - ProjectsCollection, SourcesCollection, Table, TableCollection, } from '@cube-creator/model' import { IdentifierMapping, LiteralColumnMapping, ReferenceColumnMapping } from '@cube-creator/model/ColumnMapping' import { Link } from '@cube-creator/model/lib/Link' -import { dcterms, oa, rdf, rdfs, schema } from '@tpluscode/rdf-ns-builders' +import { dcterms, hydra, oa, rdf, rdfs, schema } from '@tpluscode/rdf-ns-builders' import { RdfResource, ResourceIdentifier } from '@tpluscode/rdfine/RdfResource' import { ProjectDetails, SharedDimensionTerm } from './types' import { clone } from '@/store/searchParams' +import type { Collection } from '@rdfine/hydra' +import type { GraphPointer } from 'clownface' export const displayLanguage = ['en', 'de', 'fr', ''] -export function serializeCollection (collection: ProjectsCollection): ProjectsCollection { - return Object.freeze({ - ...serializeResource(collection), - searchParams: clone(collection.pointer), - member: collection.member.sort(sortProject), - }) as ProjectsCollection -} - -function sortProject (a: CubeProject, b: CubeProject) { - const aPlannedUpdate = a.plannedNextUpdate?.toISOString() - if (!aPlannedUpdate) { - return 1 +declare module '@rdfine/hydra' { + interface Collection { + searchParams: GraphPointer + pageSize: number + perPage: number } +} - const bPlannedUpdate = b.plannedNextUpdate?.toISOString() - if (!bPlannedUpdate) { - return -1 - } +export function serializeCollection (collection: Collection, sort: (l: T, r: T) => number): Collection { + const member = sort ? collection.member.sort(sort) : collection.member - return aPlannedUpdate.localeCompare(bPlannedUpdate) || a.label.localeCompare(b.label) + return Object.freeze({ + ...serializeResource(collection), + searchParams: clone(collection.pointer), + member, + totalItems: collection.totalItems, + }) as Collection } export function serializeProjectDetails (details: RdfResource): ProjectDetails { diff --git a/ui/src/views/SharedDimensions.vue b/ui/src/views/SharedDimensions.vue index 759fd4cd5..02fff6080 100644 --- a/ui/src/views/SharedDimensions.vue +++ b/ui/src/views/SharedDimensions.vue @@ -52,7 +52,13 @@ - +

No shared dimension yet @@ -76,6 +82,7 @@ import TermWithLanguage from '@/components/TermWithLanguage.vue' import { SharedDimension } from '@/store/types' import { useHydraForm } from '@/use-hydra-form' import { getRouteSearchParamsFromTemplatedOperation } from '@/router' +import { hydra } from '@tpluscode/rdf-ns-builders' export default defineComponent({ name: 'CubeProjectsView', @@ -94,6 +101,8 @@ export default defineComponent({ this.operation = this.collection.actions.get this.searchParams = this.collection.searchParams + this.pageSize = this.searchParams.out(hydra.limit).value + this.page = parseInt(this.$router.currentRoute.value.query.page) || 1 }, setup () { @@ -103,6 +112,8 @@ export default defineComponent({ return { ...form, + page: 1, + pageSize: 0, searchParams: shallowRef() } }, @@ -122,6 +133,8 @@ export default defineComponent({ async beforeRouteUpdate (to) { await this.$store.dispatch('sharedDimensions/fetchCollection', to.query) this.searchParams = this.collection.searchParams + this.pageSize = this.searchParams.out(hydra.limit).value + this.page = parseInt(to.query.page) || 1 }, methods: { @@ -131,11 +144,34 @@ export default defineComponent({ onSearch (e: CustomEvent) { if (this.operation && e.detail?.value) { - this.$router.push({ - query: getRouteSearchParamsFromTemplatedOperation(this.operation, e.detail?.value), - }) + this.page = 1 + const query = { + ...getRouteSearchParamsFromTemplatedOperation(this.operation, e.detail?.value), + page: 1 + } + this.$router.push({ query }) } - } + }, + + nextPage () { + this.fetchPage(this.page + 1) + }, + + prevPage () { + this.fetchPage(this.page - 1) + }, + + fetchPage (page: number) { + if (this.operation) { + this.page = page + const query = { + ...getRouteSearchParamsFromTemplatedOperation(this.operation, this.searchParams), + page + } + + this.$router.push({ query, }) + } + }, } }) diff --git a/yarn.lock b/yarn.lock index 5e1155390..9bf8f95dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1566,10 +1566,10 @@ rdf-loaders-registry "^0.2.0" sparql-http-client "^2.2.2" -"@hydrofoil/shape-to-query@^0.13.2": - version "0.13.2" - resolved "https://registry.yarnpkg.com/@hydrofoil/shape-to-query/-/shape-to-query-0.13.2.tgz#475939fe9ec5a3c0bd316f6a32bfaf4caf32cf83" - integrity sha512-7w1wPexW0XDiF7WtRkQtqnZTcJvhYKW+2rNIbl9cqZqto3A/8aKT+Hm4OKM3WTOgmGDpyDwbtE/itu5LkjzoEA== +"@hydrofoil/shape-to-query@^0.13.4": + version "0.13.4" + resolved "https://registry.yarnpkg.com/@hydrofoil/shape-to-query/-/shape-to-query-0.13.4.tgz#61ac73a33a1c8a34f6652a284ec7e2fdf5101b12" + integrity sha512-Q9GDAacePZZQkwnOxOD43BlkWBNDKr+VR05l3B8Q99D9gKYZ5AeF8kNDdH3za9LzES9Hc3hs/g0R8sM0Rz8WiQ== dependencies: "@hydrofoil/sparql-processor" "^0.1.2" "@tpluscode/rdf-ns-builders" ">=3.0.2"