@lobb-js/lobb-ext-reports 0.4.1 → 0.6.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/dist/lib/components/gridStack.svelte +117 -0
- package/dist/lib/components/gridStack.svelte.d.ts +15 -0
- package/dist/lib/components/pages/reports/components/chart.svelte +23 -9
- package/dist/lib/components/pages/reports/components/charts/metric.svelte +23 -0
- package/dist/lib/components/pages/reports/components/charts/metric.svelte.d.ts +14 -0
- package/dist/lib/components/pages/reports/components/exportButton.svelte +57 -0
- package/dist/lib/components/pages/reports/components/exportButton.svelte.d.ts +14 -0
- package/dist/lib/components/pages/reports/components/report.svelte +82 -112
- package/dist/lib/components/pages/reports/index.svelte +2 -2
- package/extensions/reports/collections/charts.ts +30 -13
- package/extensions/reports/studio/lib/components/gridStack.svelte +117 -0
- package/extensions/reports/studio/lib/components/pages/reports/components/chart.svelte +23 -9
- package/extensions/reports/studio/lib/components/pages/reports/components/charts/metric.svelte +23 -0
- package/extensions/reports/studio/lib/components/pages/reports/components/exportButton.svelte +57 -0
- package/extensions/reports/studio/lib/components/pages/reports/components/report.svelte +82 -112
- package/extensions/reports/studio/lib/components/pages/reports/index.svelte +2 -2
- package/extensions/reports/tests/configs/simple.ts +3 -9
- package/package.json +7 -4
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, type Snippet } from "svelte";
|
|
3
|
+
import { GridStack } from "gridstack";
|
|
4
|
+
import "gridstack/dist/gridstack.min.css";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
column?: number;
|
|
8
|
+
cellHeight?: string;
|
|
9
|
+
onLayoutChange?: (changes: { id: string; order: number; w: number; h: number }[]) => void;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
column = 12,
|
|
15
|
+
cellHeight = "150px",
|
|
16
|
+
onLayoutChange,
|
|
17
|
+
children,
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
let gridEl: HTMLElement | undefined = $state();
|
|
21
|
+
let grid: GridStack | null = null;
|
|
22
|
+
let initialized = false;
|
|
23
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
24
|
+
|
|
25
|
+
onMount(() => {
|
|
26
|
+
if (!gridEl) return;
|
|
27
|
+
|
|
28
|
+
grid = GridStack.init(
|
|
29
|
+
{
|
|
30
|
+
column,
|
|
31
|
+
cellHeight,
|
|
32
|
+
margin: "0.5rem",
|
|
33
|
+
float: false,
|
|
34
|
+
animate: true,
|
|
35
|
+
columnOpts: {
|
|
36
|
+
layout: "moveScale",
|
|
37
|
+
breakpointForWindow: false,
|
|
38
|
+
breakpoints: [
|
|
39
|
+
{ w: 480, c: 1 },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
gridEl,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
setTimeout(() => { initialized = true; }, 0);
|
|
47
|
+
|
|
48
|
+
grid.on("dragstop resizestop", () => {
|
|
49
|
+
grid?.compact();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
grid.on("change", () => {
|
|
53
|
+
if (!onLayoutChange || !initialized) return;
|
|
54
|
+
const currentCols = grid!.getColumn();
|
|
55
|
+
if (currentCols === 1) return;
|
|
56
|
+
const sorted = (grid!.engine.nodes as any[])
|
|
57
|
+
.filter((n) => {
|
|
58
|
+
const id = n.el?.getAttribute("data-gs-id");
|
|
59
|
+
return id != null && id !== "null";
|
|
60
|
+
})
|
|
61
|
+
.sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x))
|
|
62
|
+
.map((node, index) => ({
|
|
63
|
+
id: node.el!.getAttribute("data-gs-id") as string,
|
|
64
|
+
order: index,
|
|
65
|
+
w: Math.round(node.w * (column / currentCols)),
|
|
66
|
+
h: node.h,
|
|
67
|
+
}));
|
|
68
|
+
if (sorted.length > 0) onLayoutChange(sorted);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
72
|
+
window.dispatchEvent(new Event("resize"));
|
|
73
|
+
const width = entries[0]?.contentRect.width ?? gridEl!.offsetWidth;
|
|
74
|
+
if (width <= 480) {
|
|
75
|
+
grid?.disable();
|
|
76
|
+
} else {
|
|
77
|
+
grid?.enable();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
resizeObserver.observe(gridEl);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
onDestroy(() => {
|
|
84
|
+
resizeObserver?.disconnect();
|
|
85
|
+
resizeObserver = null;
|
|
86
|
+
grid?.destroy(false);
|
|
87
|
+
grid = null;
|
|
88
|
+
});
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<div class="grid-stack" bind:this={gridEl}>
|
|
92
|
+
{@render children()}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<style>
|
|
96
|
+
:global(.grid-stack-item-content) {
|
|
97
|
+
inset: 0;
|
|
98
|
+
position: absolute;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
:global(.grid-stack) {
|
|
102
|
+
background: transparent;
|
|
103
|
+
padding: 0.5rem;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
:global(.grid-stack-placeholder > .placeholder-content) {
|
|
107
|
+
position: absolute !important;
|
|
108
|
+
width: auto !important;
|
|
109
|
+
border: 2px dashed color-mix(in oklab, var(--muted-foreground) 25%, transparent) !important;
|
|
110
|
+
border-radius: 0.375rem !important;
|
|
111
|
+
background-color: rgba(0, 0, 0, 0.06) !important;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
:global(.dark .grid-stack-placeholder > .placeholder-content) {
|
|
115
|
+
background-color: rgba(255, 255, 255, 0.06) !important;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
import "gridstack/dist/gridstack.min.css";
|
|
3
|
+
declare const __propDef: {
|
|
4
|
+
props: Record<string, never>;
|
|
5
|
+
events: {
|
|
6
|
+
[evt: string]: CustomEvent<any>;
|
|
7
|
+
};
|
|
8
|
+
slots: {};
|
|
9
|
+
};
|
|
10
|
+
export type GridStackProps = typeof __propDef.props;
|
|
11
|
+
export type GridStackEvents = typeof __propDef.events;
|
|
12
|
+
export type GridStackSlots = typeof __propDef.slots;
|
|
13
|
+
export default class GridStack extends SvelteComponentTyped<GridStackProps, GridStackEvents, GridStackSlots> {
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -2,25 +2,33 @@
|
|
|
2
2
|
import type { ExtensionProps } from "@lobb-js/studio";
|
|
3
3
|
import Table from "./charts/table.svelte";
|
|
4
4
|
import ChartJs from "./charts/chartJs.svelte";
|
|
5
|
+
import Metric from "./charts/metric.svelte";
|
|
5
6
|
|
|
6
7
|
interface Props extends ExtensionProps {
|
|
7
8
|
chartRecord: any;
|
|
8
9
|
onChartDeleted?: () => Promise<void>;
|
|
9
10
|
onChartEdited?: () => Promise<void>;
|
|
10
|
-
context?: Record<string, any>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const {
|
|
14
14
|
chartRecord,
|
|
15
15
|
onChartDeleted,
|
|
16
16
|
onChartEdited,
|
|
17
|
-
context,
|
|
18
17
|
...props
|
|
19
18
|
}: Props = $props();
|
|
20
19
|
|
|
21
20
|
const utils = props.utils;
|
|
22
21
|
const { UpdateDetailViewButton, Button } = utils.components;
|
|
23
22
|
const icons = utils.components.Icons;
|
|
23
|
+
|
|
24
|
+
function findCustomChartComponent(type: string): any {
|
|
25
|
+
const extensions = utils.ctx?.extensions ?? {};
|
|
26
|
+
for (const ext of Object.values(extensions) as any[]) {
|
|
27
|
+
const comp = ext.components?.[`reports.chart.${type}`];
|
|
28
|
+
if (comp) return comp;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
24
32
|
let dataPromise = $state(getChartData());
|
|
25
33
|
|
|
26
34
|
async function getChartData() {
|
|
@@ -29,7 +37,6 @@
|
|
|
29
37
|
chartRecord.id,
|
|
30
38
|
{
|
|
31
39
|
action: "run_query",
|
|
32
|
-
context: JSON.stringify(context),
|
|
33
40
|
},
|
|
34
41
|
);
|
|
35
42
|
|
|
@@ -57,7 +64,7 @@
|
|
|
57
64
|
}
|
|
58
65
|
</script>
|
|
59
66
|
|
|
60
|
-
<div class="gridContainer h-
|
|
67
|
+
<div class="gridContainer h-full rounded-md border bg-background">
|
|
61
68
|
<div class="flex items-center justify-between border-b p-2 text-sm">
|
|
62
69
|
<div>{chartRecord.title}</div>
|
|
63
70
|
<div class="flex">
|
|
@@ -78,7 +85,7 @@
|
|
|
78
85
|
></Button>
|
|
79
86
|
</div>
|
|
80
87
|
</div>
|
|
81
|
-
<div class="flex items-center justify-center overflow-
|
|
88
|
+
<div class="flex min-h-0 items-center justify-center overflow-hidden">
|
|
82
89
|
<svelte:boundary>
|
|
83
90
|
{#snippet failed(error, reset)}
|
|
84
91
|
<div
|
|
@@ -93,7 +100,7 @@
|
|
|
93
100
|
{/snippet}
|
|
94
101
|
{#await dataPromise}
|
|
95
102
|
<div
|
|
96
|
-
class="flex flex-col gap-4 h-full w-full
|
|
103
|
+
class="flex flex-col gap-4 h-full w-full justify-center items-center"
|
|
97
104
|
>
|
|
98
105
|
<icons.LoaderCircle
|
|
99
106
|
class="animate-spin opacity-50"
|
|
@@ -113,10 +120,17 @@
|
|
|
113
120
|
<Table input={data.input} {...props} />
|
|
114
121
|
{:else if data.type === "chartjs"}
|
|
115
122
|
<ChartJs input={data.input} {...props} />
|
|
123
|
+
{:else if data.type === "metric"}
|
|
124
|
+
<Metric input={data.input} />
|
|
116
125
|
{:else}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
{@const CustomChart = findCustomChartComponent(data.type)}
|
|
127
|
+
{#if CustomChart}
|
|
128
|
+
<CustomChart input={data.input} {...props} />
|
|
129
|
+
{:else}
|
|
130
|
+
<div class="text-muted-foreground">
|
|
131
|
+
The ({data.type}) chart type is unsupported
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|
|
120
134
|
{/if}
|
|
121
135
|
{/await}
|
|
122
136
|
</svelte:boundary>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Input {
|
|
3
|
+
value: string | number;
|
|
4
|
+
label?: string;
|
|
5
|
+
prefix?: string;
|
|
6
|
+
suffix?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
input: Input;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { input }: Props = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="flex h-full w-full select-none flex-col items-center justify-center gap-1 p-4">
|
|
17
|
+
<div class="text-5xl font-bold tabular-nums tracking-tight">
|
|
18
|
+
{#if input.prefix}<span class="text-2xl font-medium text-muted-foreground">{input.prefix}</span>{/if}{input.value}{#if input.suffix}<span class="text-2xl font-medium text-muted-foreground">{input.suffix}</span>{/if}
|
|
19
|
+
</div>
|
|
20
|
+
{#if input.label}
|
|
21
|
+
<div class="text-sm text-muted-foreground">{input.label}</div>
|
|
22
|
+
{/if}
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: Record<string, never>;
|
|
4
|
+
events: {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
};
|
|
7
|
+
slots: {};
|
|
8
|
+
};
|
|
9
|
+
export type MetricProps = typeof __propDef.props;
|
|
10
|
+
export type MetricEvents = typeof __propDef.events;
|
|
11
|
+
export type MetricSlots = typeof __propDef.slots;
|
|
12
|
+
export default class Metric extends SvelteComponentTyped<MetricProps, MetricEvents, MetricSlots> {
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { toPng } from "html-to-image";
|
|
3
|
+
import { jsPDF } from "jspdf";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
report: any;
|
|
7
|
+
utils: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { report, utils }: Props = $props();
|
|
11
|
+
const { Button, Icons } = utils.components;
|
|
12
|
+
|
|
13
|
+
let exporting = $state(false);
|
|
14
|
+
|
|
15
|
+
async function handleExport() {
|
|
16
|
+
const container = document.querySelector<HTMLElement>(".charts-export-container");
|
|
17
|
+
if (!container || !report) return;
|
|
18
|
+
exporting = true;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const backgroundColor = getComputedStyle(document.documentElement).backgroundColor;
|
|
22
|
+
|
|
23
|
+
const imgData = await toPng(container, {
|
|
24
|
+
pixelRatio: 2,
|
|
25
|
+
backgroundColor,
|
|
26
|
+
style: {
|
|
27
|
+
overflow: "visible",
|
|
28
|
+
height: `${container.scrollHeight}px`,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const img = new Image();
|
|
33
|
+
img.src = imgData;
|
|
34
|
+
await new Promise((r) => (img.onload = r));
|
|
35
|
+
|
|
36
|
+
const pdf = new jsPDF({ orientation: "landscape", unit: "px" });
|
|
37
|
+
const pageWidth = pdf.internal.pageSize.getWidth();
|
|
38
|
+
const pageHeight = pdf.internal.pageSize.getHeight();
|
|
39
|
+
const ratio = Math.min(pageWidth / img.width, pageHeight / img.height);
|
|
40
|
+
|
|
41
|
+
pdf.addImage(imgData, "PNG", 0, 0, img.width * ratio, img.height * ratio);
|
|
42
|
+
pdf.save(`${report.name ?? "report"}.pdf`);
|
|
43
|
+
} finally {
|
|
44
|
+
exporting = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<Button
|
|
50
|
+
variant="outline"
|
|
51
|
+
class="h-7 px-3 text-xs font-normal"
|
|
52
|
+
Icon={exporting ? Icons.LoaderCircle : Icons.Upload}
|
|
53
|
+
onclick={handleExport}
|
|
54
|
+
disabled={exporting}
|
|
55
|
+
>
|
|
56
|
+
{exporting ? "Exporting..." : "Export"}
|
|
57
|
+
</Button>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: Record<string, never>;
|
|
4
|
+
events: {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
};
|
|
7
|
+
slots: {};
|
|
8
|
+
};
|
|
9
|
+
export type ExportButtonProps = typeof __propDef.props;
|
|
10
|
+
export type ExportButtonEvents = typeof __propDef.events;
|
|
11
|
+
export type ExportButtonSlots = typeof __propDef.slots;
|
|
12
|
+
export default class ExportButton extends SvelteComponentTyped<ExportButtonProps, ExportButtonEvents, ExportButtonSlots> {
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -1,156 +1,126 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import GridStackComponent from "../../../gridStack.svelte";
|
|
4
4
|
import Chart from "./chart.svelte";
|
|
5
|
+
import ExportButton from "./exportButton.svelte";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
reportId: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const props: Props = $props();
|
|
11
|
-
const { intlDate } = props.utils;
|
|
7
|
+
const { reportId, utils, ...props }: { reportId: string; utils: any; [key: string]: any } = $props();
|
|
12
8
|
const {
|
|
13
9
|
SidebarTrigger,
|
|
14
10
|
CreateDetailViewButton,
|
|
15
11
|
Icons,
|
|
16
|
-
|
|
17
|
-
} = props.utils.components;
|
|
18
|
-
|
|
19
|
-
let dateRangeValue: DateRange = $state({
|
|
20
|
-
start: intlDate
|
|
21
|
-
.today(intlDate.getLocalTimeZone())
|
|
22
|
-
.subtract({ days: 30 }),
|
|
23
|
-
end: intlDate.today(intlDate.getLocalTimeZone()),
|
|
24
|
-
});
|
|
25
|
-
let context = $derived({
|
|
26
|
-
dateRange: {
|
|
27
|
-
start: dateRangeValue.start?.toString(),
|
|
28
|
-
end: dateRangeValue.end?.toString(),
|
|
29
|
-
},
|
|
30
|
-
});
|
|
12
|
+
} = utils.components;
|
|
31
13
|
|
|
32
14
|
let report: any = $state(null);
|
|
33
|
-
let
|
|
15
|
+
let charts: any[] = $state([]);
|
|
16
|
+
let loading = $state(true);
|
|
17
|
+
|
|
18
|
+
async function loadCharts() {
|
|
19
|
+
loading = true;
|
|
20
|
+
const reportsRes = await utils.lobb.findAll("reports_dashboards", {
|
|
21
|
+
sort: "id",
|
|
22
|
+
filter: { id: reportId },
|
|
23
|
+
});
|
|
24
|
+
report = (await reportsRes.json()).data[0];
|
|
25
|
+
|
|
26
|
+
const chartsRes = await utils.lobb.findAll("reports_charts", {
|
|
27
|
+
filter: { report_id: report.id },
|
|
28
|
+
});
|
|
29
|
+
charts = (await chartsRes.json()).data;
|
|
30
|
+
loading = false;
|
|
31
|
+
}
|
|
34
32
|
|
|
35
|
-
async function
|
|
36
|
-
const
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
report = (await reportsResponse.json()).data[0];
|
|
46
|
-
const chartsResponse = await props.utils.lobb.findAll(
|
|
47
|
-
"reports_charts",
|
|
48
|
-
{
|
|
49
|
-
filter: {
|
|
50
|
-
report_id: report.id,
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
return (await chartsResponse.json()).data;
|
|
33
|
+
async function handleLayoutChange(changes: { id: string; order: number; w: number; h: number }[]) {
|
|
34
|
+
for (const change of changes) {
|
|
35
|
+
await utils.lobb.updateOne("reports_charts", change.id, {
|
|
36
|
+
sort_order: change.order,
|
|
37
|
+
col_span: change.w,
|
|
38
|
+
row_span: change.h,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
55
41
|
}
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
chartsPromise = Promise.resolve(await getCharts());
|
|
43
|
+
function gridItemAttrs(chart: any): Record<string, unknown> {
|
|
44
|
+
return { "gs-w": chart.col_span ?? 6, "gs-h": chart.row_span ?? 2, "gs-auto-position": "true" };
|
|
60
45
|
}
|
|
46
|
+
|
|
47
|
+
onMount(() => loadCharts());
|
|
61
48
|
</script>
|
|
62
49
|
|
|
63
|
-
<div class="
|
|
64
|
-
{#
|
|
65
|
-
<div
|
|
66
|
-
class="flex flex-col gap-4 h-full w-full absolute justify-center items-center"
|
|
67
|
-
>
|
|
50
|
+
<div class="report-layout">
|
|
51
|
+
{#if loading}
|
|
52
|
+
<div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
|
|
68
53
|
<Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
|
|
69
54
|
<div class="flex flex-col items-center justify-center">
|
|
70
|
-
<div class="text-muted-foreground">
|
|
71
|
-
|
|
72
|
-
</div>
|
|
73
|
-
<div class="text-muted-foreground text-xs">
|
|
74
|
-
Loading all the charts of the selected dashboard
|
|
75
|
-
</div>
|
|
55
|
+
<div class="text-muted-foreground">Loading the dashboard...</div>
|
|
56
|
+
<div class="text-xs text-muted-foreground">Loading all the charts of the selected dashboard</div>
|
|
76
57
|
</div>
|
|
77
58
|
</div>
|
|
78
|
-
{:
|
|
79
|
-
<div
|
|
80
|
-
class="flex justify-between gap-2 overflow-auto border-b bg-background p-2"
|
|
81
|
-
>
|
|
59
|
+
{:else}
|
|
60
|
+
<div class="flex shrink-0 items-center justify-between gap-2 border-b bg-background p-2">
|
|
82
61
|
<div class="flex gap-2">
|
|
83
|
-
<div class="mt-1">
|
|
84
|
-
<SidebarTrigger />
|
|
85
|
-
</div>
|
|
62
|
+
<div class="mt-1"><SidebarTrigger /></div>
|
|
86
63
|
<div class="flex flex-col justify-center">
|
|
87
64
|
<h2 class="font-medium text-primary">{report.name}</h2>
|
|
88
|
-
<div class="text-xs text-muted-foreground">
|
|
89
|
-
{report.description}
|
|
90
|
-
</div>
|
|
65
|
+
<div class="text-xs text-muted-foreground">{report.description}</div>
|
|
91
66
|
</div>
|
|
92
67
|
</div>
|
|
93
68
|
<div class="flex gap-2 self-end">
|
|
94
|
-
<
|
|
69
|
+
<ExportButton {report} {utils} />
|
|
95
70
|
<CreateDetailViewButton
|
|
96
71
|
collectionName="reports_charts"
|
|
97
|
-
values={{
|
|
98
|
-
report_id: {
|
|
99
|
-
id: props.reportId,
|
|
100
|
-
name: report.name,
|
|
101
|
-
},
|
|
102
|
-
}}
|
|
72
|
+
values={{ report_id: { id: props.reportId, name: report.name } }}
|
|
103
73
|
variant="default"
|
|
104
74
|
class="h-7 px-3 text-xs font-normal"
|
|
105
75
|
Icon={Icons.Plus}
|
|
106
|
-
onSuccessfullSave={async () => await
|
|
76
|
+
onSuccessfullSave={async () => await loadCharts()}
|
|
107
77
|
>
|
|
108
78
|
Create chart
|
|
109
79
|
</CreateDetailViewButton>
|
|
110
80
|
</div>
|
|
111
81
|
</div>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
>
|
|
118
|
-
<
|
|
119
|
-
<div class="
|
|
120
|
-
<div>No charts available</div>
|
|
121
|
-
<div class="text-xs">
|
|
122
|
-
Create a new chart to fill this report page
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
82
|
+
|
|
83
|
+
<div class="charts-export-container overflow-auto p-2">
|
|
84
|
+
{#if charts.length === 0}
|
|
85
|
+
<div class="flex w-full flex-col items-center justify-center gap-4 p-10 text-muted-foreground">
|
|
86
|
+
<Icons.CircleSlash2 class="opacity-50" size="50" />
|
|
87
|
+
<div class="flex flex-col items-center justify-center">
|
|
88
|
+
<div>No charts available</div>
|
|
89
|
+
<div class="text-xs">Create a new chart to fill this report page</div>
|
|
125
90
|
</div>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
{...
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
91
|
+
</div>
|
|
92
|
+
{:else}
|
|
93
|
+
{#key charts}
|
|
94
|
+
<GridStackComponent onLayoutChange={handleLayoutChange}>
|
|
95
|
+
{#each [...charts].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) as chart (chart.id)}
|
|
96
|
+
<div
|
|
97
|
+
class="grid-stack-item"
|
|
98
|
+
data-gs-id={chart.id}
|
|
99
|
+
{...gridItemAttrs(chart)}
|
|
100
|
+
>
|
|
101
|
+
<div class="grid-stack-item-content">
|
|
102
|
+
<Chart
|
|
103
|
+
chartRecord={chart}
|
|
104
|
+
onChartDeleted={async () => await loadCharts()}
|
|
105
|
+
onChartEdited={async () => await loadCharts()}
|
|
106
|
+
{utils}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
{/each}
|
|
112
|
+
</GridStackComponent>
|
|
113
|
+
{/key}
|
|
114
|
+
{/if}
|
|
140
115
|
</div>
|
|
141
|
-
{/
|
|
116
|
+
{/if}
|
|
142
117
|
</div>
|
|
143
118
|
|
|
144
119
|
<style>
|
|
145
|
-
.
|
|
120
|
+
.report-layout {
|
|
146
121
|
display: grid;
|
|
147
|
-
grid-template-columns: 1fr;
|
|
148
122
|
grid-template-rows: auto 1fr;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.chartsGridContainer {
|
|
152
|
-
display: grid;
|
|
153
|
-
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
|
154
|
-
grid-gap: 10px;
|
|
123
|
+
width: 100%;
|
|
124
|
+
height: 100%;
|
|
155
125
|
}
|
|
156
126
|
</style>
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
|
|
64
64
|
<Sidebar title="Reports" data={sideBarData}>
|
|
65
65
|
{#snippet belowSearch()}
|
|
66
|
-
<div class="p-2
|
|
66
|
+
<div class="p-2">
|
|
67
67
|
<CreateDetailViewButton
|
|
68
68
|
collectionName="reports_dashboards"
|
|
69
69
|
variant="outline"
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
</div>
|
|
106
106
|
{/if}
|
|
107
107
|
{/snippet}
|
|
108
|
-
<div class="
|
|
108
|
+
<div class="flex h-full w-full flex-col bg-muted">
|
|
109
109
|
{#if reportId}
|
|
110
110
|
{#key reportId}
|
|
111
111
|
<Report {utils} {reportId} {...props} />
|
|
@@ -8,30 +8,47 @@ export const charts: CollectionConfig = {
|
|
|
8
8
|
},
|
|
9
9
|
"report_id": {
|
|
10
10
|
"type": "integer",
|
|
11
|
-
"
|
|
12
|
-
"required": true,
|
|
13
|
-
},
|
|
11
|
+
"required": true,
|
|
14
12
|
},
|
|
15
13
|
"title": {
|
|
16
14
|
"type": "string",
|
|
17
15
|
"length": 255,
|
|
18
|
-
"
|
|
19
|
-
"required": true,
|
|
20
|
-
},
|
|
16
|
+
"required": true,
|
|
21
17
|
},
|
|
22
18
|
"description": {
|
|
23
19
|
"type": "string",
|
|
24
20
|
"length": 255,
|
|
25
21
|
},
|
|
22
|
+
"sort_order": {
|
|
23
|
+
"type": "integer",
|
|
24
|
+
},
|
|
25
|
+
"row_span": {
|
|
26
|
+
"type": "integer",
|
|
27
|
+
"default": 2,
|
|
28
|
+
},
|
|
29
|
+
"col_span": {
|
|
30
|
+
"type": "integer",
|
|
31
|
+
"default": 6,
|
|
32
|
+
"enum": [
|
|
33
|
+
{ "value": 1, "description": "1/12" },
|
|
34
|
+
{ "value": 2, "description": "2/12" },
|
|
35
|
+
{ "value": 3, "description": "3/12" },
|
|
36
|
+
{ "value": 4, "description": "4/12" },
|
|
37
|
+
{ "value": 5, "description": "5/12" },
|
|
38
|
+
{ "value": 6, "description": "6/12" },
|
|
39
|
+
{ "value": 7, "description": "7/12" },
|
|
40
|
+
{ "value": 8, "description": "8/12" },
|
|
41
|
+
{ "value": 9, "description": "9/12" },
|
|
42
|
+
{ "value": 10, "description": "10/12" },
|
|
43
|
+
{ "value": 11, "description": "11/12" },
|
|
44
|
+
{ "value": 12, "description": "Full width" },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
26
47
|
"chart": {
|
|
27
48
|
"type": "text",
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"pre_processors": {
|
|
32
|
-
"default":
|
|
33
|
-
"async function chart(query: QueryFn, context: Record<string, unknown>) {\n\treturn {\n\t\ttype: 'table',\n\t\tinput: {\n\t\t\tdata: []\n\t\t}\n\t};\n}",
|
|
34
|
-
},
|
|
49
|
+
"required": true,
|
|
50
|
+
"default":
|
|
51
|
+
"async function chart(query: QueryFn, context: Record<string, unknown>) {\n\treturn {\n\t\ttype: 'table',\n\t\tinput: {\n\t\t\tdata: []\n\t\t}\n\t};\n}",
|
|
35
52
|
"ui": {
|
|
36
53
|
"input": {
|
|
37
54
|
"type": "code",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, type Snippet } from "svelte";
|
|
3
|
+
import { GridStack } from "gridstack";
|
|
4
|
+
import "gridstack/dist/gridstack.min.css";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
column?: number;
|
|
8
|
+
cellHeight?: string;
|
|
9
|
+
onLayoutChange?: (changes: { id: string; order: number; w: number; h: number }[]) => void;
|
|
10
|
+
children: Snippet;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
column = 12,
|
|
15
|
+
cellHeight = "150px",
|
|
16
|
+
onLayoutChange,
|
|
17
|
+
children,
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
let gridEl: HTMLElement | undefined = $state();
|
|
21
|
+
let grid: GridStack | null = null;
|
|
22
|
+
let initialized = false;
|
|
23
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
24
|
+
|
|
25
|
+
onMount(() => {
|
|
26
|
+
if (!gridEl) return;
|
|
27
|
+
|
|
28
|
+
grid = GridStack.init(
|
|
29
|
+
{
|
|
30
|
+
column,
|
|
31
|
+
cellHeight,
|
|
32
|
+
margin: "0.5rem",
|
|
33
|
+
float: false,
|
|
34
|
+
animate: true,
|
|
35
|
+
columnOpts: {
|
|
36
|
+
layout: "moveScale",
|
|
37
|
+
breakpointForWindow: false,
|
|
38
|
+
breakpoints: [
|
|
39
|
+
{ w: 480, c: 1 },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
gridEl,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
setTimeout(() => { initialized = true; }, 0);
|
|
47
|
+
|
|
48
|
+
grid.on("dragstop resizestop", () => {
|
|
49
|
+
grid?.compact();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
grid.on("change", () => {
|
|
53
|
+
if (!onLayoutChange || !initialized) return;
|
|
54
|
+
const currentCols = grid!.getColumn();
|
|
55
|
+
if (currentCols === 1) return;
|
|
56
|
+
const sorted = (grid!.engine.nodes as any[])
|
|
57
|
+
.filter((n) => {
|
|
58
|
+
const id = n.el?.getAttribute("data-gs-id");
|
|
59
|
+
return id != null && id !== "null";
|
|
60
|
+
})
|
|
61
|
+
.sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x))
|
|
62
|
+
.map((node, index) => ({
|
|
63
|
+
id: node.el!.getAttribute("data-gs-id") as string,
|
|
64
|
+
order: index,
|
|
65
|
+
w: Math.round(node.w * (column / currentCols)),
|
|
66
|
+
h: node.h,
|
|
67
|
+
}));
|
|
68
|
+
if (sorted.length > 0) onLayoutChange(sorted);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
72
|
+
window.dispatchEvent(new Event("resize"));
|
|
73
|
+
const width = entries[0]?.contentRect.width ?? gridEl!.offsetWidth;
|
|
74
|
+
if (width <= 480) {
|
|
75
|
+
grid?.disable();
|
|
76
|
+
} else {
|
|
77
|
+
grid?.enable();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
resizeObserver.observe(gridEl);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
onDestroy(() => {
|
|
84
|
+
resizeObserver?.disconnect();
|
|
85
|
+
resizeObserver = null;
|
|
86
|
+
grid?.destroy(false);
|
|
87
|
+
grid = null;
|
|
88
|
+
});
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<div class="grid-stack" bind:this={gridEl}>
|
|
92
|
+
{@render children()}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<style>
|
|
96
|
+
:global(.grid-stack-item-content) {
|
|
97
|
+
inset: 0;
|
|
98
|
+
position: absolute;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
:global(.grid-stack) {
|
|
102
|
+
background: transparent;
|
|
103
|
+
padding: 0.5rem;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
:global(.grid-stack-placeholder > .placeholder-content) {
|
|
107
|
+
position: absolute !important;
|
|
108
|
+
width: auto !important;
|
|
109
|
+
border: 2px dashed color-mix(in oklab, var(--muted-foreground) 25%, transparent) !important;
|
|
110
|
+
border-radius: 0.375rem !important;
|
|
111
|
+
background-color: rgba(0, 0, 0, 0.06) !important;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
:global(.dark .grid-stack-placeholder > .placeholder-content) {
|
|
115
|
+
background-color: rgba(255, 255, 255, 0.06) !important;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -2,25 +2,33 @@
|
|
|
2
2
|
import type { ExtensionProps } from "@lobb-js/studio";
|
|
3
3
|
import Table from "./charts/table.svelte";
|
|
4
4
|
import ChartJs from "./charts/chartJs.svelte";
|
|
5
|
+
import Metric from "./charts/metric.svelte";
|
|
5
6
|
|
|
6
7
|
interface Props extends ExtensionProps {
|
|
7
8
|
chartRecord: any;
|
|
8
9
|
onChartDeleted?: () => Promise<void>;
|
|
9
10
|
onChartEdited?: () => Promise<void>;
|
|
10
|
-
context?: Record<string, any>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const {
|
|
14
14
|
chartRecord,
|
|
15
15
|
onChartDeleted,
|
|
16
16
|
onChartEdited,
|
|
17
|
-
context,
|
|
18
17
|
...props
|
|
19
18
|
}: Props = $props();
|
|
20
19
|
|
|
21
20
|
const utils = props.utils;
|
|
22
21
|
const { UpdateDetailViewButton, Button } = utils.components;
|
|
23
22
|
const icons = utils.components.Icons;
|
|
23
|
+
|
|
24
|
+
function findCustomChartComponent(type: string): any {
|
|
25
|
+
const extensions = utils.ctx?.extensions ?? {};
|
|
26
|
+
for (const ext of Object.values(extensions) as any[]) {
|
|
27
|
+
const comp = ext.components?.[`reports.chart.${type}`];
|
|
28
|
+
if (comp) return comp;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
24
32
|
let dataPromise = $state(getChartData());
|
|
25
33
|
|
|
26
34
|
async function getChartData() {
|
|
@@ -29,7 +37,6 @@
|
|
|
29
37
|
chartRecord.id,
|
|
30
38
|
{
|
|
31
39
|
action: "run_query",
|
|
32
|
-
context: JSON.stringify(context),
|
|
33
40
|
},
|
|
34
41
|
);
|
|
35
42
|
|
|
@@ -57,7 +64,7 @@
|
|
|
57
64
|
}
|
|
58
65
|
</script>
|
|
59
66
|
|
|
60
|
-
<div class="gridContainer h-
|
|
67
|
+
<div class="gridContainer h-full rounded-md border bg-background">
|
|
61
68
|
<div class="flex items-center justify-between border-b p-2 text-sm">
|
|
62
69
|
<div>{chartRecord.title}</div>
|
|
63
70
|
<div class="flex">
|
|
@@ -78,7 +85,7 @@
|
|
|
78
85
|
></Button>
|
|
79
86
|
</div>
|
|
80
87
|
</div>
|
|
81
|
-
<div class="flex items-center justify-center overflow-
|
|
88
|
+
<div class="flex min-h-0 items-center justify-center overflow-hidden">
|
|
82
89
|
<svelte:boundary>
|
|
83
90
|
{#snippet failed(error, reset)}
|
|
84
91
|
<div
|
|
@@ -93,7 +100,7 @@
|
|
|
93
100
|
{/snippet}
|
|
94
101
|
{#await dataPromise}
|
|
95
102
|
<div
|
|
96
|
-
class="flex flex-col gap-4 h-full w-full
|
|
103
|
+
class="flex flex-col gap-4 h-full w-full justify-center items-center"
|
|
97
104
|
>
|
|
98
105
|
<icons.LoaderCircle
|
|
99
106
|
class="animate-spin opacity-50"
|
|
@@ -113,10 +120,17 @@
|
|
|
113
120
|
<Table input={data.input} {...props} />
|
|
114
121
|
{:else if data.type === "chartjs"}
|
|
115
122
|
<ChartJs input={data.input} {...props} />
|
|
123
|
+
{:else if data.type === "metric"}
|
|
124
|
+
<Metric input={data.input} />
|
|
116
125
|
{:else}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
{@const CustomChart = findCustomChartComponent(data.type)}
|
|
127
|
+
{#if CustomChart}
|
|
128
|
+
<CustomChart input={data.input} {...props} />
|
|
129
|
+
{:else}
|
|
130
|
+
<div class="text-muted-foreground">
|
|
131
|
+
The ({data.type}) chart type is unsupported
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|
|
120
134
|
{/if}
|
|
121
135
|
{/await}
|
|
122
136
|
</svelte:boundary>
|
package/extensions/reports/studio/lib/components/pages/reports/components/charts/metric.svelte
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Input {
|
|
3
|
+
value: string | number;
|
|
4
|
+
label?: string;
|
|
5
|
+
prefix?: string;
|
|
6
|
+
suffix?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
input: Input;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { input }: Props = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="flex h-full w-full select-none flex-col items-center justify-center gap-1 p-4">
|
|
17
|
+
<div class="text-5xl font-bold tabular-nums tracking-tight">
|
|
18
|
+
{#if input.prefix}<span class="text-2xl font-medium text-muted-foreground">{input.prefix}</span>{/if}{input.value}{#if input.suffix}<span class="text-2xl font-medium text-muted-foreground">{input.suffix}</span>{/if}
|
|
19
|
+
</div>
|
|
20
|
+
{#if input.label}
|
|
21
|
+
<div class="text-sm text-muted-foreground">{input.label}</div>
|
|
22
|
+
{/if}
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { toPng } from "html-to-image";
|
|
3
|
+
import { jsPDF } from "jspdf";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
report: any;
|
|
7
|
+
utils: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { report, utils }: Props = $props();
|
|
11
|
+
const { Button, Icons } = utils.components;
|
|
12
|
+
|
|
13
|
+
let exporting = $state(false);
|
|
14
|
+
|
|
15
|
+
async function handleExport() {
|
|
16
|
+
const container = document.querySelector<HTMLElement>(".charts-export-container");
|
|
17
|
+
if (!container || !report) return;
|
|
18
|
+
exporting = true;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const backgroundColor = getComputedStyle(document.documentElement).backgroundColor;
|
|
22
|
+
|
|
23
|
+
const imgData = await toPng(container, {
|
|
24
|
+
pixelRatio: 2,
|
|
25
|
+
backgroundColor,
|
|
26
|
+
style: {
|
|
27
|
+
overflow: "visible",
|
|
28
|
+
height: `${container.scrollHeight}px`,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const img = new Image();
|
|
33
|
+
img.src = imgData;
|
|
34
|
+
await new Promise((r) => (img.onload = r));
|
|
35
|
+
|
|
36
|
+
const pdf = new jsPDF({ orientation: "landscape", unit: "px" });
|
|
37
|
+
const pageWidth = pdf.internal.pageSize.getWidth();
|
|
38
|
+
const pageHeight = pdf.internal.pageSize.getHeight();
|
|
39
|
+
const ratio = Math.min(pageWidth / img.width, pageHeight / img.height);
|
|
40
|
+
|
|
41
|
+
pdf.addImage(imgData, "PNG", 0, 0, img.width * ratio, img.height * ratio);
|
|
42
|
+
pdf.save(`${report.name ?? "report"}.pdf`);
|
|
43
|
+
} finally {
|
|
44
|
+
exporting = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<Button
|
|
50
|
+
variant="outline"
|
|
51
|
+
class="h-7 px-3 text-xs font-normal"
|
|
52
|
+
Icon={exporting ? Icons.LoaderCircle : Icons.Upload}
|
|
53
|
+
onclick={handleExport}
|
|
54
|
+
disabled={exporting}
|
|
55
|
+
>
|
|
56
|
+
{exporting ? "Exporting..." : "Export"}
|
|
57
|
+
</Button>
|
|
@@ -1,156 +1,126 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import GridStackComponent from "../../../gridStack.svelte";
|
|
4
4
|
import Chart from "./chart.svelte";
|
|
5
|
+
import ExportButton from "./exportButton.svelte";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
reportId: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const props: Props = $props();
|
|
11
|
-
const { intlDate } = props.utils;
|
|
7
|
+
const { reportId, utils, ...props }: { reportId: string; utils: any; [key: string]: any } = $props();
|
|
12
8
|
const {
|
|
13
9
|
SidebarTrigger,
|
|
14
10
|
CreateDetailViewButton,
|
|
15
11
|
Icons,
|
|
16
|
-
|
|
17
|
-
} = props.utils.components;
|
|
18
|
-
|
|
19
|
-
let dateRangeValue: DateRange = $state({
|
|
20
|
-
start: intlDate
|
|
21
|
-
.today(intlDate.getLocalTimeZone())
|
|
22
|
-
.subtract({ days: 30 }),
|
|
23
|
-
end: intlDate.today(intlDate.getLocalTimeZone()),
|
|
24
|
-
});
|
|
25
|
-
let context = $derived({
|
|
26
|
-
dateRange: {
|
|
27
|
-
start: dateRangeValue.start?.toString(),
|
|
28
|
-
end: dateRangeValue.end?.toString(),
|
|
29
|
-
},
|
|
30
|
-
});
|
|
12
|
+
} = utils.components;
|
|
31
13
|
|
|
32
14
|
let report: any = $state(null);
|
|
33
|
-
let
|
|
15
|
+
let charts: any[] = $state([]);
|
|
16
|
+
let loading = $state(true);
|
|
17
|
+
|
|
18
|
+
async function loadCharts() {
|
|
19
|
+
loading = true;
|
|
20
|
+
const reportsRes = await utils.lobb.findAll("reports_dashboards", {
|
|
21
|
+
sort: "id",
|
|
22
|
+
filter: { id: reportId },
|
|
23
|
+
});
|
|
24
|
+
report = (await reportsRes.json()).data[0];
|
|
25
|
+
|
|
26
|
+
const chartsRes = await utils.lobb.findAll("reports_charts", {
|
|
27
|
+
filter: { report_id: report.id },
|
|
28
|
+
});
|
|
29
|
+
charts = (await chartsRes.json()).data;
|
|
30
|
+
loading = false;
|
|
31
|
+
}
|
|
34
32
|
|
|
35
|
-
async function
|
|
36
|
-
const
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
report = (await reportsResponse.json()).data[0];
|
|
46
|
-
const chartsResponse = await props.utils.lobb.findAll(
|
|
47
|
-
"reports_charts",
|
|
48
|
-
{
|
|
49
|
-
filter: {
|
|
50
|
-
report_id: report.id,
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
return (await chartsResponse.json()).data;
|
|
33
|
+
async function handleLayoutChange(changes: { id: string; order: number; w: number; h: number }[]) {
|
|
34
|
+
for (const change of changes) {
|
|
35
|
+
await utils.lobb.updateOne("reports_charts", change.id, {
|
|
36
|
+
sort_order: change.order,
|
|
37
|
+
col_span: change.w,
|
|
38
|
+
row_span: change.h,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
55
41
|
}
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
chartsPromise = Promise.resolve(await getCharts());
|
|
43
|
+
function gridItemAttrs(chart: any): Record<string, unknown> {
|
|
44
|
+
return { "gs-w": chart.col_span ?? 6, "gs-h": chart.row_span ?? 2, "gs-auto-position": "true" };
|
|
60
45
|
}
|
|
46
|
+
|
|
47
|
+
onMount(() => loadCharts());
|
|
61
48
|
</script>
|
|
62
49
|
|
|
63
|
-
<div class="
|
|
64
|
-
{#
|
|
65
|
-
<div
|
|
66
|
-
class="flex flex-col gap-4 h-full w-full absolute justify-center items-center"
|
|
67
|
-
>
|
|
50
|
+
<div class="report-layout">
|
|
51
|
+
{#if loading}
|
|
52
|
+
<div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
|
|
68
53
|
<Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
|
|
69
54
|
<div class="flex flex-col items-center justify-center">
|
|
70
|
-
<div class="text-muted-foreground">
|
|
71
|
-
|
|
72
|
-
</div>
|
|
73
|
-
<div class="text-muted-foreground text-xs">
|
|
74
|
-
Loading all the charts of the selected dashboard
|
|
75
|
-
</div>
|
|
55
|
+
<div class="text-muted-foreground">Loading the dashboard...</div>
|
|
56
|
+
<div class="text-xs text-muted-foreground">Loading all the charts of the selected dashboard</div>
|
|
76
57
|
</div>
|
|
77
58
|
</div>
|
|
78
|
-
{:
|
|
79
|
-
<div
|
|
80
|
-
class="flex justify-between gap-2 overflow-auto border-b bg-background p-2"
|
|
81
|
-
>
|
|
59
|
+
{:else}
|
|
60
|
+
<div class="flex shrink-0 items-center justify-between gap-2 border-b bg-background p-2">
|
|
82
61
|
<div class="flex gap-2">
|
|
83
|
-
<div class="mt-1">
|
|
84
|
-
<SidebarTrigger />
|
|
85
|
-
</div>
|
|
62
|
+
<div class="mt-1"><SidebarTrigger /></div>
|
|
86
63
|
<div class="flex flex-col justify-center">
|
|
87
64
|
<h2 class="font-medium text-primary">{report.name}</h2>
|
|
88
|
-
<div class="text-xs text-muted-foreground">
|
|
89
|
-
{report.description}
|
|
90
|
-
</div>
|
|
65
|
+
<div class="text-xs text-muted-foreground">{report.description}</div>
|
|
91
66
|
</div>
|
|
92
67
|
</div>
|
|
93
68
|
<div class="flex gap-2 self-end">
|
|
94
|
-
<
|
|
69
|
+
<ExportButton {report} {utils} />
|
|
95
70
|
<CreateDetailViewButton
|
|
96
71
|
collectionName="reports_charts"
|
|
97
|
-
values={{
|
|
98
|
-
report_id: {
|
|
99
|
-
id: props.reportId,
|
|
100
|
-
name: report.name,
|
|
101
|
-
},
|
|
102
|
-
}}
|
|
72
|
+
values={{ report_id: { id: props.reportId, name: report.name } }}
|
|
103
73
|
variant="default"
|
|
104
74
|
class="h-7 px-3 text-xs font-normal"
|
|
105
75
|
Icon={Icons.Plus}
|
|
106
|
-
onSuccessfullSave={async () => await
|
|
76
|
+
onSuccessfullSave={async () => await loadCharts()}
|
|
107
77
|
>
|
|
108
78
|
Create chart
|
|
109
79
|
</CreateDetailViewButton>
|
|
110
80
|
</div>
|
|
111
81
|
</div>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
>
|
|
118
|
-
<
|
|
119
|
-
<div class="
|
|
120
|
-
<div>No charts available</div>
|
|
121
|
-
<div class="text-xs">
|
|
122
|
-
Create a new chart to fill this report page
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
82
|
+
|
|
83
|
+
<div class="charts-export-container overflow-auto p-2">
|
|
84
|
+
{#if charts.length === 0}
|
|
85
|
+
<div class="flex w-full flex-col items-center justify-center gap-4 p-10 text-muted-foreground">
|
|
86
|
+
<Icons.CircleSlash2 class="opacity-50" size="50" />
|
|
87
|
+
<div class="flex flex-col items-center justify-center">
|
|
88
|
+
<div>No charts available</div>
|
|
89
|
+
<div class="text-xs">Create a new chart to fill this report page</div>
|
|
125
90
|
</div>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
{...
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
91
|
+
</div>
|
|
92
|
+
{:else}
|
|
93
|
+
{#key charts}
|
|
94
|
+
<GridStackComponent onLayoutChange={handleLayoutChange}>
|
|
95
|
+
{#each [...charts].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) as chart (chart.id)}
|
|
96
|
+
<div
|
|
97
|
+
class="grid-stack-item"
|
|
98
|
+
data-gs-id={chart.id}
|
|
99
|
+
{...gridItemAttrs(chart)}
|
|
100
|
+
>
|
|
101
|
+
<div class="grid-stack-item-content">
|
|
102
|
+
<Chart
|
|
103
|
+
chartRecord={chart}
|
|
104
|
+
onChartDeleted={async () => await loadCharts()}
|
|
105
|
+
onChartEdited={async () => await loadCharts()}
|
|
106
|
+
{utils}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
{/each}
|
|
112
|
+
</GridStackComponent>
|
|
113
|
+
{/key}
|
|
114
|
+
{/if}
|
|
140
115
|
</div>
|
|
141
|
-
{/
|
|
116
|
+
{/if}
|
|
142
117
|
</div>
|
|
143
118
|
|
|
144
119
|
<style>
|
|
145
|
-
.
|
|
120
|
+
.report-layout {
|
|
146
121
|
display: grid;
|
|
147
|
-
grid-template-columns: 1fr;
|
|
148
122
|
grid-template-rows: auto 1fr;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.chartsGridContainer {
|
|
152
|
-
display: grid;
|
|
153
|
-
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
|
154
|
-
grid-gap: 10px;
|
|
123
|
+
width: 100%;
|
|
124
|
+
height: 100%;
|
|
155
125
|
}
|
|
156
126
|
</style>
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
|
|
64
64
|
<Sidebar title="Reports" data={sideBarData}>
|
|
65
65
|
{#snippet belowSearch()}
|
|
66
|
-
<div class="p-2
|
|
66
|
+
<div class="p-2">
|
|
67
67
|
<CreateDetailViewButton
|
|
68
68
|
collectionName="reports_dashboards"
|
|
69
69
|
variant="outline"
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
</div>
|
|
106
106
|
{/if}
|
|
107
107
|
{/snippet}
|
|
108
|
-
<div class="
|
|
108
|
+
<div class="flex h-full w-full flex-col bg-muted">
|
|
109
109
|
{#if reportId}
|
|
110
110
|
{#key reportId}
|
|
111
111
|
<Report {utils} {reportId} {...props} />
|
|
@@ -33,21 +33,15 @@ export const simpleConfig: Config = {
|
|
|
33
33
|
donor_name: {
|
|
34
34
|
type: "string",
|
|
35
35
|
length: 255,
|
|
36
|
-
|
|
37
|
-
required: true,
|
|
38
|
-
},
|
|
36
|
+
required: true,
|
|
39
37
|
},
|
|
40
38
|
amount: {
|
|
41
39
|
type: "decimal",
|
|
42
|
-
|
|
43
|
-
required: true,
|
|
44
|
-
},
|
|
40
|
+
required: true,
|
|
45
41
|
},
|
|
46
42
|
created_at: {
|
|
47
43
|
type: "date",
|
|
48
|
-
|
|
49
|
-
default: "{{ now }}",
|
|
50
|
-
},
|
|
44
|
+
default: "{{ now }}",
|
|
51
45
|
},
|
|
52
46
|
},
|
|
53
47
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/lobb-ext-reports",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"license": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -34,16 +34,19 @@
|
|
|
34
34
|
"package": "svelte-package --input extensions/reports/studio"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@lobb-js/core": "^0.
|
|
37
|
+
"@lobb-js/core": "^0.23.0",
|
|
38
38
|
"chart.js": "^4.4.8",
|
|
39
|
+
"gridstack": "^12.6.0",
|
|
39
40
|
"hono": "^4.7.0",
|
|
41
|
+
"html-to-image": "^1.11.13",
|
|
42
|
+
"jspdf": "^4.2.1",
|
|
40
43
|
"lodash-es": "^4.17.21",
|
|
41
44
|
"openapi-types": "^12.1.3"
|
|
42
45
|
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@faker-js/faker": "^9.6.0",
|
|
45
48
|
"@playwright/test": "^1.58.2",
|
|
46
|
-
"@lobb-js/studio": "^0.
|
|
49
|
+
"@lobb-js/studio": "^0.19.0",
|
|
47
50
|
"@lucide/svelte": "^0.563.1",
|
|
48
51
|
"@sveltejs/adapter-node": "^5.5.4",
|
|
49
52
|
"@sveltejs/kit": "^2.55.0",
|