Skip to content

Commit 4116199

Browse files
committed
feat: enhance chart components with responsive sizing and improved accessibility
- Updated LineChart.vue to use dynamic sizing based on element size, improving responsiveness. - Adjusted padding and stroke widths for better visual clarity in LineChart. - Enhanced PieChart.vue with a new layout and improved legend display, including percentage labels. - Refactored StackedBarChart.vue to support dynamic sizing and improved label visibility. - Modified PivotTableWidget.vue and TableWidget.vue to ensure proper flex layout and overflow handling. - Updated widget registry to include new components and improved type definitions. - Enhanced API schemas to support JSON configurations instead of stringified YAML. - Improved dashboard configuration service to handle unknown types and publish updates via WebSocket.
1 parent 3553024 commit 4116199

29 files changed

Lines changed: 1058 additions & 341 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
2+
import type { Ref, ShallowRef } from 'vue'
3+
4+
type ElementSizeState<T extends HTMLElement> = {
5+
el: ShallowRef<T | null>
6+
width: Ref<number>
7+
height: Ref<number>
8+
}
9+
10+
export function useElementSize<T extends HTMLElement>(): ElementSizeState<T> {
11+
const el = shallowRef<T | null>(null)
12+
const width = ref(0)
13+
const height = ref(0)
14+
15+
let observer: ResizeObserver | undefined
16+
17+
onMounted(() => {
18+
observer = new ResizeObserver(([entry]) => {
19+
if (!entry) {
20+
return
21+
}
22+
23+
width.value = Math.floor(entry.contentRect.width)
24+
height.value = Math.floor(entry.contentRect.height)
25+
})
26+
27+
if (el.value) {
28+
observer.observe(el.value)
29+
}
30+
})
31+
32+
onBeforeUnmount(() => {
33+
observer?.disconnect()
34+
})
35+
36+
return {
37+
el: el as ShallowRef<T | null>,
38+
width,
39+
height,
40+
}
41+
}

custom/model/dashboard.types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,19 @@ export type DashboardWidgetSize = 'small' | 'medium' | 'large' | 'wide' | 'full'
2828

2929
export type WidgetLayout = {
3030
size?: DashboardWidgetSize
31+
width?: number
3132
minWidth?: number
3233
maxWidth?: number | null
34+
height?: number
3335
}
3436

3537
export type DashboardWidgetConfig = {
3638
id: string
3739
group_id: string
3840
label?: string
3941
size?: DashboardWidgetSize
42+
width?: number
43+
height?: number
4044
minWidth?: number
4145
maxWidth?: number | null
4246
order: number

custom/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "custom",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"dependencies": {
7+
"yaml": "^2.9.0"
8+
}
9+
}

custom/pnpm-lock.yaml

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

custom/queries/useUpdateDashboardConfig.ts

Whitespace-only changes.

custom/runtime/DashboardGroup.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<button
1818
type="button"
1919
class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
20-
title="Edit YAML"
20+
title="Edit JSON"
2121
@click="emit('edit-group', group)"
2222
>
2323
<svg
@@ -111,8 +111,10 @@
111111
:can-move-down="index < widgets.length - 1"
112112
:layout="{
113113
size: widget.size,
114+
width: widget.width,
114115
minWidth: widget.minWidth,
115116
maxWidth: widget.maxWidth,
117+
height: widget.height,
116118
}"
117119
@edit="emit('edit-widget', widget)"
118120
@move-up="emit('move-widget-up', widget.id)"

