pennsieve-dashboard 0.3.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,21 @@
7
7
 
8
8
  The **PennsieveDashboard** is a web application that provides a user interface for interacting with Pennsieve data, visualizing metrics, and managing dashboards relevant to scientific datasets.
9
9
 
10
+ ---
11
+
12
+ ## Breaking Changes
13
+
14
+ ### v1.0.0 — `s3Url` and `apiUrl` removed from `dashboard:globalVars`
15
+
16
+ The dashboard no longer provides `s3Url` or `apiUrl` as top-level properties in the injected `dashboard:globalVars` context. These values now live exclusively in `services` (e.g. `services.s3Url`, `services.ApiUrl`).
17
+
18
+ **Widget libraries must update `useGlobalVars.ts`** to remove `s3Url`/`apiUrl` from `GlobalVarsShape` and read them from `services` instead.
19
+
20
+ | PennsieveDashboard | SparcDashWidgets | PrecisionDashWidgets |
21
+ |--------------------|------------------|----------------------|
22
+ | >= 1.0.0 | >= 1.0.0 | >= 1.0.0 |
23
+
24
+ Pre-1.0 versions of these packages are not compatible with 1.0+. If you're building a custom widget library, use the `services` pattern described in [Building a Component Library](#3-building-a-component-library).
10
25
 
11
26
  ---
12
27
 
@@ -58,131 +73,7 @@ Before you begin, ensure you have the following installed:
58
73
  # or
59
74
  yarn install
60
75
  ```
61
- 3. App.vue
62
- For install and testing copy and paste this into your App.vue
63
-
64
- ```
65
- //App.vue
66
- <script setup>
67
- import { computed, ref } from 'vue';
68
- import {PennsieveDashboard, MarkdownWidget, TextWidget} from 'pennsieve-dashboard'
69
- import 'element-plus/dist/index.css';
70
- import 'pennsieve-dashboard/style.css'
71
-
72
- /*
73
- Computed
74
- */
75
- const publicationStatus = computed(() => {
76
- return "foobar"
77
- });
78
-
79
- const filesCount = computed(() => {
80
- return "b"
81
- });
82
-
83
- const collaboratorCounts = computed(()=>{
84
- return "a"
85
- })
86
-
87
-
88
- const availableWidgets = [
89
- //name must match componentKey
90
- { name: 'TextWidget', component: TextWidget },
91
- {name:'MarkdownWidget',component:MarkdownWidget}
92
- ]
93
- const defaultLayout = [
94
- {
95
- id: 'MarkdownWidget-6',
96
- x: 0, y: 0, w: 3, h: 9,
97
- componentKey: 'MarkdownWidget',
98
- componentName: 'Markdown Widget',
99
- component: MarkdownWidget,
100
- Props:{
101
- markdownText:[
102
- '# Human DRG Dataset Dashboard',
103
- '',
104
- 'This is a dashboard associated with the **NIH HEAL PRECISION Human Pain** consortium project. It aggregates data from several U19 centers in a standardized way. Using the different widgets, you can view, query and export the data in various ways:',
105
- '',
106
- '## Widgets',
107
- '',
108
- '### UMAP Viewer',
109
- 'This widget provides the UMAP representation of the entire dataset, you can select the color mapping based on different metadata elements.',
110
- '',
111
- '### The Data Explorer',
112
- 'Directly query over the data using SQL and export the results as a CSV file.',
113
- '',
114
- '### Proportion Viewer',
115
- 'Explore metrics between the different datasets that comprise the aggregated data.'
116
- ].join('\n')
117
- }
118
- },
119
- {
120
- id: "TextWidget-1",
121
- x: 0, y: 0, h: 1, w:4,
122
- componentName:"Text",
123
- componentKey:"TextWidget",
124
- component:TextWidget,
125
- hideHeader:true,
126
- Props:{displayText:"Dataset Overview",hideHeader:true}
127
- },
128
- {
129
- id: "TextWidget-2",
130
- x: 0, y: 1, h: 2, w:1,
131
- componentName:"Files",
132
- componentKey:"TextWidget",
133
- component:TextWidget,
134
- Props:{bindedKey:"FileCount"}
135
- },
136
- {
137
- id: "TextWidget-3",
138
- x: 1, y: 1, h: 2, w:2,
139
- componentName:"Status",
140
- componentKey:"TextWidget",
141
- component:TextWidget,
142
- Props:{bindedKey:"Status"}
143
- },
144
- {
145
- id: "TextWidget-4",
146
- x: 3, y: 1, h: 2, w:2,
147
- componentName:"Collaborator Counts",
148
- componentKey:"TextWidget",
149
- component:TextWidget,
150
- Props:{bindedKey:"CollaboratorCounts"}
151
- }
152
- ]
153
-
154
- const dashboardOptions = ref({
155
- globalData: {
156
- FileCount: filesCount.value,
157
- Status: publicationStatus.value,
158
- CollaboratorCounts: collaboratorCounts.value
159
- },
160
- availableWidgets,
161
- defaultLayout,
162
- })
163
-
164
- </script>
165
- <template>
166
- <PennsieveDashboard class="dashboard-app" :options="dashboardOptions"></PennsieveDashboard>
167
-
168
- </template>
169
-
170
- <style scoped>
171
- /* set style vars from outside the dashboard > customize to match your application */
172
- .dashboard-app{
173
- --el-color-primary: #243d8e;
174
- --el-color-primary-light-3: #fbfdff;
175
- --el-color-primary-dark-2: #546085;
176
- --color:#243d8e;
177
- --el-dialog-width: 90%;
178
- --dash-secondary: #243d8e;
179
- }
180
-
181
- </style>
182
-
183
- ```
184
-
185
- 4. Run
76
+ 3. Run
186
77
  ```
