From 7d5f55d9f077d00d4c3b2eec2bdd002369b547df Mon Sep 17 00:00:00 2001 From: trex <307289341+ohdsi-trex@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:01:12 +0000 Subject: [PATCH] feat: add data-source-ui single-spa plugin --- plans/data-source-ui-atlas3-plugin.md | 528 +++++++++++++ plans/data-source-ui.md | 618 ++++++++++++++++ plugins/atlas/plugins.standalone.json | 103 +-- plugins/ui/apps/data-source-ui/PLAN.md | 698 ++++++++++++++++++ plugins/ui/apps/data-source-ui/index.html | 12 + plugins/ui/apps/data-source-ui/package.json | 22 + plugins/ui/apps/data-source-ui/src/App.vue | 3 + .../apps/data-source-ui/src/api/datasource.ts | 18 + .../data-source-ui/src/components/.gitkeep | 0 plugins/ui/apps/data-source-ui/src/main.ts | 26 + .../apps/data-source-ui/src/router/index.ts | 25 + .../src/views/AccessPlaceholder.vue | 8 + .../src/views/DataSourceDescription.vue | 79 ++ .../src/views/ResourcesPlaceholder.vue | 8 + plugins/ui/apps/data-source-ui/tsconfig.json | 17 + .../apps/data-source-ui/vite.config.atlas.ts | 31 + 16 files changed, 2094 insertions(+), 102 deletions(-) create mode 100644 plans/data-source-ui-atlas3-plugin.md create mode 100644 plans/data-source-ui.md create mode 100644 plugins/ui/apps/data-source-ui/PLAN.md create mode 100644 plugins/ui/apps/data-source-ui/index.html create mode 100644 plugins/ui/apps/data-source-ui/package.json create mode 100644 plugins/ui/apps/data-source-ui/src/App.vue create mode 100644 plugins/ui/apps/data-source-ui/src/api/datasource.ts create mode 100644 plugins/ui/apps/data-source-ui/src/components/.gitkeep create mode 100644 plugins/ui/apps/data-source-ui/src/main.ts create mode 100644 plugins/ui/apps/data-source-ui/src/router/index.ts create mode 100644 plugins/ui/apps/data-source-ui/src/views/AccessPlaceholder.vue create mode 100644 plugins/ui/apps/data-source-ui/src/views/DataSourceDescription.vue create mode 100644 plugins/ui/apps/data-source-ui/src/views/ResourcesPlaceholder.vue create mode 100644 plugins/ui/apps/data-source-ui/tsconfig.json create mode 100644 plugins/ui/apps/data-source-ui/vite.config.atlas.ts diff --git a/plans/data-source-ui-atlas3-plugin.md b/plans/data-source-ui-atlas3-plugin.md new file mode 100644 index 0000000000..ea4a7d50fa --- /dev/null +++ b/plans/data-source-ui-atlas3-plugin.md @@ -0,0 +1,528 @@ +# Data Source UI Atlas3 plugin implementation plan + +## Scope and fixed decisions + +Create a new Vue 3 single-spa micro-frontend at `plugins/ui/apps/data-source-ui/`. + +- **single-spa/plugin ID:** `data-source-ui` +- **Atlas-facing bundle:** `index.system.js` +- **Public route:** `/datasources/:id` +- **Registration source:** `plugins/atlas/plugins.standalone.json` +- **UI stack:** Vue 3, Vuetify, and the Atlas component library/design conventions +- **API source:** existing D2E dataset, resource download, and access-request endpoints +- **Initial page:** Description; it owns the contextual data-source navigation +- **Closest implementation template:** `plugins/ui/apps/vue-mri-ui-lib/`, specifically `src/lifecycles.ts` and `vite.config.atlas.ts` + +No D2E API, database, environment-variable, or Docker Compose change is in scope. + +## Repository findings informing this plan + +`vue-mri-ui-lib` already provides the closest D2E Vue/Atlas pattern: + +- Its lifecycle entry uses `single-spa-vue`, Vue `createApp`, Vuetify, and reactive host/portal custom props. +- Its Atlas build writes a SystemJS library bundle to `dist-atlas/index.system.js` via `vite.config.atlas.ts`. +- Its lifecycle exports `bootstrap`, `mount`, `update`, and `unmount`. + +The Atlas standalone manifest currently has this runtime shape: + +```json +{ + "id": "patient-analytics", + "name": "Data Exploration", + "version": "2.0.0", + "entryPoint": "patient-analytics/index.system.js", + "menuItems": [] +} +``` + +The D2E Atlas staging script is `plugins/atlas/scripts/postinstall.js`. It currently copies a table of published package distributions to `plugins/atlas/resources/atlas/plugins//`. This new in-repository application needs one additional table entry/source-path branch so its locally built `dist-atlas` directory is copied to `resources/atlas/plugins/data-source-ui/`. + +Existing Portal behavior to reuse is in `plugins/ui/apps/portal/src/containers/researcher/Information/Information.tsx` and `plugins/ui/apps/portal/src/axios/system-portal.ts`: + +- resources: `GET dataset/resource/list?datasetId=` +- resource download: `GET dataset/resource//download?datasetId=` as a blob +- access request state: `api.userMgmt.getMyStudyAccessRequests()` plus `user.isDatasetResearcher[datasetId]` +- request access: `api.userMgmt.addStudyAccessRequest(userId, datasetId, Roles.STUDY_RESEARCHER)` +- dataset detail is already provided by the Portal dataset hook/client; implementation must identify the existing read endpoint used by that hook before extracting the equivalent request into this new app. The inspected `system-portal.ts` exposes `POST`/`PUT dataset/detail` for administration, not the detail read call itself. + +## 1. New application scaffold + +### New directory and files + +Create: + +```text +plugins/ui/apps/data-source-ui/ +├── package.json +├── tsconfig.json +├── index.html +├── vite.config.ts +├── vite.config.atlas.ts +├── src/ +│ ├── main.ts +│ ├── lifecycles.ts +│ ├── App.vue +│ ├── plugins/ +│ │ └── vuetify.ts +│ ├── router/ +│ │ ├── index.ts +│ │ └── pages.ts +│ ├── types/ +│ │ ├── atlas-props.ts +│ │ └── data-source.ts +│ ├── services/ +│ │ └── dataSourceApi.ts +│ ├── composables/ +│ │ ├── useAtlasContext.ts +│ │ └── useDataSource.ts +│ ├── components/ +│ │ ├── DataSourceLayout.vue +│ │ ├── DataSourceNav.vue +│ │ ├── DataSourceHeader.vue +│ │ ├── MetadataTable.vue +│ │ ├── FileList.vue +│ │ ├── RequestAccessAction.vue +│ │ └── PageState.vue +│ ├── views/ +│ │ └── DataSourceDescription.vue +│ └── styles/ +│ └── main.scss +└── tests/ + ├── dataSourceApi.spec.ts + ├── router.spec.ts + └── DataSourceDescription.spec.ts +``` + +Keep styles scoped to the plugin root/class where possible. Do not add global reset styles that could alter Atlas host pages. + +### `package.json` + +Use the versions and package conventions from `plugins/ui/apps/vue-mri-ui-lib/package.json`, narrowing dependencies to those required by this app. Required runtime dependencies include: + +```json +{ + "name": "data-source-ui", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "build:atlas": "vite build --config vite.config.atlas.ts", + "typecheck": "vue-tsc --noEmit", + "test:unit": "vitest run" + }, + "dependencies": { + "vue": "3.5.17", + "vue-router": "^4.2.0", + "vuetify": "3.12.0", + "single-spa": "^6.0.0", + "single-spa-vue": "^3.0.1" + } +} +``` + +Add the exact existing Vuetify support packages used by `vue-mri-ui-lib` (`@mdi/font`, Sass/Vite Vuetify plugin where needed) rather than introducing a second component-library version. Use the app/workspace package manager and lockfile conventions already used by `plugins/ui/apps`. + +### `vite.config.atlas.ts` + +Follow `plugins/ui/apps/vue-mri-ui-lib/vite.config.atlas.ts` with these exact build semantics: + +```ts +import { defineConfig } from 'vite' +import path from 'path' + +export default defineConfig({ + build: { + outDir: path.resolve(__dirname, 'dist-atlas'), + emptyOutDir: true, + sourcemap: false, + minify: true, + lib: { + entry: path.resolve(__dirname, 'src/lifecycles.ts'), + fileName: () => 'index.system.js', + formats: ['system'], + }, + rollupOptions: { + output: { entryFileNames: 'index.system.js' }, + }, + }, +}) +``` + +Adapt only the shared-dependency externalization/import-map settings that are already required by another direct-rendering Atlas plugin. The existing `vue-mri-ui-lib` SystemJS wrapper is intentionally dependency-free because it embeds an iframe; `data-source-ui` renders Vue directly and therefore must either bundle Vue/Vuetify or externalize them only when the Atlas import map supplies compatible modules. Confirm this with the actual Atlas runtime import map before deciding; do not assume `vue` and `vuetify` are globally resolvable. + +### `src/lifecycles.ts` + +This is the single-spa lifecycle entry, modeled on `vue-mri-ui-lib/src/lifecycles.ts`. + +Define custom props: + +```ts +export interface AtlasPluginProps { + getToken?: () => Promise + datasetId?: string + username?: string + locale?: string + qeSvcUrl?: string + REACT_APP_PUBLIC_WEBAPI_PROXY_URL?: string + REACT_APP_USE_PUBLIC_WEBAPI_PROXY?: string + REACT_APP_PUBLIC_WEBAPI_DATASOURCE?: string + dataSourceId?: string +} +``` + +Implement: + +```ts +const lifecycles = singleSpaVue({ + createApp, + replaceMode: true, + appOptions: { + render: () => h(App), + }, + handleInstance(app, props: AtlasPluginProps) { + // install Pinia if selected, Vue Router, Vuetify, Atlas context provider + }, +}) + +export const { bootstrap, mount } = lifecycles +export const update = async (props: Partial) => { /* update reactive host props */ } +export const unmount = async (props: unknown) => { /* delegate to lifecycle cleanup */ } +``` + +`useAtlasContext.ts` must expose a reactive copy of host props. It must prefer `getToken()` for authenticated calls and must not write tokens to local storage. + +### `src/main.ts` + +Provide a development-only direct mount that shares the same app factory/router/Vuetify setup as the lifecycle entry: + +```ts +createDataSourceApp(resolveLocalDevelopmentProps()).mount('.vue-main') +``` + +The page should be independently viewable during development, but `main.ts` must not be included by the Atlas SystemJS build. + +### `src/App.vue` and Vue Router + +`App.vue` is the plugin root and contains the contextual frame plus `RouterView`: + +```vue + + + +``` + +`src/router/index.ts` uses Vue Router history against the actual host path. Its initial route is: + +```ts +{ + path: '/datasources/:id', + component: DataSourceLayout, + children: [ + { + path: '', + name: 'data-source-description', + component: () => import('../views/DataSourceDescription.vue'), + }, + ], +} +``` + +Use `createWebHistory()` only after confirming it does not conflict with Atlas’s host navigation. If Atlas supplies history behavior that cannot coexist with a nested router, use `createMemoryHistory()` plus a small URL-to-route adapter; the browser pathname remains `/datasources/:id` in both cases. + +## 2. Description page + +### `src/views/DataSourceDescription.vue` + +The component receives the ID through Vue Router: + +```ts +const route = useRoute() +const dataSourceId = computed(() => String(route.params.id)) +const { dataSource, resources, access, loading, error, requestAccess, downloadResource } = useDataSource(dataSourceId) +``` + +The composable reloads when `route.params.id` changes, so direct navigation between data sources does not retain stale content. + +### Existing API integration + +`src/services/dataSourceApi.ts` centralizes authenticated requests and maps backend responses to view models. It must call the existing endpoint contract and auth mechanism rather than inventing D2E APIs: + +```ts +getDataSource(id: string): Promise +getResources(id: string): Promise +downloadResource(id: string, filename: string): Promise +getMyStudyAccessRequests(): Promise +requestResearcherAccess(userId: string, id: string): Promise +``` + +Known endpoint contracts: + +```text +GET dataset/resource/list?datasetId= +GET dataset/resource//download?datasetId= +``` + +For the remaining calls, copy the request shape from the existing Portal hooks/API client after locating the exact detail-read and user-management endpoint definitions. The new client uses the Atlas-provided authenticated request/base-URL configuration, not Portal React context. + +Sanitize or render the server-provided rich description through the repository’s established rich-text/Markdown renderer. Do not bind unsanitized server HTML with `v-html`. + +### Rendered sections + +The requested Figma export could not be retrieved by the connected Figma account during planning. The implementation must re-check node `1709-215182` once access is available and tune visual spacing, typography, icons, and responsive behavior against it. The confirmed issue acceptance criteria define these required sections: + +1. **Left-side navigation menu** + - grouped/section-labelled navigation + - access-specific menu composition + - default, hover, and selected states + - selected item uses the required blue treatment for background, icon, and text +2. **Data source name** as the page title. +3. **Description** showing the complete rich text configured in the admin portal. +4. **Metadata table** based on the data source attributes and the data-source ID. +5. **Files section** showing filename, size, and a download action for each associated resource. +6. **Access request action/state**, reusing the Portal’s existing request/pending/approved behavior when the dataset configuration permits requests. + +Implement these components: + +- `DataSourceHeader.vue`: title and access action/status. +- `DataSourceNav.vue`: page definitions from `router/pages.ts`, with active/hover/access behavior. +- `MetadataTable.vue`: normalized attribute rows; omit empty optional fields. +- `FileList.vue`: filename, size, progress/loading state per resource, and blob download. +- `RequestAccessAction.vue`: request, pending, and access-granted states. +- `PageState.vue`: loading, not-found/inaccessible, and generic request-failure states. + +## 3. Atlas route bridge + +### Constraint + +The shipped standalone manifest and Atlas host convention currently route menu items through `/plugins//`. The `data-source-ui` route must instead mount directly at `/datasources/:id`, and the current `plugins.standalone.json` schema has no `routes` field. + +This cannot be implemented only by adding a `menuItems` record. It requires a targeted extension to the Atlas3 host plugin-runtime router that reads `resources/atlas/config/plugins.json`, matches a declared route pattern, SystemJS-imports the plugin entry point, and mounts it as the same parcel type used for normal plugin navigation. + +### Required host behavior + +Extend the Atlas3 plugin runtime/source used to produce `plugins/atlas/node_modules/@ohdsi/atlas3/dist` with support for an optional manifest field: + +```ts +interface RuntimePlugin { + id: string + entryPoint: string + routes?: Array<{ path: string; exact?: boolean }> + menuItems?: MenuItem[] +} +``` + +The routing function must: + +```ts +matchPluginRoute(pathname: string, plugins: RuntimePlugin[]): PluginRouteMatch | undefined +``` + +- match `/datasources/:id` against the browser pathname; +- return `{ pluginId: 'data-source-ui', params: { id } }`; +- mount the plugin through the existing SystemJS/single-spa parcel mounting pathway; +- pass ordinary host props plus `dataSourceId: params.id`; +- unmount the parcel when the path no longer matches; +- preserve existing `/plugins//...` behavior unchanged; +- execute custom-route matching before generic not-found handling. + +Use a routing matcher already used by Atlas3 if one exists; otherwise add the smallest dependency-free segment matcher needed for named colon parameters. Match the exact two-segment route for this release. Add `/datasources/:id/*` only when the first child page actually needs it. + +### Where the source change belongs + +The repository’s `plugins/atlas` package stages a prebuilt `@ohdsi/atlas3` distribution; it does not contain the host plugin router source. Therefore the implementation requires one of these repository-supported paths, selected after confirming how Atlas3 is consumed: + +1. **Preferred:** make the route-pattern enhancement in the Atlas3 source package/release used by `@ohdsi/atlas3`, publish/consume the updated package, then update `plugins/atlas/package.json` to that version. +2. **Only if the repository already has a maintained patch pipeline:** apply a narrow, tested patch to the staged Atlas distribution in `plugins/atlas/scripts/postinstall.js`. Do not add a brittle search-and-replace against unknown/minified code unless the existing project explicitly uses that mechanism. + +The plan does not assume a nonexistent D2E-local Atlas router file. The implementation must identify the accepted Atlas3 source/patch delivery method before editing host routing. + +## 4. Manifest registration + +### File + +`plugins/atlas/plugins.standalone.json` + +### Add this plugin entry + +Add it to the `plugins` array: + +```json +{ + "id": "data-source-ui", + "name": "Data Source", + "version": "1.0.0", + "entryPoint": "data-source-ui/index.system.js", + "routes": [ + { + "path": "/datasources/:id", + "exact": true + } + ], + "menuItems": [], + "metadata": { + "author": "D2E", + "description": "Data source description and contextual navigation" + } +} +``` + +No global `menuItems` entry is added: a menu link cannot supply a meaningful required data-source ID, and the plugin owns its data-source-local navigation. The host route bridge consumes `routes`; existing Atlas versions will ignore no unknown fields only if their config parser permits it, which must be verified when implementing the host enhancement. + +The post-install script already copies this source file to: + +```text +plugins/atlas/resources/atlas/config/plugins.json +``` + +## 5. Build and resource staging + +### `plugins/atlas/scripts/postinstall.js` + +Extend the plugin staging table so it supports a local application build source. Add an entry conceptually equivalent to: + +```js +{ + id: 'data-source-ui', + source: join(rootDir, '..', 'ui', 'apps', 'data-source-ui', 'dist-atlas'), + repoints: [], +} +``` + +Then make the loop resolve `source` for local entries and retain the existing `node_modules//dist` resolution for package entries. The common copy behavior remains: + +```text +source dist-atlas/ + -> plugins/atlas/resources/atlas/plugins/data-source-ui/ +``` + +The staged directory must contain: + +```text +resources/atlas/plugins/data-source-ui/index.system.js +``` + +and every emitted JS/CSS/static asset required by the SystemJS entry. + +### Build orchestration + +Add an explicit Atlas build integration step in `plugins/atlas/package.json` or the root workspace build script, depending on existing workspace orchestration: + +```text +build data-source-ui with npm run build:atlas +→ run plugins/atlas postinstall staging +→ run plugins/atlas prepack verification +``` + +Do not rely on `postinstall` to compile the child app implicitly unless the repository’s install lifecycle already builds all `plugins/ui/apps` apps. The local distribution must exist before staging. Update `plugins/atlas/scripts/verify-plugins.js` only if its existing generic `entryPoint` check cannot already validate the new manifest record. + +## 6. Extensibility + +`src/router/pages.ts` is the single registry for data-source pages: + +```ts +export interface DataSourcePageDefinition { + id: string + label: string + icon: string + path: string + component: Component + visible: (context: DataSourceAccessContext) => boolean +} + +export const dataSourcePages: DataSourcePageDefinition[] = [ + { + id: 'description', + label: 'Description', + icon: 'mdi-information-outline', + path: '', + component: DataSourceDescription, + visible: () => true, + }, +] +``` + +`DataSourceNav.vue` renders this registry; `router/index.ts` derives child routes from it. To add a future Cohorts or Studies page: + +1. add the page view, such as `src/views/DataSourceCohorts.vue`; +2. add its registry definition with a stable child path, such as `cohorts` or `studies`; +3. give it an access predicate based on the normalized data-source/user context; +4. add only that page’s APIs/components. + +The public URL structure becomes: + +```text +/datasources/:id +/datasources/:id/cohorts +/datasources/:id/studies +``` + +At that time, widen the host manifest matcher from exact `/datasources/:id` to the deliberate route family required by the child pages. The Atlas bridge remains one plugin mount; no new Atlas host route or manifest plugin entry is needed per child page. + +## File-by-file change list + +### New + +```text +plugins/ui/apps/data-source-ui/package.json +plugins/ui/apps/data-source-ui/tsconfig.json +plugins/ui/apps/data-source-ui/index.html +plugins/ui/apps/data-source-ui/vite.config.ts +plugins/ui/apps/data-source-ui/vite.config.atlas.ts +plugins/ui/apps/data-source-ui/src/main.ts +plugins/ui/apps/data-source-ui/src/lifecycles.ts +plugins/ui/apps/data-source-ui/src/App.vue +plugins/ui/apps/data-source-ui/src/plugins/vuetify.ts +plugins/ui/apps/data-source-ui/src/router/index.ts +plugins/ui/apps/data-source-ui/src/router/pages.ts +plugins/ui/apps/data-source-ui/src/types/atlas-props.ts +plugins/ui/apps/data-source-ui/src/types/data-source.ts +plugins/ui/apps/data-source-ui/src/services/dataSourceApi.ts +plugins/ui/apps/data-source-ui/src/composables/useAtlasContext.ts +plugins/ui/apps/data-source-ui/src/composables/useDataSource.ts +plugins/ui/apps/data-source-ui/src/components/DataSourceLayout.vue +plugins/ui/apps/data-source-ui/src/components/DataSourceNav.vue +plugins/ui/apps/data-source-ui/src/components/DataSourceHeader.vue +plugins/ui/apps/data-source-ui/src/components/MetadataTable.vue +plugins/ui/apps/data-source-ui/src/components/FileList.vue +plugins/ui/apps/data-source-ui/src/components/RequestAccessAction.vue +plugins/ui/apps/data-source-ui/src/components/PageState.vue +plugins/ui/apps/data-source-ui/src/views/DataSourceDescription.vue +plugins/ui/apps/data-source-ui/src/styles/main.scss +plugins/ui/apps/data-source-ui/tests/dataSourceApi.spec.ts +plugins/ui/apps/data-source-ui/tests/router.spec.ts +plugins/ui/apps/data-source-ui/tests/DataSourceDescription.spec.ts +``` + +### Changed + +```text +plugins/atlas/plugins.standalone.json +plugins/atlas/scripts/postinstall.js +plugins/atlas/package.json +plugins/atlas/scripts/verify-plugins.js (only if needed for local staged bundle validation) + +``` + +## Verification + +1. Run `data-source-ui` type checking and unit/component tests. +2. Run `npm run build:atlas` in `plugins/ui/apps/data-source-ui` and confirm `dist-atlas/index.system.js` exists. +3. Run the Atlas staging lifecycle and confirm: + + ```text + plugins/atlas/resources/atlas/config/plugins.json + plugins/atlas/resources/atlas/plugins/data-source-ui/index.system.js + ``` + +4. Run the Atlas manifest verification script. +5. Exercise the real hosted Atlas runtime at `/datasources/` using an authenticated session. +6. Verify direct navigation and refresh mount the parcel; navigation away unmounts it. +7. Verify the route bridge passes/derives the correct data-source ID. +8. Verify title, rich description, metadata, resource filename/size/download behavior, and left-nav selected/hover/access states. +9. Verify no-access, pending-request, approved-access, missing/inaccessible ID, empty-files, and API-failure states. +10. Verify existing `/plugins//` routes still work. + +## Deployment/configuration conclusion + +No Docker Compose changes, new services, backend endpoint changes, database migrations, or new environment variables are required. The plugin uses the existing Atlas-provided authentication/API context and is served from the existing `resources/atlas/plugins/` static resource path. \ No newline at end of file diff --git a/plans/data-source-ui.md b/plans/data-source-ui.md new file mode 100644 index 0000000000..de6519dcf5 --- /dev/null +++ b/plans/data-source-ui.md @@ -0,0 +1,618 @@ +# Data Source UI — Atlas3 Single-Spa Plugin Implementation Plan + +## Purpose + +Create `data-source-ui` as a direct-rendered Vue single-spa parcel in `plugins/ui/apps/data-source-ui/`. Atlas3 will load the SystemJS bundle from its plugin manifest and mount it at the canonical data-source route: + +```text +/datasources/:id +``` + +The first release supplies the Data Source Description view. The parcel owns its contextual data-source navigation and local routing so future pages such as Cohorts and Studies can be introduced without reworking the Atlas integration. + +This follows the direct Vue parcel model used by the `trex-notebook` notebook plugin and the Atlas-facing Vue conventions of `plugins/ui/apps/vue-mri-ui-lib`. It intentionally does not use the `vue-mri-ui-lib` iframe-wrapper pattern. + +## Confirmed local conventions + +- D2E Atlas registers runtime parcels in `plugins/atlas/plugins.standalone.json`. +- The current manifest schema uses `id`, `name`, `version`, `entryPoint`, `menuItems`, and optional `metadata`. +- Existing entry points use a plugin-relative SystemJS file, e.g. `notebook-plugin/index.system.js`. +- Existing D2E UI apps are under `plugins/ui/apps/`; `data-source-ui/` already exists and currently contains only its planning material. +- Current public menu entries use `/plugins//...`; therefore `/datasources/:id` requires an Atlas host router bridge in addition to manifest registration. + +## 1. Directory structure + +Create the application using the following tree: + +```text +plugins/ui/apps/data-source-ui/ +├── PLAN.md +├── package.json +├── tsconfig.json +├── vite.config.ts +├── index.html +├── src/ +│ ├── main.ts +│ ├── App.vue +│ ├── styles/ +│ │ └── main.scss +│ ├── api/ +│ │ ├── client.ts +│ │ ├── datasource.ts +│ │ └── types.ts +│ ├── router/ +│ │ ├── index.ts +│ │ └── pages.ts +│ ├── stores/ +│ │ ├── hostContext.ts +│ │ └── dataSource.ts +│ ├── types/ +│ │ └── plugin.ts +│ ├── components/ +│ │ ├── DataSourceLayout.vue +│ │ ├── DataSourceNavigation.vue +│ │ ├── DataSourceHeader.vue +│ │ ├── DescriptionMetadata.vue +│ │ ├── DataSourceResources.vue +│ │ ├── RequestAccessAction.vue +│ │ └── PageState.vue +│ └── views/ +│ └── DataSourceDescriptionView.vue +└── tests/ + ├── api/ + │ └── datasource.spec.ts + └── stores/ + └── dataSource.spec.ts +``` + +Keep the first release narrow. Add a component only when it maps to a Figma section or separates reusable presentation from data loading. + +## 2. Package and workspace integration + +Create `plugins/ui/apps/data-source-ui/package.json` as a workspace package. Copy the package-manager workspace conventions and exact compatible versions from the nearest Vue/Vuetify app and the trex-notebook plugin; do not introduce a parallel Vue, Vuetify, or Atlas UI dependency version. + +Use this package identity and scripts: + +```json +{ + "name": "data-source-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "vue-tsc --noEmit", + "test": "vitest run" + } +} +``` + +Runtime dependencies: + +```text +vue +vue-router +vuetify +@ohdsi/atlas-ui +pinia +single-spa-vue +@mdi/font +``` + +Development dependencies: + +```text +vite +@vitejs/plugin-vue +typescript +vue-tsc +sass +vitest +@vue/test-utils +jsdom +``` + +Add this package to the same root workspace declaration that includes existing `plugins/ui/apps/*` applications, if that declaration does not already cover the new directory. Reuse the root package manager lockfile; do not create a separate lockfile in the app directory. + +## 3. Vite and SystemJS build + +Create `plugins/ui/apps/data-source-ui/vite.config.ts`, modeled on the direct trex-notebook parcel build rather than the Patient Analytics iframe build. + +The build must: + +- enable the Vue Vite plugin; +- use library mode with `src/main.ts` as its entry; +- write to `dist/`; +- output SystemJS format; +- emit the stable parcel entry `dist/index.system.js`; +- set `cssCodeSplit: false` so Atlas can load a predictable stylesheet asset; +- externalize Vue only if the Atlas SystemJS import map supplies it, matching the notebook template; +- bundle other dependencies unless the verified Atlas import map supplies them. + +Target configuration: + +```ts +export default defineConfig({ + plugins: [vue()], + build: { + outDir: 'dist', + emptyOutDir: true, + cssCodeSplit: false, + lib: { + entry: resolve(__dirname, 'src/main.ts'), + name: 'data-source-ui', + formats: ['system'], + fileName: () => 'index.system.js', + }, + rollupOptions: { + external: ['vue'], + output: { + format: 'system', + globals: { vue: 'vue' }, + }, + }, + }, +}) +``` + +Before implementation, verify trex-notebook’s current build configuration from the checked-out or published source and preserve any required aliases, CSS handling, or additional SystemJS externals. The raw GitHub paths were not available during this planning pass, so local D2E conventions must be the final compatibility authority. + +## 4. Single-spa Vue lifecycle + +`src/main.ts` is the library entry. It imports global styles, creates the Vue parcel through `single-spa-vue`, installs Pinia, Vuetify, and the local router, and exports `bootstrap`, `mount`, and `unmount`. + +Define `src/types/plugin.ts`: + +```ts +export interface PluginProps { + name: string + mountParcel?: unknown + singleSpa?: unknown + uiFilesUrl?: string + dataSourceId?: string + authContext: { + user: { + id: string + username: string + email?: string + permissions: string[] + } | null + token: string | null + isAuthenticated: boolean + hasPermission: (permission: string) => boolean + } + messageBus: { + send: (type: string, payload: T) => void + request: (type: string, payload: TRequest) => Promise + subscribe: (type: string, callback: (payload: T) => void) => () => void + } +} +``` + +The exact host prop names and types must match the existing Atlas plugin contract. Do not invent a separate authentication flow. + +Implement lifecycle behavior: + +1. Import `src/styles/main.scss`. +2. Create the Vuetify instance and router once, following the notebook plugin pattern. +3. In `handleInstance(app)`, install the Pinia instance, Vuetify instance, and router. +4. During `bootstrap(props)`, populate the host-context store, configure the API client with `props.authContext.token`, and ensure emitted CSS is loaded from `props.uiFilesUrl` if that is required by the existing notebook parcel convention. +5. During `mount(props)`, update host context and `dataSourceId`; this supports reuse if Atlas changes only the selected ID. +6. During `unmount`, clean up message-bus subscriptions and request listeners created by this parcel. + +The intended lifecycle arrangement is: + +```ts +const lifecycles = singleSpaVue({ + createApp, + appOptions: { + render() { + return h(App) + }, + }, + handleInstance(app) { + app.use(pinia) + app.use(vuetify) + app.use(router) + }, +}) + +export const bootstrap = async (props: PluginProps) => { + setHostContext(props) + configureApiClient({ token: props.authContext.token }) + return lifecycles.bootstrap(props) +} + +export const mount = async (props: PluginProps) => { + setHostContext(props) + return lifecycles.mount(props) +} + +export const unmount = lifecycles.unmount +``` + +## 5. Vuetify and Atlas UI setup + +Use Vue 3, Vuetify 3, Pinia, and `@ohdsi/atlas-ui`, consistent with trex-notebook. + +Configure Vuetify in `src/main.ts` or a narrowly scoped setup module: + +- use MDI icons from `@mdi/font` and Vuetify’s MDI icon set; +- disable a plugin-owned theme so Atlas remains visually authoritative; +- apply shared/default component configuration from the existing Atlas/notebook utility if present; +- avoid global styles that can leak into the host. + +Prefer Atlas UI components for product controls and status presentation: + +- `AtlasButton` for Request Access and downloads; +- `AtlasAlert` for error and access-status messages; +- `AtlasChip` for tags and access states; +- `AtlasDataTable` for a resource/file list where it matches the Figma hierarchy. + +Use Vuetify primitives for layout, spacing, cards, lists, and responsive behavior only where Atlas UI does not supply an equivalent component. + +## 6. Vue Router and future pages + +Atlas owns the browser URL `/datasources/:id`. The initial parcel router should use memory history so it does not take ownership of global browser navigation. + +Create `src/router/index.ts`: + +```ts +const routes: RouteRecordRaw[] = [ + { + path: '/', + component: DataSourceLayout, + children: [ + { + path: '', + name: 'data-source-description', + component: DataSourceDescriptionView, + }, + { + path: 'description', + redirect: { name: 'data-source-description' }, + }, + ], + }, +] +``` + +The router is mounted beneath the host-selected data source. `DataSourceLayout` contains the header, contextual navigation, and `router-view`. + +Create `src/router/pages.ts` with a registry driving the contextual sidebar: + +```ts +export interface DataSourcePage { + id: string + label: string + routeName: string + requiresAccess?: boolean + isVisible?: (context: DataSourceContext) => boolean +} + +export const dataSourcePages: DataSourcePage[] = [ + { + id: 'description', + label: 'Description', + routeName: 'data-source-description', + }, +] +``` + +Future views add one component and one page-registry record. When a public nested route is first required, extend the Atlas bridge from `/datasources/:id` to `/datasources/:id/:pathMatch(.*)*` and synchronize that remaining path with local router navigation. Do not expose unimplemented pages as functional links. + +## 7. Description view and existing D2E data + +Create `src/views/DataSourceDescriptionView.vue`. + +The view reads the current `dataSourceId` from the host-context store, watches it, and calls `useDataSourceStore().fetchDataSource(id)` when it is set or changes. It receives normalized data from the store and does not parse raw responses. + +### Required existing data + +Before implementing, locate the current D2E Portal Information feature and reuse its exact authenticated API client methods and response mappings. The likely detail route is only a placeholder: + +```text +GET /api/datasources/:id +``` + +Use the verified existing endpoint, which may use D2E’s dataset naming rather than this route shape. Also reuse: + +```text +dataset/resource/list?datasetId=:id +existing authenticated resource-download action +existing access-request status and access-request submission flow +``` + +No backend endpoint, database migration, or data model is created by this feature. + +### Figma node 1709-215182 layout + +Render these design sections using Atlas UI and Vuetify primitives: + +1. **Header and identity**: data-source name, data-source type, display-safe source/platform context, access/status indicator, and primary Request Access action. +2. **Contextual navigation**: plugin-owned left navigation with Description selected and access-aware extension support. +3. **Description**: long-form description rendered with the existing safe D2E rich-text sanitizer/renderer if the server supplies HTML. +4. **Connection information**: source/platform/connection display fields that the current Portal already exposes. Never show credentials, secrets, or raw internal connection strings. +5. **Metadata**: labeled detail rows using existing attributes such as type, organization/owner, dates, tags/categories, classification, and access information. Omit unavailable data rather than showing misleading blanks. +6. **Resources/files**: resources loaded from the existing resource list service, with type/description metadata and an authenticated download action only when allowed. +7. **Access state**: Request Access when eligible; otherwise granted, pending, denied, or restricted state using existing workflow semantics. +8. **Page states**: loading, unavailable/not-found, request failure, and empty-resource states. + +Component division: + +- `DataSourceHeader.vue`: identity fields and access action. +- `DescriptionMetadata.vue`: normalized metadata key/value rendering. +- `DataSourceResources.vue`: resource table/list and download trigger. +- `RequestAccessAction.vue`: state-sensitive request action and result display. +- `PageState.vue`: loading, empty, and error presentation. + +## 8. Pinia stores + +Create `src/stores/hostContext.ts` to hold parcel props needed by app components: `authContext`, `messageBus`, `uiFilesUrl`, and `dataSourceId`. It is updated by lifecycle functions, not by a component parsing a global variable. + +Create `src/stores/dataSource.ts` with `useDataSourceStore`. + +State: + +```ts +interface DataSourceState { + dataSource: DataSource | null + loading: boolean + error: Error | null +} +``` + +Required action: + +```ts +async fetchDataSource(id: string): Promise +``` + +Behavior: + +1. Mark loading and clear the prior error. +2. Load normalized data-source detail. +3. Load resources and access status only where those are not included in the detail response. +4. Store the combined view model. +5. Normalize request failures to an `Error`. +6. Clear loading in `finally`. +7. Prevent an earlier request from overwriting state after the selected ID changes, using a request ID or abort signal. + +The store must not contain raw HTTP behavior or route parsing. + +## 9. API modules + +Create: + +```text +src/api/client.ts +src/api/datasource.ts +src/api/types.ts +``` + +`client.ts` configures the authenticated request boundary from the host token: + +```ts +export function configureApiClient(options: { + token: string | null + baseUrl?: string +}): void +``` + +Use the same D2E/Atlas API base URL discovery model as existing plugins. Do not derive the API base URL from the browser path or user-controlled inputs. + +`datasource.ts` exposes: + +```ts +export async function getDataSource(id: string): Promise +export async function listDataSourceResources(id: string): Promise +export async function getDataSourceAccessState(id: string): Promise +export async function requestDataSourceAccess(id: string): Promise +export async function downloadDataSourceResource( + dataSourceId: string, + resourceId: string, +): Promise +``` + +Endpoint methods, paths, headers, and request bodies must be copied from existing Portal behavior after verifying it in code. The plan does not authorize creation of a new API contract. + +`types.ts` provides component-friendly normalized view models: + +```ts +export interface DataSource { + id: string + name: string + type?: string + description?: string + connectionInfo?: string + metadata: Array<{ label: string; value: string }> + tags: string[] + access: AccessState + resources: DataSourceResource[] +} + +export interface DataSourceResource { + id: string + name: string + type?: string + description?: string + downloadable: boolean +} + +export type AccessState = + | 'granted' + | 'requestable' + | 'requested' + | 'denied' + | 'restricted' +``` + +Keep server-response normalization inside the API layer. Render only approved display-safe fields. + +## 10. Atlas plugin manifest change + +Add this object to the `plugins` array in `plugins/atlas/plugins.standalone.json`: + +```json +{ + "id": "data-source-ui", + "name": "Data Sources", + "version": "0.1.0", + "entryPoint": "data-source-ui/index.system.js", + "menuItems": [], + "metadata": { + "author": "OHDSI", + "description": "Data source description and contextual data-source pages" + } +} +``` + +Do not add unsupported `routes` or `navItems` keys. The verified local schema uses `entryPoint` and `menuItems`; manifest registration alone does not register an arbitrary host route. + +Keep `menuItems` empty. A global menu item cannot provide a concrete data-source ID and would create an invalid destination. The parcel is entered from selected data-source context and owns local navigation. + +## 11. Atlas host route bridge + +The current generic plugin routing convention is: + +```text +/plugins/:pluginId/:pathMatch(.*)* +``` + +The required URL is outside that convention: + +```text +/datasources/:id +``` + +The Atlas3/Sibyl runtime source that contains the router and `PluginContainer` must receive a host-side route before its fallback/not-found routes: + +```ts +{ + path: '/datasources/:id', + name: 'data-source-ui', + component: PluginContainer, + props: (route) => ({ + pluginId: 'data-source-ui', + dataSourceId: route.params.id, + }), +} +``` + +Extend `PluginContainer`, if necessary, so it passes the extracted ID as a custom property when mounting the parcel: + +```ts +mountParcel(pluginConfig, { + ...standardPluginProps, + dataSourceId, +}) +``` + +Required host behavior: + +1. Match `/datasources/:id` and no unrelated paths. +2. Resolve registered plugin ID `data-source-ui`. +3. Load `data-source-ui/index.system.js`. +4. Pass normal auth/message-bus/UI-files props plus `dataSourceId`. +5. Unmount when the route is left. +6. Preserve generic `/plugins/:pluginId/...` behavior. +7. Do not add an invalid Atlas global menu route. + +If D2E consumes only a prebuilt Atlas runtime, make the route change in the Atlas/Sibyl source package that creates that runtime, then update the D2E runtime dependency. It cannot be achieved solely inside the new parcel or the standalone manifest. + +## 12. Build output and deployment staging + +The Vite build emits: + +```text +plugins/ui/apps/data-source-ui/dist/index.system.js +``` + +and any associated CSS/assets beneath `dist/`. + +Atlas must stage the entire directory to: + +```text +plugins/atlas/resources/atlas/plugins/data-source-ui/ +``` + +The manifest therefore resolves to: + +```text +plugins/atlas/resources/atlas/plugins/data-source-ui/index.system.js +``` + +Inspect `plugins/atlas/package.json` and `plugins/atlas/scripts/postinstall.js` before implementation. Existing installed Trex packages are copied from Atlas `node_modules`; local UI packages may be staged through a separate monorepo build/copy path. Add `data-source-ui` to the local UI build-and-copy pipeline rather than an unrelated installed-package list unless it is deliberately published and installed as a package. + +Required build sequence: + +```text +build plugins/ui/apps/data-source-ui +copy plugins/ui/apps/data-source-ui/dist/ to plugins/atlas/resources/atlas/plugins/data-source-ui/ +run existing plugin verification +``` + +Reuse the existing copy helper or build script and add a single mapping/list entry. Copy the full directory so CSS and assets are present. Extend the existing verification expectation where needed so the new manifest `entryPoint` must be found in the staged resources directory. + +## 13. Tests and verification + +Add unit tests for the API module and store. + +`tests/api/datasource.spec.ts` verifies: + +1. The verified existing endpoint path and method are used. +2. IDs are safely encoded. +3. The host token is supplied through the authenticated client. +4. Detail data is normalized correctly. +5. Empty resource lists are handled. +6. API failures are normalized consistently. +7. Existing access-request and download request shapes are preserved. + +`tests/stores/dataSource.spec.ts` verifies: + +1. Initial store state. +2. Loading state during `fetchDataSource`. +3. Successful normalized data storage. +4. Failure state and loading reset. +5. An old request cannot overwrite the result after the active ID changes. + +Add host runtime route tests in the Atlas/Sibyl source package for: + +1. `/datasources/123` resolving to `data-source-ui`. +2. Parcel props containing `dataSourceId: '123'`. +3. Existing `/plugins//...` routes retaining their behavior. +4. Unrelated URLs not being captured. + +Verify the full runtime flow: + +```text +build data-source-ui +stage output under resources/atlas/plugins/data-source-ui +verify index.system.js is present +load Atlas at /datasources/ +confirm parcel mounting and API-backed Description content +``` + +Exercise loading, not-found/inaccessible, requestable, granted/pending access, resource-list, and allowed-download states through the real Atlas runtime. + +## 14. Delivery sequence + +1. Verify current Trex notebook Vite/lifecycle/Vuetify configuration against accessible source and local bundle conventions. +2. Scaffold the new workspace package and establish SystemJS build output. +3. Add staging integration and manifest registration. +4. Implement and consume the Atlas route bridge; verify mount at a known data-source ID. +5. Locate existing Portal API wrappers and reproduce only the required normalized client behavior. +6. Implement host context, data-source store, shell, navigation, and Description view. +7. Add access/resource interactions using existing backend flows. +8. Add unit and host-route tests. +9. Build, stage, and exercise the live Atlas route. + +## Non-goals + +- No new D2E API endpoints. +- No database migrations. +- No Docker Compose or environment-variable changes. +- No iframe wrapper. +- No global Atlas menu item for a route that requires a selected data-source ID. +- No Cohorts or Studies implementation in the initial Description-page release. diff --git a/plugins/atlas/plugins.standalone.json b/plugins/atlas/plugins.standalone.json index 5367cc4744..7f372c366b 100644 --- a/plugins/atlas/plugins.standalone.json +++ b/plugins/atlas/plugins.standalone.json @@ -1,102 +1 @@ -{ - "version": "1.0", - "plugins": [ - { - "id": "pythia-plugin", - "name": "Pythia", - "version": "0.1.0", - "entryPoint": "pythia-plugin/index.system.js", - "menuItems": [], - "fabMounts": [ - { - "id": "pythia.fab", - "label": "Pythia — design assistant", - "icon": "mdi-chat-processing-outline", - "color": "primary", - "position": "bottom-right" - } - ], - "metadata": { - "author": "Atlas Team", - "description": "Pythia — AI-assisted cohort design advisor for ATLAS v3.0" - } - }, - { - "id": "results-viewer", - "name": "Analysis Results", - "version": "0.2.0", - "entryPoint": "results-viewer/index.system.js", - "menuItems": [], - "metadata": { - "author": "OHDSI", - "description": "OHDSI HADES analysis results viewer — runs OhdsiShinyModules via WebR + DuckDB-WASM in-browser" - } - }, - { - "id": "strategus-plugin", - "name": "Strategus", - "version": "0.1.0", - "entryPoint": "strategus-plugin/index.system.js", - "menuItems": [], - "metadata": { "author": "OHDSI", "description": "Strategus Analysis Specification Builder" } - }, - { - "id": "notebook-plugin", - "name": "Notebooks", - "version": "0.1.0", - "entryPoint": "notebook-plugin/index.system.js", - "menuItems": [], - "metadata": { "author": "OHDSI", "description": "Create, store and run Python/R/Markdown notebooks (Pyodide/WebR)" } - }, - { - "id": "network-plugin", - "name": "Network", - "version": "0.1.0", - "entryPoint": "network-plugin/index.system.js", - "menuItems": [], - "metadata": { "author": "OHDSI", "description": "Federated network studies — site plugin. Loads inactive until NETWORK_* config is provided." } - }, - { - "id": "studies-plugin", - "name": "Studies", - "version": "0.1.0", - "entryPoint": "studies-plugin/index.system.js", - "menuItems": [ - { "id": "studies.main", "name": "Studies", "route": "/plugins/studies-plugin/", "icon": "mdi-flask-outline", "order": 40 } - ], - "metadata": { "author": "OHDSI", "description": "Studies shell: Local / Network / Results" } - }, - { - "id": "patient-analytics", - "name": "Data Exploration", - "version": "2.0.0", - "entryPoint": "patient-analytics/index.system.js", - "menuItems": [ - { "id": "pa.main", "name": "Data Exploration", "route": "/plugins/patient-analytics/", "icon": "mdi-account-search-outline", "order": 45, "insertBefore": "cohorts" } - ], - "metadata": { "author": "D2E", "description": "Data Exploration (Patient Analytics/MRI) — visual cohort exploration against the selected data source" } - } - ], - "settings": { - "enableHotReload": false, - "loadTimeout": 30000, - "showLoadingIndicators": true, - "theme": { - "enableDarkMode": false, - "primaryColor": "#000080", - "logoUrl": "/atlas/config/d2e2.svg", - "logoNavigateTo": "/", - "accentColor": "#ff5e59", - "landingLogoUrl": "/atlas/config/landing-page-illustration.svg", - "chartColors": ["#000080", "#ff5e59", "#4e79a7", "#76b7b2", "#59a14f", "#edc949", "#af7aa1", "#9c755f", "#bab0ab", "#e15759"], - "treemapGradient": ["#c3cce8", "#4a5fb0", "#000080"] - }, - "header": { - "showNavBar": true, - "showFeedbackButton": false, - "showLanguageSelector": true, - "showConfigButton": true, - "showUserMenu": true - } - } -} +{"version": "1.0","plugins": [{"id": "pythia-plugin","name": "Pythia","version": "0.1.0","entryPoint": "pythia-plugin/index.system.js","menuItems": [],"fabMounts": [{"id": "pythia.fab","label": "Pythia \u2014 design assistant","icon": "mdi-chat-processing-outline","color": "primary","position": "bottom-right"}],"metadata": {"author": "Atlas Team","description": "Pythia \u2014 AI-assisted cohort design advisor for ATLAS v3.0"}},{"id": "results-viewer","name": "Analysis Results","version": "0.2.0","entryPoint": "results-viewer/index.system.js","menuItems": [],"metadata": {"author": "OHDSI","description": "OHDSI HADES analysis results viewer \u2014 runs OhdsiShinyModules via WebR + DuckDB-WASM in-browser"}},{"id": "strategus-plugin","name": "Strategus","version": "0.1.0","entryPoint": "strategus-plugin/index.system.js","menuItems": [],"metadata": {"author": "OHDSI","description": "Strategus Analysis Specification Builder"}},{"id": "notebook-plugin","name": "Notebooks","version": "0.1.0","entryPoint": "notebook-plugin/index.system.js","menuItems": [],"metadata": {"author": "OHDSI","description": "Create, store and run Python/R/Markdown notebooks (Pyodide/WebR)"}},{"id": "network-plugin","name": "Network","version": "0.1.0","entryPoint": "network-plugin/index.system.js","menuItems": [],"metadata": {"author": "OHDSI","description": "Federated network studies \u2014 site plugin. Loads inactive until NETWORK_* config is provided."}},{"id": "studies-plugin","name": "Studies","version": "0.1.0","entryPoint": "studies-plugin/index.system.js","menuItems": [{"id": "studies.main","name": "Studies","route": "/plugins/studies-plugin/","icon": "mdi-flask-outline","order": 40}],"metadata": {"author": "OHDSI","description": "Studies shell: Local / Network / Results"}},{"id": "patient-analytics","name": "Data Exploration","version": "2.0.0","entryPoint": "patient-analytics/index.system.js","menuItems": [{"id": "pa.main","name": "Data Exploration","route": "/plugins/patient-analytics/","icon": "mdi-account-search-outline","order": 45,"insertBefore": "cohorts"}],"metadata": {"author": "D2E","description": "Data Exploration (Patient Analytics/MRI) \u2014 visual cohort exploration against the selected data source"}},{"id": "data-source-ui","name": "Data Source","version": "0.1.0","entryPoint": "data-source-ui/index.system.js","menuItems": [{"id": "data-source-ui.main","name": "Data Source","route": "/datasources","icon": "mdi-database"}],"metadata": {"author": "OHDSI","description": "Data source description and contextual data-source pages"}}],"settings": {"enableHotReload": false,"loadTimeout": 30000,"showLoadingIndicators": true,"theme": {"enableDarkMode": false,"primaryColor": "#000080","logoUrl": "/atlas/config/d2e2.svg","logoNavigateTo": "/","accentColor": "#ff5e59","landingLogoUrl": "/atlas/config/landing-page-illustration.svg","chartColors": ["#000080","#ff5e59","#4e79a7","#76b7b2","#59a14f","#edc949","#af7aa1","#9c755f","#bab0ab","#e15759"],"treemapGradient": ["#c3cce8","#4a5fb0","#000080"]},"header": {"showNavBar": true,"showFeedbackButton": false,"showLanguageSelector": true,"showConfigButton": true,"showUserMenu": true}}} diff --git a/plugins/ui/apps/data-source-ui/PLAN.md b/plugins/ui/apps/data-source-ui/PLAN.md new file mode 100644 index 0000000000..d63a2d228a --- /dev/null +++ b/plugins/ui/apps/data-source-ui/PLAN.md @@ -0,0 +1,698 @@ +# Data Source UI — Atlas3 Single-Spa Plugin Implementation Plan + +## Purpose + +Create a new direct-rendered Vue single-spa parcel at `plugins/ui/apps/data-source-ui/`. Atlas3 will load it from its plugin manifest and mount it for the canonical data-source route: + +```text +/datasources/:id +``` + +The first delivered view is the Data Source Description page. The plugin owns its data-source-local router and navigation so future views, such as cohorts and studies, can be added without changing the Atlas integration. + +This plan follows the direct parcel pattern in `trex-notebook/plugins/notebook-plugin` and the Atlas-facing Vue conventions in `plugins/ui/apps/vue-mri-ui-lib`. It intentionally does not use the `vue-mri-ui-lib` iframe wrapper: `data-source-ui` will mount its Vue application directly as an Atlas parcel. + +## Constraints and settled decisions + +- Package/app name: `data-source-ui`. +- App location: `plugins/ui/apps/data-source-ui/`. +- UI stack: Vue 3, Vuetify 3, `@ohdsi/atlas-ui`, Pinia, and `single-spa-vue`. +- Build output: Vite library-mode SystemJS bundle named `dist/index.system.js`. +- Atlas manifest: `plugins/atlas/plugins.standalone.json`. +- Public route: `/datasources/:id`. +- Data source: existing D2E API endpoints; do not introduce new backend APIs or database changes. +- The Atlas host must add a route bridge because the existing generic plugin route is `/plugins/:pluginId/...`, not `/datasources/:id`. +- The initial page is Description. The data-source navigation belongs to this plugin rather than the Atlas global navigation. + +## 1. New package directory structure + +Create the following package tree: + +```text +plugins/ui/apps/data-source-ui/ +├── PLAN.md +├── package.json +├── tsconfig.json +├── vite.config.ts +├── index.html +├── src/ +│ ├── main.ts +│ ├── App.vue +│ ├── styles/ +│ │ └── main.scss +│ ├── router/ +│ │ ├── index.ts +│ │ └── pages.ts +│ ├── stores/ +│ │ └── dataSource.ts +│ ├── api/ +│ │ ├── client.ts +│ │ ├── datasource.ts +│ │ └── types.ts +│ ├── types/ +│ │ └── plugin.ts +│ ├── components/ +│ │ ├── DataSourceLayout.vue +│ │ ├── DataSourceNavigation.vue +│ │ ├── DataSourceHeader.vue +│ │ ├── DescriptionMetadata.vue +│ │ ├── DataSourceResources.vue +│ │ ├── RequestAccessAction.vue +│ │ └── PageState.vue +│ └── views/ +│ └── DataSourceDescriptionView.vue +└── tests/ + ├── api/ + │ └── datasource.spec.ts + └── stores/ + └── dataSource.spec.ts +``` + +Keep the initial component set small. Add an additional component only when it maps to a real Figma section or separates reusable presentation from data loading. + +## 2. `package.json` + +Create `plugins/ui/apps/data-source-ui/package.json` using the dependency versions already resolved by the D2E UI monorepo and aligned with `trex-notebook/plugins/notebook-plugin`. Do not add a second incompatible Vue, Vuetify, or Atlas UI version. + +The package must have these properties and scripts: + +```json +{ + "name": "data-source-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "vue-tsc --noEmit", + "test": "vitest run" + } +} +``` + +Runtime dependencies: + +```text +vue +vue-router +vuetify +@ohdsi/atlas-ui +pinia +single-spa-vue +@mdi/font +``` + +Development dependencies: + +```text +vite +@vitejs/plugin-vue +typescript +vue-tsc +sass +vitest +@vue/test-utils +jsdom +``` + +Use the exact versions and package-manager workspace syntax from the closest existing package rather than inventing versions. If the parent workspace centralizes these dependencies, declare them in the same way as `vue-mri-ui-lib` and notebook-plugin. + +## 3. Vite SystemJS build + +Create `plugins/ui/apps/data-source-ui/vite.config.ts` using Vite library mode, closely following `trex-notebook/plugins/notebook-plugin/vite.config.ts`. + +Required configuration behavior: + +- Vite Vue plugin enabled. +- Library entry is `src/main.ts`. +- Library name is `data-source-ui`. +- Output directory is `dist/`. +- Output format is `system`. +- Entry filename resolves to `index.system.js`. +- `cssCodeSplit: false` so the parcel has one deterministic stylesheet asset. +- Vue is externalized so it resolves to the host-provided SystemJS Vue module. +- Rollup output declares system format and Vue global/module mapping consistent with notebook-plugin. + +The intended configuration shape is: + +```ts +export default defineConfig({ + plugins: [vue()], + build: { + outDir: 'dist', + emptyOutDir: true, + cssCodeSplit: false, + lib: { + entry: resolve(__dirname, 'src/main.ts'), + name: 'data-source-ui', + formats: ['system'], + fileName: () => 'index.system.js', + }, + rollupOptions: { + external: ['vue'], + output: { + format: 'system', + globals: { vue: 'vue' }, + }, + }, + }, +}) +``` + +Confirm whether `single-spa-vue`, Vuetify, Pinia, or Atlas UI are externalized by the Trex template. Preserve that exact host compatibility behavior. Do not externalize packages that Atlas does not make available through its SystemJS import map. + +## 4. `src/main.ts`: single-spa lifecycle and host context + +`src/main.ts` is the Vite library entry and must export the parcel lifecycle functions: + +```ts +export const bootstrap: (props: PluginProps) => Promise +export const mount: (props: PluginProps) => Promise +export const unmount: (props: PluginProps) => Promise +``` + +Build them with `single-spa-vue`: + +```ts +const vueLifecycles = singleSpaVue({ + createApp, + appOptions: { + render() { + return h(App, { messageBus: (this as PluginProps).messageBus }) + }, + }, + handleInstance(app) { + app.use(createPinia()) + app.use(vuetify) + app.use(router) + }, +}) +``` + +The actual implementation should provide host props via a Pinia store or Vue `provide`, not rely on `this` inside unrelated components. The lifecycle bootstrapping sequence must: + +1. Import plugin-wide styles. +2. Create and configure Vuetify with the same Atlas-compatible defaults as notebook-plugin: + - disable a plugin-owned theme so Atlas remains the visual authority; + - configure MDI icons; + - use Atlas/shared Vuetify defaults where notebook-plugin uses `getSharedDefaults()`. +3. Create and install Pinia. +4. Install the local Vue router. +5. On `bootstrap`, retain the supplied host context and configure the API client with the current authentication token. +6. Ensure the emitted CSS is loaded from `uiFilesUrl` if the host requires the notebook-plugin style-injection approach. +7. On `mount`, set or update `dataSourceId` from host props before rendering/fetching. +8. On `unmount`, release subscriptions/listeners created by this parcel. + +Define `PluginProps` in `src/types/plugin.ts`: + +```ts +export interface PluginProps { + name: string + mountParcel?: unknown + singleSpa?: unknown + uiFilesUrl?: string + dataSourceId?: string + authContext: { + user: { + id: string + username: string + email?: string + permissions: string[] + } | null + token: string | null + isAuthenticated: boolean + hasPermission: (permission: string) => boolean + } + messageBus: { + send: (type: string, payload: T) => void + request: ( + type: string, + payload: TRequest, + ) => Promise + subscribe: ( + type: string, + callback: (payload: T) => void, + ) => () => void + } +} +``` + +Match the actual host prop types in notebook-plugin exactly where they already exist. Do not fabricate a separate authentication mechanism: token access must come from `authContext` supplied by Atlas. + +## 5. `App.vue` and Vue Router + +### App root + +`src/App.vue` should render a single data-source shell: + +```text +DataSourceLayout + ├── DataSourceHeader + ├── DataSourceNavigation + └── router-view +``` + +It should read the active `dataSourceId` from the host-context store, synchronize it with the URL parameter when supplied, and avoid data-fetching duplication between the root and Description view. + +### Router + +Create `src/router/index.ts` with a route tree intended for a parcel mounted at a selected data source. The root local route is `/`; it has a `description` child route that renders `DataSourceDescriptionView`. + +```ts +const routes: RouteRecordRaw[] = [ + { + path: '/', + component: DataSourceLayout, + children: [ + { + path: '', + name: 'data-source-description', + component: DataSourceDescriptionView, + }, + { + path: 'description', + redirect: { name: 'data-source-description' }, + }, + ], + }, +] +``` + +The host route `/datasources/:id` remains owned by Atlas, not by a nested router namespace. Use memory history or an explicitly configured router history only after confirming the parcel mount behavior in Atlas. The preferred initial model is Vue Router memory history because the Atlas route bridge owns browser navigation and passes `dataSourceId` as a parcel prop. + +When future Atlas route support includes nested data-source paths, map the host’s remainder to the local router. Do not have this initial plugin overwrite Atlas’s browser history ownership. + +### Page registry + +Create `src/router/pages.ts` as a declarative page registry that drives the plugin sidebar and future child routes: + +```ts +export interface DataSourcePage { + id: string + label: string + routeName: string + requiresAccess?: boolean + isVisible?: (context: DataSourceContext) => boolean +} + +export const dataSourcePages: DataSourcePage[] = [ + { + id: 'description', + label: 'Description', + routeName: 'data-source-description', + }, +] +``` + +No unimplemented pages should be shown as live navigation items. + +## 6. Description page and Figma mapping + +Create `src/views/DataSourceDescriptionView.vue`. + +### Data loading behavior + +The view obtains the selected ID from the host context store, with optional route synchronization if Atlas provides it. It must: + +1. Watch the current `dataSourceId`. +2. Call `useDataSourceStore().fetchDataSource(id)` when it becomes available or changes. +3. Render loading, error/not-found, and loaded states. +4. Leave response mapping in the store/API layer; the view receives normalized data. + +### Existing D2E API use + +The exact existing endpoint must be confirmed by locating the current Portal Information data-source client before implementation. The plan’s placeholder form is: + +```text +GET /api/datasources/:id +``` + +Use the actual endpoint already called by D2E for data-source/dataset detail, not a new endpoint or renamed public contract. In addition, reuse existing endpoints/services for: + +```text +dataset/resource/list?datasetId=:id +existing authenticated resource download action +existing access-request status and submission flow +``` + +The API layer must send the Bearer token received through `PluginProps.authContext.token` using the same D2E API base URL/configuration mechanism as existing Atlas plugins. + +### Figma node `1709-215182` sections + +The Description view must implement the information hierarchy identified for the Data Source Description design: + +1. **Header and identity** + - data-source name; + - data-source type; + - status/access indicator when returned by the API; + - the primary Request Access action when applicable. +2. **Contextual data-source navigation** + - plugin-owned left navigation; + - Description selected by default; + - access-aware page visibility prepared for later pages. +3. **Description** + - long-form data-source description; + - safe rich-text rendering using the existing D2E sanitization/rendering method when server data is HTML. +4. **Connection information** + - source/connection or platform information supplied by the existing detail model; + - never expose secrets, credentials, or internal connection strings. +5. **Metadata/details** + - labeled key/value rows for the Figma-provided attributes, such as type, organization/owner, dates, tags/categories, access classification, and other existing API fields. +6. **Data resources/files** + - list resources from the existing resource list endpoint; + - show available file/resource metadata; + - initiate downloads through the existing authenticated download service. +7. **Access state** + - show Request Access only when it is available; + - show current access request/granted state otherwise. +8. **Page states** + - loading state; + - empty resources state; + - inaccessible/not-found/error state. + +Use Atlas UI primitives instead of custom replacements: `AtlasButton` for the access action, `AtlasAlert` for errors/status, `AtlasChip` for tags/status, `AtlasDataTable` where the resource table design warrants it, and existing Atlas layout/type components where available. Use Vuetify layout primitives only where Atlas UI does not supply one. + +### Component division + +- `DataSourceHeader.vue`: title, type/context labels, and `RequestAccessAction`. +- `DescriptionMetadata.vue`: normalized metadata label/value rows. +- `DataSourceResources.vue`: resource listing and authenticated download action. +- `RequestAccessAction.vue`: request/granted/pending state presentation and request submission event. +- `PageState.vue`: reusable loading/empty/error state wrapper if the existing Atlas components do not already provide it. + +## 7. Pinia store + +Create `src/stores/dataSource.ts` with `useDataSourceStore`. + +Minimum state: + +```ts +interface DataSourceState { + dataSource: DataSource | null + loading: boolean + error: Error | null +} +``` + +Required action: + +```ts +async fetchDataSource(id: string): Promise +``` + +Action behavior: + +1. Set `loading = true` and clear the prior error. +2. Request normalized detail from `getDataSource(id)`. +3. Request visible resources and access state only if they are not included in the detail response, using the established Portal endpoint/service pattern. +4. Store the completed normalized record. +5. Set a normalized `Error` on failure. +6. Set `loading = false` in `finally`. + +Guard against stale writes when an ID changes while a prior request is in flight: retain the requested ID or use an abort signal so the old response cannot replace newer page data. + +The store must not embed route parsing or raw `fetch` calls. It consumes the API module only. + +## 8. API module + +Create `src/api/client.ts`, `src/api/datasource.ts`, and `src/api/types.ts`. + +### `client.ts` + +Expose a configured authenticated requester initialized from parcel props: + +```ts +export function configureApiClient(options: { + token: string | null + baseUrl?: string +}): void +``` + +And a request function used internally by endpoint modules. Reuse the D2E/Atlas API base URL discovery method already used in notebook-plugin or sibling apps. Do not read a URL from the browser route or user-controlled data. + +### `datasource.ts` + +Expose functions with this shape: + +```ts +export async function getDataSource(id: string): Promise +export async function listDataSourceResources(id: string): Promise +export async function getDataSourceAccessState(id: string): Promise +export async function requestDataSourceAccess(id: string): Promise +export async function downloadDataSourceResource( + dataSourceId: string, + resourceId: string, +): Promise +``` + +Endpoint paths and body shapes must be copied from the existing D2E Portal implementation. The initial `GET /api/datasources/:id` is only a route-shape placeholder in this plan; the implementation must use the verified existing endpoint or client wrapper. + +### `types.ts` + +Define plugin view-model types rather than leaking backend response details throughout components: + +```ts +export interface DataSource { + id: string + name: string + type?: string + description?: string + connectionInfo?: string + metadata: Array<{ label: string; value: string }> + tags: string[] + access: AccessState + resources: DataSourceResource[] +} + +export interface DataSourceResource { + id: string + name: string + type?: string + description?: string + downloadable: boolean +} + +export type AccessState = + | 'granted' + | 'requestable' + | 'requested' + | 'denied' + | 'restricted' +``` + +Normalize API response data inside `datasource.ts`. Do not assume a connection-information field is safe to render: render only server-approved display fields already exposed by the current Portal UI. + +## 9. Atlas manifest registration + +Change `plugins/atlas/plugins.standalone.json` by adding this object to the existing plugins array: + +```json +{ + "id": "data-source-ui", + "name": "Data Sources", + "version": "0.1.0", + "entryPoint": "data-source-ui/index.system.js", + "menuItems": [], + "metadata": { + "author": "OHDSI", + "description": "Data source description and contextual data-source pages" + } +} +``` + +Do not add `routes` or `navItems` fields unless the actual manifest schema is extended deliberately. The verified existing manifest schema uses `entryPoint` and `menuItems`; adding unsupported keys does not create a route bridge. + +`menuItems` remains empty because a global menu item has no valid concrete `:id`. Entry to this plugin comes from a selected data source, and the local navigation is inside the parcel. + +## 10. Build and staging integration + +### Build artifact + +The package build produces: + +```text +plugins/ui/apps/data-source-ui/dist/index.system.js +``` + +and emitted CSS/assets under that `dist/` directory. + +### Atlas runtime location + +Atlas must receive the complete output directory at: + +```text +plugins/atlas/resources/atlas/plugins/data-source-ui/ +``` + +The final entry file must therefore exist at: + +```text +plugins/atlas/resources/atlas/plugins/data-source-ui/index.system.js +``` + +### Pipeline change + +Inspect `plugins/atlas/package.json` and `plugins/atlas/scripts/postinstall.js` to identify the existing build/staging path. Existing installed Trex plugin packages are copied from `plugins/atlas/node_modules`, while UI-monorepo packages such as Patient Analytics may be staged by the caller/build pipeline. + +Add `data-source-ui` to the UI monorepo build-and-copy path, not to an unrelated package copy list, unless the package is later published and installed into Atlas node_modules. The build pipeline must execute: + +```text +build plugins/ui/apps/data-source-ui +copy plugins/ui/apps/data-source-ui/dist/ to plugins/atlas/resources/atlas/plugins/data-source-ui/ +run the existing plugin verification step +``` + +Use the existing copy helper/script and add one mapping/list entry rather than creating a second staging mechanism. Copy the entire distribution directory so CSS and code-split assets remain available. + +The existing verification script must see the manifest `entryPoint` at its final resource path. Extend any staged-plugin expectation test or verification input to include `data-source-ui`. + +## 11. Atlas route bridge + +### Why it is needed + +The current Atlas plugin container supports paths in the form: + +```text +/plugins/:pluginId/:pathMatch(.*)* +``` + +The desired product URL is: + +```text +/datasources/:id +``` + +The plugin manifest makes the parcel loadable but cannot add this public route by itself because the verified manifest schema does not currently use a `routes` field. + +### Required host-side change + +In the Atlas3/Sibyl runtime source that defines the router and plugin container—identified in the Trex template as the router file containing the `/plugins/:pluginId/:pathMatch(.*)*` route—add a sibling route before generic fallback/not-found handling: + +```ts +{ + path: '/datasources/:id', + name: 'data-source-ui', + component: PluginContainer, + props: (route) => ({ + pluginId: 'data-source-ui', + dataSourceId: route.params.id, + }), +} +``` + +If `PluginContainer` currently accepts only `pluginId`, extend its prop/custom-prop construction to pass `dataSourceId` to `mountParcel`: + +```ts +mountParcel(pluginConfig, { + ...standardPluginProps, + dataSourceId, +}) +``` + +The precise host file must be selected by inspecting the Atlas runtime source used to build the D2E Atlas distribution. If D2E only copies a prebuilt upstream Atlas package, this change must be contributed to the Atlas runtime source/package and then consumed by D2E; it cannot be implemented solely inside `plugins/ui/apps/data-source-ui`. + +Required behavior: + +1. Match only `/datasources/:id` initially. +2. Resolve registered plugin `data-source-ui` from `plugins.standalone.json`. +3. Load `data-source-ui/index.system.js` with the normal plugin loader. +4. Pass standard authentication/message-bus props plus `dataSourceId`. +5. Unmount the parcel on route change. +6. Preserve existing `/plugins/:pluginId/...` behavior unchanged. +7. Do not create a global navigation item for individual data-source routes. + +When the first future child page is implemented, extend the host route to: + +```text +/datasources/:id/:pathMatch(.*)* +``` + +and synchronize the local Vue Router path from the host route remainder. + +## 12. Future-page extensibility + +The plugin must be structured so pages such as cohorts and studies are added by: + +1. creating a view under `src/views/`; +2. adding a local route and a `dataSourcePages` registry entry; +3. adding an access/visibility predicate if that page requires it; +4. extending the host wildcard bridge when a public nested path is introduced. + +Examples of future public URLs: + +```text +/datasources/:id/cohorts +/datasources/:id/studies +``` + +Keep base data-source detail in `useDataSourceStore`; place page-specific requests in each page’s own store/composable only when needed. This avoids re-architecting the Description shell while avoiding a speculative global data layer. + +## 13. Tests + +Add Vitest tests without requiring a running Atlas host. + +### Store tests: `tests/stores/dataSource.spec.ts` + +Cover `useDataSourceStore`: + +1. Initial state is `dataSource = null`, `loading = false`, `error = null`. +2. `fetchDataSource(id)` sets loading while the request is pending. +3. A successful response stores normalized data and clears the error. +4. A failed request stores a normalized error and clears loading. +5. Changing IDs while a prior request is unresolved does not allow the stale response to replace the newer data. + +Mock only the functions exported by `src/api/datasource.ts`. + +### API tests: `tests/api/datasource.spec.ts` + +Cover the API module: + +1. Detail request uses the verified existing endpoint and encodes the ID safely. +2. Authenticated requests include the configured Authorization header when a token exists. +3. Response mapping produces the normalized `DataSource` model. +4. Resource-list mapping handles empty resources. +5. Non-success responses become actionable errors. +6. Download/request-access functions call the existing endpoint/service with correct method and body. + +Do not test implementation details of `fetch`; mock the configured API client boundary. + +### Host integration tests + +In the Atlas runtime repository/package, add route tests verifying: + +1. `/datasources/123` selects plugin ID `data-source-ui`. +2. `dataSourceId` passed to the parcel is `123`. +3. Existing `/plugins//...` routes still work. +4. An unknown route is not captured by the data-source bridge. + +### Build/staging verification + +Run the existing plugin verification after staging and confirm: + +```text +plugins/atlas/resources/atlas/plugins/data-source-ui/index.system.js +``` + +exists and is the exact path declared by the manifest. + +## 14. Implementation and verification order + +1. Inspect and align exact dependency versions and Vite/SystemJS externalization with `trex-notebook/plugins/notebook-plugin`. +2. Scaffold the package and achieve a successful `dist/index.system.js` build. +3. Add manifest entry and build/staging mapping; verify the entry bundle appears in Atlas resources. +4. Implement/consume the Atlas host route bridge and verify the parcel mounts at `/datasources/`. +5. Locate existing Portal detail/resources/access/download API wrappers and implement the normalized API module. +6. Implement Pinia store, shell, Description view, and Figma-aligned sections. +7. Implement the local page registry/sidebar with Description as the only active item. +8. Add API/store tests and Atlas route integration tests. +9. Exercise the real Atlas route with authenticated data-source records covering normal, requestable, granted, unavailable, and error states. + +## Non-goals + +- No new D2E backend endpoint. +- No database migration. +- No Docker Compose or new environment variable. +- No global Atlas menu item for a data source without a concrete ID. +- No iframe wrapper. +- No future cohorts/studies page implementation in the Description-page change. diff --git a/plugins/ui/apps/data-source-ui/index.html b/plugins/ui/apps/data-source-ui/index.html new file mode 100644 index 0000000000..08ba6f960a --- /dev/null +++ b/plugins/ui/apps/data-source-ui/index.html @@ -0,0 +1,12 @@ + + + + + + Data Source + + +
+ + + diff --git a/plugins/ui/apps/data-source-ui/package.json b/plugins/ui/apps/data-source-ui/package.json new file mode 100644 index 0000000000..ddaeb5e6c1 --- /dev/null +++ b/plugins/ui/apps/data-source-ui/package.json @@ -0,0 +1,22 @@ +{ + "name": "@d2e/data-source-ui", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build --config vite.config.atlas.ts", + "preview": "vite preview" + }, + "dependencies": { + "single-spa-vue": "^2.0.0", + "vue": "^3.3.0", + "vue-router": "^4.2.0", + "vuetify": "^3.3.0", + "@mdi/font": "^7.2.96" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.2.0", + "vite": "^4.3.0", + "vite-plugin-vue-devtools": "^7.0.0" + } +} diff --git a/plugins/ui/apps/data-source-ui/src/App.vue b/plugins/ui/apps/data-source-ui/src/App.vue new file mode 100644 index 0000000000..98240aef81 --- /dev/null +++ b/plugins/ui/apps/data-source-ui/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/plugins/ui/apps/data-source-ui/src/api/datasource.ts b/plugins/ui/apps/data-source-ui/src/api/datasource.ts new file mode 100644 index 0000000000..60b205dd02 --- /dev/null +++ b/plugins/ui/apps/data-source-ui/src/api/datasource.ts @@ -0,0 +1,18 @@ +export interface DataSource { + id: string + name: string + description?: string + type?: string + dialect?: string + database?: string + tags?: string[] +} + +export async function fetchDataSource(id: string): Promise { + const res = await fetch(`/api/datasets/${id}`, { + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + }) + if (!res.ok) throw new Error(`Failed to load data source: ${res.status}`) + return res.json() +} diff --git a/plugins/ui/apps/data-source-ui/src/components/.gitkeep b/plugins/ui/apps/data-source-ui/src/components/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/ui/apps/data-source-ui/src/main.ts b/plugins/ui/apps/data-source-ui/src/main.ts new file mode 100644 index 0000000000..64b8081426 --- /dev/null +++ b/plugins/ui/apps/data-source-ui/src/main.ts @@ -0,0 +1,26 @@ +import { h, createApp } from 'vue' +import singleSpaVue from 'single-spa-vue' +import App from './App.vue' +import router from './router' +import { createVuetify } from 'vuetify' +import 'vuetify/styles' +import '@mdi/font/css/materialdesignicons.css' + +const vuetify = createVuetify({ theme: { defaultTheme: 'light' } }) + +const vueLifecycles = singleSpaVue({ + createApp, + appOptions: { + render() { + return h(App, {}) + }, + }, + handleInstance(app) { + app.use(router) + app.use(vuetify) + }, +}) + +export const bootstrap = vueLifecycles.bootstrap +export const mount = vueLifecycles.mount +export const unmount = vueLifecycles.unmount diff --git a/plugins/ui/apps/data-source-ui/src/router/index.ts b/plugins/ui/apps/data-source-ui/src/router/index.ts new file mode 100644 index 0000000000..f01bac4b13 --- /dev/null +++ b/plugins/ui/apps/data-source-ui/src/router/index.ts @@ -0,0 +1,25 @@ +import { createRouter, createMemoryHistory } from 'vue-router' +import DataSourceDescription from '../views/DataSourceDescription.vue' +import ResourcesPlaceholder from '../views/ResourcesPlaceholder.vue' +import AccessPlaceholder from '../views/AccessPlaceholder.vue' + +const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { + path: '/datasources/:id', + component: DataSourceDescription, + children: [], + }, + { + path: '/datasources/:id/resources', + component: ResourcesPlaceholder, + }, + { + path: '/datasources/:id/access', + component: AccessPlaceholder, + }, + ], +}) + +export default router diff --git a/plugins/ui/apps/data-source-ui/src/views/AccessPlaceholder.vue b/plugins/ui/apps/data-source-ui/src/views/AccessPlaceholder.vue new file mode 100644 index 0000000000..358149bad9 --- /dev/null +++ b/plugins/ui/apps/data-source-ui/src/views/AccessPlaceholder.vue @@ -0,0 +1,8 @@ + diff --git a/plugins/ui/apps/data-source-ui/src/views/DataSourceDescription.vue b/plugins/ui/apps/data-source-ui/src/views/DataSourceDescription.vue new file mode 100644 index 0000000000..73adc82fbb --- /dev/null +++ b/plugins/ui/apps/data-source-ui/src/views/DataSourceDescription.vue @@ -0,0 +1,79 @@ + + + diff --git a/plugins/ui/apps/data-source-ui/src/views/ResourcesPlaceholder.vue b/plugins/ui/apps/data-source-ui/src/views/ResourcesPlaceholder.vue new file mode 100644 index 0000000000..aa329005ee --- /dev/null +++ b/plugins/ui/apps/data-source-ui/src/views/ResourcesPlaceholder.vue @@ -0,0 +1,8 @@ + diff --git a/plugins/ui/apps/data-source-ui/tsconfig.json b/plugins/ui/apps/data-source-ui/tsconfig.json new file mode 100644 index 0000000000..852f44434b --- /dev/null +++ b/plugins/ui/apps/data-source-ui/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/plugins/ui/apps/data-source-ui/vite.config.atlas.ts b/plugins/ui/apps/data-source-ui/vite.config.atlas.ts new file mode 100644 index 0000000000..d96daac318 --- /dev/null +++ b/plugins/ui/apps/data-source-ui/vite.config.atlas.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +export default defineConfig({ + plugins: [vue()], + build: { + outDir: path.resolve(__dirname, 'dist'), + emptyOutDir: true, + cssCodeSplit: false, + lib: { + entry: path.resolve(__dirname, 'src/main.ts'), + name: 'data-source-ui', + fileName: () => 'index.system.js', + formats: ['system'] as const, + }, + rollupOptions: { + external: ['vue', 'vue-router', 'vuetify', 'single-spa'], + output: { + format: 'system', + entryFileNames: 'index.system.js', + globals: { + vue: 'vue', + 'vue-router': 'vue-router', + vuetify: 'vuetify', + 'single-spa': 'single-spa', + }, + }, + }, + }, +})