custom/runtime/DashboardPage.vue

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<template>
2+
<div class="min-h-full w-full">
3+
<div
4+
v-if="isLoading"
5+
class="p-6 text-sm text-lightListTableText dark:text-darkListTableText"
6+
>
7+
Loading dashboard...
8+
</div>
9+
10+
<div
11+
v-else-if="error"
12+
class="p-6 text-sm text-lightInputErrorColor"
13+
>
14+
<div>Failed to load dashboard</div>
15+
16+
<Button
17+
type="button"
18+
class="mt-3"
19+
@click="refetch"
20+
>
21+
Retry
22+
</Button>
23+
</div>
24+
25+
<DashboardRuntime
26+
v-else-if="dashboard"
27+
:dashboard-slug="dashboardSlug"
28+
:dashboard-id="dashboard.id"
29+
:label="dashboard.label"
30+
:config="dashboard.config"
31+
:revision="dashboard.revision"
32+
:is-admin="isAdmin"
33+
:is-refreshing="isFetching"
34+
/>
35+
36+
<div
37+
v-else
38+
class="dashboard-page__state"
39+
>
40+
Dashboard not found
41+
</div>
42+
</div>
43+
</template>
44+
45+
46+
47+
<script setup lang="ts">
48+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
49+
import { useRoute } from 'vue-router'
50+
import { Button } from '@/afcl'
51+
import { useCoreStore } from '@/stores/core'
52+
import websocket from '@/websocket'
53+
import DashboardRuntime from './DashboardRuntime.vue'
54+
import { useDashboardConfig } from '../queries/useDashboardConfig.js'
55+
56+
const route = useRoute()
57+
const coreStore = useCoreStore()
58+
59+
const dashboardSlug = computed(() => {
60+
const slug = route.params.slug
61+
62+
if (Array.isArray(slug)) {
63+
return slug[0] || 'default'
64+
}
65+
66+
return (slug as string) || 'default'
67+
})
68+
69+
const {
70+
data: dashboard,
71+
isLoading,
72+
isFetching,
73+
error,
74+
refetch,
75+
} = useDashboardConfig(dashboardSlug)
76+
77+
const isAdmin = computed(() => {
78+
return coreStore.adminUser?.dbUser.role === 'superadmin'
79+
})
80+
81+
const DASHBOARD_CONFIG_UPDATED_TOPIC_PREFIX = '/opentopic/dashboard-config-updated'
82+
const subscribedTopic = ref<string | null>(null)
83+
84+
const dashboardConfigUpdatedTopic = computed(() => {
85+
return `${DASHBOARD_CONFIG_UPDATED_TOPIC_PREFIX}/${dashboardSlug.value}`
86+
})
87+
88+
function handleDashboardConfigUpdated(data: { slug?: string; revision?: number }) {
89+
if (data.slug && data.slug !== dashboardSlug.value) {
90+
return
91+
}
92+
93+
if (typeof data.revision === 'number' && dashboard.value && data.revision <= dashboard.value.revision) {
94+
return
95+
}
96+
97+
void refetch()
98+
}
99+
100+
function subscribeToDashboardUpdates() {
101+
if (subscribedTopic.value) {
102+
websocket.unsubscribe(subscribedTopic.value)
103+
}
104+
105+
subscribedTopic.value = dashboardConfigUpdatedTopic.value
106+
websocket.subscribe(subscribedTopic.value, handleDashboardConfigUpdated)
107+
}
108+
109+
watch(dashboardConfigUpdatedTopic, subscribeToDashboardUpdates)
110+
111+
onMounted(() => {
112+
subscribeToDashboardUpdates()
113+
})
114+
115+
onUnmounted(() => {
116+
if (!subscribedTopic.value) {
117+
return
118+
}
119+
120+
websocket.unsubscribe(subscribedTopic.value)
121+
})
122+
</script>

custom/runtime/DashboardRuntime.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
<section class="w-full max-w-2xl rounded-lg border border-lightListBorder bg-lightDropdownOptionsBackground p-4 shadow-xl dark:border-darkListBorder dark:bg-darkDropdownOptionsBackground">
5858
<header class="mb-3 flex items-center justify-between gap-3">
5959
<h2 class="m-0 text-base font-bold text-lightNavbarText dark:text-darkNavbarText">
60-
Group YAML
60+
Group JSON
6161
</h2>
6262

6363
<button
@@ -123,7 +123,7 @@
123123
<section class="w-full max-w-2xl rounded-lg border border-lightListBorder bg-lightDropdownOptionsBackground p-4 shadow-xl dark:border-darkListBorder dark:bg-darkDropdownOptionsBackground">
124124
<header class="mb-3 flex items-center justify-between gap-3">
125125
<h2 class="m-0 text-base font-bold text-lightNavbarText dark:text-darkNavbarText">
126-
Widget YAML
126+
Widget JSON
127127
</h2>
128128

129129
<button

custom/runtime/WidgetRenderer.vue

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="flex flex-col gap-1">
2+
<div class="flex h-full min-h-0 flex-col gap-1">
33
<div
44
v-if="isAdmin"
55
class="text-xs font-bold uppercase tracking-normal text-lightListTableText dark:text-darkListTableText"
@@ -11,33 +11,10 @@
1111
{{ widgetTitle }}
1212
</div>
1313

14-
<TableWidget
15-
v-if="widget.target === 'table'"
16-
class="mt-3"
17-
:widget="widget"
18-
:dashboard-slug="dashboardSlug"
19-
/>
20-
21-
<ChartWidget
22-
v-if="widget.target === 'chart'"
23-
:widget="widget"
24-
:dashboard-slug="dashboardSlug"
25-
/>
26-
27-
<KpiCardWidget
28-
v-if="widget.target === 'kpi_card'"
29-
:widget="widget"
30-
:dashboard-slug="dashboardSlug"
31-
/>
32-
33-
<PivotTableWidget
34-
v-if="widget.target === 'pivot_table'"
35-
:widget="widget"
36-
:dashboard-slug="dashboardSlug"
37-
/>
38-
39-
<GaugeCardWidget
40-
v-if="widget.target === 'gauge_card'"
14+
<component
15+
:is="widgetComponent"
16+
v-if="widgetComponent"
17+
class="mt-3 min-h-0 flex-1 overflow-hidden"
4118
:widget="widget"
4219
:dashboard-slug="dashboardSlug"
4320
/>
@@ -48,19 +25,23 @@
4825