187
78
  npm run dev
188
79
  ```
@@ -251,16 +142,19 @@ Without these, the dashboard and widgets will not render correctly.
251
142
 
252
143
  #### 2. Available Widgets
253
144
  ```js
145
+ import { markRaw } from ‘vue’
146
+
254
147
  const availableWidgets = [
255
- { name: 'TextWidget', component: TextWidget },
256
- { name: 'MarkdownWidget', component: MarkdownWidget }
148
+ { name: TextWidget’, component: markRaw(TextWidget) },
149
+ { name: MarkdownWidget’, component: markRaw(MarkdownWidget) }
257
150
  ];
258
151
  ```
259
152
 
260
- - This array **registers which widgets the dashboard can use**.
261
- - Each widget entry must define:
262
- - `name` – must match the widget’s `componentKey` in the layout.
263
- - `component` – the imported Vue component.
153
+ - This array **registers which widgets the dashboard can use**.
154
+ - Each widget entry must define:
155
+ - `name` – must match the widget’s `componentKey` in the layout.
156
+ - `component` – the imported Vue component.
157
+ - **Wrap components with `markRaw()`** to prevent Vue reactivity warnings. This is needed because Vue will attempt to make component objects reactive when they’re stored in reactive state (like `ref()` or `reactive()`), which is unnecessary and triggers console warnings. `markRaw()` tells Vue to skip reactivity for these objects.
264
158
  - You can extend this list with custom widgets or external widget libraries.
265
159
 
266
160
  ---
@@ -308,6 +202,12 @@ const publicationStatus = computed(() => "foobar");
308
202
  const filesCount = computed(() => "b");
309
203
  const collaboratorCounts = computed(() => "a");
310
204
 
205
+ const services = {
206
+ s3Url: "https://your-bucket.s3.amazonaws.com/your-data",
207
+ ApiUrl: "https://api.pennsieve.net",
208
+ // Add any other configurables your widgets need (API keys, endpoints, etc.)
209
+ };
210
+
311
211
  const dashboardOptions = ref({
312
212
  globalData: {
313
213
  FileCount: filesCount.value,
@@ -316,12 +216,14 @@ const dashboardOptions = ref({
316
216
  },
317
217
  availableWidgets,
318
218
  defaultLayout,
219
+ services,
319
220
  });
320
221
  ```
321
222
 
322
- - **Computed values**: hold live data you want to expose inside the dashboard. These could come from an API call or another reactive source.
323
- - **globalData**: key/value pairs accessible to widgets. For example, a `TextWidget` can bind to `FileCount` or `Status`.
324
- - **dashboardOptions**: combines global data, available widgets, and layout into a single configuration object that powers the dashboard.
223
+ - **Computed values**: hold live data you want to expose inside the dashboard. These could come from an API call or another reactive source.
224
+ - **globalData**: key/value pairs accessible to widgets. For example, a `TextWidget` can bind to `FileCount` or `Status`.
225
+ - **services**: key/value map of configurables (data URLs, API endpoints, API keys) that the dashboard provides to all widgets via `useDashboardGlobalVars().services`. Widgets read values like `services.s3Url` or `services.ScicrunchApiKey` from this object.
226
+ - **dashboardOptions**: combines global data, available widgets, layout, and services into a single configuration object that powers the dashboard.
325
227
 
326
228
  ---
327
229
 
@@ -387,7 +289,6 @@ When no URL parameters are present, behavior is unchanged — the dashboard uses
387
289
 
388
290
  ---
389
291
 
390
- ## Contributing
391
292
  ## Contributing
392
293
 
393
294
  We welcome contributions from developers and the community. There are two main ways you can get involved:
@@ -397,52 +298,155 @@ If you encounter a bug, performance problem, or unexpected behavior in the dashb
397
298
  - Please report it through the official support channels.