4926
<script setup lang="ts">
5027
import { computed } from 'vue'
51-
import ChartWidget from '../widgets/chart/ChartWidget.vue'
52-
import GaugeCardWidget from '../widgets/gauge-card/GaugeCardWidget.vue'
53-
import KpiCardWidget from '../widgets/kpi-card/KpiCardWidget.vue'
54-
import PivotTableWidget from '../widgets/pivot-table/PivotTableWidget.vue'
55-
import TableWidget from '../widgets/table/TableWidget.vue'
5628
import type { DashboardWidgetConfig } from '../model/dashboard.types.js'
29+
import { getWidgetLabel, getWidgetRegistration } from '../widgets/registry.js'
5730
5831
const props = defineProps<{
5932
widget: DashboardWidgetConfig
6033
dashboardSlug: string
6134
isAdmin: boolean
6235
}>()
6336
37+
const widgetRegistration = computed(() => {
38+
return getWidgetRegistration(props.widget.target)
39+
})
40+
41+
const widgetComponent = computed(() => {
42+
return widgetRegistration.value?.component
43+
})
44+
6445
const widgetTitle = computed(() => {
6546
if (props.widget.label) {
6647
return props.widget.label
@@ -74,6 +55,6 @@ const widgetTitle = computed(() => {
7455
return props.widget.chart?.title || 'Untitled chart'
7556
}
7657
77-
return props.widget.target.replaceAll('_', ' ')
58+
return getWidgetLabel(props.widget.target)
7859
})
7960
</script>

custom/runtime/WidgetShell.vue

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div
3-
class="group relative min-h-24 grow shrink basis-[var(--widget-basis)] rounded-lg bg-lightListTable p-3 min-w-[var(--widget-min-width)] max-w-[var(--widget-max-width)] dark:bg-darkListTable"
3+
class="group relative flex min-h-24 grow shrink basis-[var(--widget-basis)] flex-col overflow-hidden rounded-lg bg-lightListTable p-3 min-w-[var(--widget-min-width)] max-w-[var(--widget-max-width)] dark:bg-darkListTable"
44
:class="isAdmin ? 'border border-dashed border-lightListBorder dark:border-darkListBorder' : ''"
55
:style="widgetLayoutVars"
66
>
@@ -13,7 +13,7 @@
1313
<button
1414
type="button"
1515
class="flex h-8 w-8 items-center justify-center rounded-lg border border-lightListViewButtonBorder bg-lightListViewButtonBackground text-lightListViewButtonText shadow-sm hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover dark:border-darkListViewButtonBorder dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:hover:bg-darkListViewButtonBackgroundHover dark:hover:text-darkListViewButtonTextHover"
16-
title="Edit YAML"
16+
title="Edit JSON"
1717
@click="emit('edit')"
1818
>
1919
<svg
@@ -106,6 +106,8 @@ import { computed } from 'vue'
106106
import type { CSSProperties } from 'vue'
107107
import type { WidgetLayout } from '../model/dashboard.types.js'
108108
109+
const DEFAULT_WIDGET_HEIGHT = 500
110+
109111
const sizeToFlexBasis: Record<NonNullable<WidgetLayout['size']>, string> = {
110112
small: '260px',
111113
medium: '360px',
@@ -130,13 +132,15 @@ const emit = defineEmits<{
130132
131133
const widgetLayoutVars = computed<CSSProperties>(() => {
132134
const basis = sizeToFlexBasis[props.layout?.size ?? 'medium']
135+
const fixedWidth = formatWidth(props.layout?.width)
133136
134137
return {
135-
'--widget-basis': basis,
136-
'--widget-min-width': formatWidth(props.layout?.minWidth) ?? basis,
138+
'--widget-basis': fixedWidth ?? basis,
139+
'--widget-min-width': fixedWidth ?? formatWidth(props.layout?.minWidth) ?? basis,
137140
'--widget-max-width': props.layout?.maxWidth === null
138141
? 'none'
139-
: formatWidth(props.layout?.maxWidth) ?? 'none',
142+
: fixedWidth ?? formatWidth(props.layout?.maxWidth) ?? 'none',
143+
height: formatWidth(props.layout?.height ?? DEFAULT_WIDGET_HEIGHT),
140144
}
141145
})
142146

0 commit comments

Comments
 (0)