398
299
  - Include as much detail as possible (steps to reproduce, screenshots, browser/OS version, etc.) so we can investigate and resolve quickly.
399
300
 
400
- ### 2. Building Custom Widget Libraries
401
- The dashboard is designed to be extensible. Developers can build their own widget libraries and plug them into the dashboard.
301
+ ### 2. Adding a Custom Component to an Existing Widget Library
402
302
 
403
- For reference, check out the [SPARC Dash Widgets Library](https://github.com/nih-sparc/SparcDashWidgets), which provides an example of how to build custom widgets. You can also see it in action at the [SPARC Dashboard Demo](https://staging.sparc.science/apps/sparc-dashboard).
303
+ Each widget library (e.g. SparcDashWidgets, PrecisionDashWidgets) follows the same component convention. Below is a real-world example the **BiolucidaViewer** from SparcDashWidgets showing how a component connects to the dashboard’s shared state via `useDashboardGlobalVars()`.
404
304
 
405
- #### Sample Component
406
- Here’s a minimal example of a custom widget component:
407
-
408
- ```
409
- //vue
305
+ ```vue
410
306
  <template>
411
- <!-- Child icons show up in the widget header -->
307
+ <!-- Scoped slot exposes the widget name and header icons to the dashboard shell -->
412
308
  <slot :widgetName="widgetName" :childIcons="childIcons"></slot>
413
309
 
414
- <div class="my-widget-name-wrap" v-bind="$attrs">
415
- <!-- Component body goes here -->
310
+ <div v-bind="$attrs" class="tw-flex tw-flex-col">
311
+ <div v-if="selectedImage" class="bv-metadata">
312
+ <div><b>Sex:</b> {{ selectedImage.sex }}</div>
313
+ <div><b>Age:</b> {{ selectedImage.ageRange }}</div>
314
+ <div><b>Sample Path:</b> {{ selectedImage.relativePath }}</div>
315
+ </div>
316
+ <div class="tw-h-screen tw-flex tw-justify-center">
317
+ <iframe class="tw-p-1 tw-w-screen" :src="biolucidaPath"></iframe>
318
+ </div>
416
319
  </div>
417
320
  </template>
418
321
 
419
- <script setup lang="ts">
420
- import { ref, watch, computed, shallowRef } from 'vue'
421
- import { Edit } from '@element-plus/icons-vue'
322
+ <script setup>
323
+ import { ref, computed, watch, shallowRef } from "vue";
324
+ import { useDashboardGlobalVars } from ‘../../useGlobalVars’
325
+ import { InfoFilled } from "@element-plus/icons-vue";
326
+
327
+ const widgetName = ref(‘MBF Image Viewer’);
328
+ const childIcons = shallowRef([
329
+ { comp: InfoFilled, tooltip: "Lock Biolucida Viewer" }
330
+ ]);
422
331
 
423
- defineOptions({ inheritAttrs: false })
332
+ const props = defineProps({
333
+ imageID: 0,
334
+ isLocked: { default: false, type: Boolean }
335
+ });
424
336
 
425
- // Props can be passed in from the defaultLayout in dashboard options or set programmatically.
426
- const props = defineProps<{
427
- property1?: string
428
- }>()
337
+ // Access the dashboard’s injected global vars (services, filters, etc.)
338
+ const GlobalVars = useDashboardGlobalVars();
429
339
 
430
- const emit = defineEmits<{
431
- (e: 'updateComponent', value: string): void
432
- }>()
340
+ // Read a filter value reactively
341
+ const selectedImage = computed(() => {
342
+ return GlobalVars?.filters?.SELECTED_IMAGE;
343
+ });
433
344
 
434
- // Provide values for your scoped slot so it doesn't error
435
- const widgetName = 'example widget'
436
- const childIcons = shallowRef([
437
- { comp: Edit, event: toggleMode, tooltip: 'click to open edit mode' }
438
- ])
345
+ const biolucidaPath = ref("");
439
346
 
440
- function toggleMode() {
441
- // Custom code for toggling edit mode
442
- }
347
+ watch(selectedImage, (newVal, oldVal) => {
348
+ if (newVal?.biolucidaID !== oldVal?.biolucidaID) {
349
+ biolucidaPath.value = `https://sparc.biolucida.net/image?c=${newVal?.biolucidaID}`;
350
+ }
351
+ }, { immediate: true });
443
352
  </script>
444
353
  ```
445
354
 
355
+ The key takeaway: call `useDashboardGlobalVars()` in your component to access the dashboard’s shared state — `services` (data URLs, API keys), `filters`, and filter-setting methods — without any extra wiring. See the [Building a Component Library](#3-building-a-component-library) section below for a full explanation of this composable.
356
+
357
+ ---
358
+
359
+ ### 3. Building a Component Library
360
+
361
+ If you’re building a **new widget library** (rather than adding to an existing one), you need to understand how the PennsieveDashboard communicates with widget components.
362
+
363
+ #### How the Dashboard Provides Context
364
+
365
+ When the `PennsieveDashboard` component mounts, it calls `provide("dashboard:globalVars", ...)` to inject a shared context object into the component tree. Any descendant component — including widgets from external libraries — can access this context using Vue’s `inject`.
366
+
367
+ #### The `useGlobalVars.ts` Composable
368
+
369
+ To access the injected context, your library should include a `useGlobalVars.ts` file (you can copy this directly from [SparcDashWidgets](https://github.com/nih-sparc/SparcDashWidgets)):
370
+
371
+ ```ts
372
+ // src/useGlobalVars.ts
373
+ import { inject } from ‘vue’
374
+
375
+ export const DASHBOARD_GLOBAL_VARS_KEY = ‘dashboard:globalVars’ as const
376
+ export type FilterValue = string | number | boolean | null | string[] | number[]
377
+ export type Filters = Record<string, FilterValue>
378
+ export type Services = Record<string, any>
379
+
380
+ export type GlobalVarsShape = {
381
+ services: Services | import(‘vue’).Ref<Services> // All configurables (URLs, API keys, etc.)
382
+ filters: Filters | import(‘vue’).Ref<Filters> // Shared reactive filter state
383
+ setFilter: (key: string, value: FilterValue, isDisplayed?: boolean) => void
384
+ clearFilter: (key: string) => void
385
+ resetFilters?: (next?: Filters) => void
386
+ }
387
+
388
+ export function useDashboardGlobalVars(required = false) {
389
+ const gv = inject<GlobalVarsShape | null>(DASHBOARD_GLOBAL_VARS_KEY, null)
390
+ if (!gv && required) {
391
+ console.warn(‘[Widget] dashboard:globalVars not provided.’)
392
+ }
393
+ return gv
394
+ }
395
+ ```
396
+
397
+ #### What Each Property Does
398
+
399
+ | Property | Description |
400
+ |----------|-------------|
401
+ | `services` | Key-value map of configurables passed in from the host application — data URLs, API endpoints, API keys, etc. For example: `services.s3Url`, `services.ApiUrl`, `services.ScicrunchApiKey`. |
402
+ | `filters` | Reactive key-value object representing the current dashboard filter state. Widgets read from this to stay in sync. |
403
+ | `setFilter(key, value, isDisplayed?)` | Set a filter value. `isDisplayed` controls whether the filter chip appears in the dashboard header. |
404
+ | `clearFilter(key)` | Remove a single filter by key. |
405
+ | `resetFilters(next?)` | Clear all filters, optionally replacing them with `next`. |
406
+
407
+ #### Using Filters for Cross-Widget Communication
408
+
409
+ Filters are the primary mechanism for widgets to communicate. One widget sets a filter, and other widgets react to the change:
410
+
411
+ ```ts
412
+ const GlobalVars = useDashboardGlobalVars();
413
+
414
+ // Read a service configurable (e.g. data URL, API key)
415
+ const dataUrl = unref(GlobalVars?.services)?.s3Url;
416
+ const apiKey = unref(GlobalVars?.services)?.ScicrunchApiKey;
417
+
418
+ // Set a filter (other widgets will see this reactively)
419
+ GlobalVars?.setFilter(‘SELECTED_GENE’, ‘CDH9’, true);
420
+
421
+ // Read a filter
422
+ const gene = GlobalVars?.filters?.SELECTED_GENE;
423
+
424
+ // Clear a filter
425
+ GlobalVars?.clearFilter(‘SELECTED_GENE’);
426
+
427
+ // Reset all filters
428
+ GlobalVars?.resetFilters();
429
+ ```
430
+
431
+ #### Library Structure
432
+
433
+ A typical widget library follows this structure:
434
+
435
+ ```
436
+ your-widget-library/
437
+ ├── src/
438
+ │ ├── components/ # Widget components (public API)
439
+ │ │ ├── MyWidget/
440
+ │ │ │ └── MyWidget.vue
441
+ │ │ └── index.js # Barrel export
442
+ │ ├── useGlobalVars.ts # Copy from SparcDashWidgets
443
+ │ └── main.ts # Dev entry point
444
+ ├── package.json
445
+ └── vite.config.ts # Build in library mode
446
+ ```
447
+
448
+ Your `vite.config.ts` should build in library mode with `vue`, `pinia`, and `element-plus` as externals. See SparcDashWidgets or PrecisionDashWidgets for working examples.
449
+
446
450
  ## Contact Support
447
451
 
448
452
  There are several ways to get in contact with our support team.