@lobb-js/lobb-ext-reports 0.5.0 → 0.6.1

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.
@@ -45,11 +45,11 @@
45
45
 
46
46
  setTimeout(() => { initialized = true; }, 0);
47
47
 
48
- grid.on("dragstop resizestop", () => {
48
+ grid.on("dragstop", () => {
49
49
  grid?.compact();
50
50
  });
51
51
 
52
- grid.on("change", () => {
52
+ function saveLayout() {
53
53
  if (!onLayoutChange || !initialized) return;
54
54
  const currentCols = grid!.getColumn();
55
55
  if (currentCols === 1) return;
@@ -66,6 +66,13 @@
66
66
  h: node.h,
67
67
  }));
68
68
  if (sorted.length > 0) onLayoutChange(sorted);
69
+ }
70
+
71
+ grid.on("change", saveLayout);
72
+
73
+ grid.on("resizestop", () => {
74
+ grid?.compact();
75
+ saveLayout();
69
76
  });
70
77
 
71
78
  resizeObserver = new ResizeObserver((entries) => {
@@ -102,4 +109,16 @@
102
109
  background: transparent;
103
110
  padding: 0.5rem;
104
111
  }
112
+
113
+ :global(.grid-stack-placeholder > .placeholder-content) {
114
+ position: absolute !important;
115
+ width: auto !important;
116
+ border: 2px dashed color-mix(in oklab, var(--muted-foreground) 25%, transparent) !important;
117
+ border-radius: 0.375rem !important;
118
+ background-color: rgba(0, 0, 0, 0.06) !important;
119
+ }
120
+
121
+ :global(.dark .grid-stack-placeholder > .placeholder-content) {
122
+ background-color: rgba(255, 255, 255, 0.06) !important;
123
+ }
105
124
  </style>
@@ -8,20 +8,27 @@
8
8
  chartRecord: any;
9
9
  onChartDeleted?: () => Promise<void>;
10
10
  onChartEdited?: () => Promise<void>;
11
- context?: Record<string, any>;
12
11
  }
13
12
 
14
13
  const {
15
14
  chartRecord,
16
15
  onChartDeleted,
17
16
  onChartEdited,
18
- context,
19
17
  ...props
20
18
  }: Props = $props();
21
19
 
22
20
  const utils = props.utils;
23
21
  const { UpdateDetailViewButton, Button } = utils.components;
24
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
+ }
25
32
  let dataPromise = $state(getChartData());
26
33
 
27
34
  async function getChartData() {
@@ -30,7 +37,6 @@
30
37
  chartRecord.id,
31
38
  {
32
39
  action: "run_query",
33
- context: JSON.stringify(context),
34
40
  },
35
41
  );
36
42
 
@@ -117,9 +123,14 @@
117
123
  {:else if data.type === "metric"}
118
124
  <Metric input={data.input} />
119
125
  {:else}
120
- <div class="text-muted-foreground">
121
- The ({data.type}) chart type is unsupported
122
- </div>
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}
123
134
  {/if}
124
135
  {/await}
125
136
  </svelte:boundary>
@@ -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,33 +1,15 @@
1
1
  <script lang="ts">
2
- import type { ExtensionProps } from "@lobb-js/studio";
3
- import type { DateRange } from "bits-ui";
4
2
  import { onMount } from "svelte";
5
3
  import GridStackComponent from "../../../gridStack.svelte";
6
4
  import Chart from "./chart.svelte";
5
+ import ExportButton from "./exportButton.svelte";
7
6
 
8
- interface Props extends ExtensionProps {
9
- reportId: string;
10
- }
11
-
12
- const props: Props = $props();
13
- const { intlDate } = props.utils;
7
+ const { reportId, utils, ...props }: { reportId: string; utils: any; [key: string]: any } = $props();
14
8
  const {
15
9
  SidebarTrigger,
16
10
  CreateDetailViewButton,
17
11
  Icons,
18
- RangeCalendarButton
19
- } = props.utils.components;
20
-
21
- let dateRangeValue: DateRange = $state({
22
- start: intlDate.today(intlDate.getLocalTimeZone()).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
15
  let charts: any[] = $state([]);
@@ -35,13 +17,13 @@
35
17
 
36
18
  async function loadCharts() {
37
19
  loading = true;
38
- const reportsRes = await props.utils.lobb.findAll("reports_dashboards", {
20
+ const reportsRes = await utils.lobb.findAll("reports_dashboards", {
39
21
  sort: "id",
40
- filter: { id: props.reportId },
22
+ filter: { id: reportId },
41
23
  });
42
24
  report = (await reportsRes.json()).data[0];
43
25
 
44
- const chartsRes = await props.utils.lobb.findAll("reports_charts", {
26
+ const chartsRes = await utils.lobb.findAll("reports_charts", {
45
27
  filter: { report_id: report.id },
46
28
  });
47
29
  charts = (await chartsRes.json()).data;
@@ -50,7 +32,7 @@
50
32
 
51
33
  async function handleLayoutChange(changes: { id: string; order: number; w: number; h: number }[]) {
52
34
  for (const change of changes) {
53
- await props.utils.lobb.updateOne("reports_charts", change.id, {
35
+ await utils.lobb.updateOne("reports_charts", change.id, {
54
36
  sort_order: change.order,
55
37
  col_span: change.w,
56
38
  row_span: change.h,
@@ -67,7 +49,7 @@
67
49
 
68
50
  <div class="report-layout">
69
51
  {#if loading}
70
- <div class="flex h-full w-full flex-col items-center justify-center gap-4">
52
+ <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
71
53
  <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
72
54
  <div class="flex flex-col items-center justify-center">
73
55
  <div class="text-muted-foreground">Loading the dashboard...</div>
@@ -84,7 +66,7 @@
84
66
  </div>
85
67
  </div>
86
68
  <div class="flex gap-2 self-end">
87
- <RangeCalendarButton bind:value={dateRangeValue} />
69
+ <ExportButton {report} {utils} />
88
70
  <CreateDetailViewButton
89
71
  collectionName="reports_charts"
90
72
  values={{ report_id: { id: props.reportId, name: report.name } }}
@@ -98,7 +80,7 @@
98
80
  </div>
99
81
  </div>
100
82
 
101
- <div class="overflow-auto p-2">
83
+ <div class="charts-export-container overflow-auto p-2">
102
84
  {#if charts.length === 0}
103
85
  <div class="flex w-full flex-col items-center justify-center gap-4 p-10 text-muted-foreground">
104
86
  <Icons.CircleSlash2 class="opacity-50" size="50" />
@@ -117,15 +99,13 @@
117
99
  {...gridItemAttrs(chart)}
118
100
  >
119
101
  <div class="grid-stack-item-content">
120
- {#key context}
121
- <Chart
122
- chartRecord={chart}
123
- onChartDeleted={async () => await loadCharts()}
124
- onChartEdited={async () => await loadCharts()}
125
- {context}
126
- {...props}
127
- />
128
- {/key}
102
+ <Chart
103
+ chartRecord={chart}
104
+ onChartDeleted={async () => await loadCharts()}
105
+ onChartEdited={async () => await loadCharts()}
106
+ {utils}
107
+ {...props}
108
+ />
129
109
  </div>
130
110
  </div>
131
111
  {/each}
@@ -45,11 +45,11 @@
45
45
 
46
46
  setTimeout(() => { initialized = true; }, 0);
47
47
 
48
- grid.on("dragstop resizestop", () => {
48
+ grid.on("dragstop", () => {
49
49
  grid?.compact();
50
50
  });
51
51
 
52
- grid.on("change", () => {
52
+ function saveLayout() {
53
53
  if (!onLayoutChange || !initialized) return;
54
54
  const currentCols = grid!.getColumn();
55
55
  if (currentCols === 1) return;
@@ -66,6 +66,13 @@
66
66
  h: node.h,
67
67
  }));
68
68
  if (sorted.length > 0) onLayoutChange(sorted);
69
+ }
70
+
71
+ grid.on("change", saveLayout);
72
+
73
+ grid.on("resizestop", () => {
74
+ grid?.compact();
75
+ saveLayout();
69
76
  });
70
77
 
71
78
  resizeObserver = new ResizeObserver((entries) => {
@@ -102,4 +109,16 @@
102
109
  background: transparent;
103
110
  padding: 0.5rem;
104
111
  }
112
+
113
+ :global(.grid-stack-placeholder > .placeholder-content) {
114
+ position: absolute !important;
115
+ width: auto !important;
116
+ border: 2px dashed color-mix(in oklab, var(--muted-foreground) 25%, transparent) !important;
117
+ border-radius: 0.375rem !important;
118
+ background-color: rgba(0, 0, 0, 0.06) !important;
119
+ }
120
+
121
+ :global(.dark .grid-stack-placeholder > .placeholder-content) {
122
+ background-color: rgba(255, 255, 255, 0.06) !important;
123
+ }
105
124
  </style>
@@ -8,20 +8,27 @@
8
8
  chartRecord: any;
9
9
  onChartDeleted?: () => Promise<void>;
10
10
  onChartEdited?: () => Promise<void>;
11
- context?: Record<string, any>;
12
11
  }
13
12
 
14
13
  const {
15
14
  chartRecord,
16
15
  onChartDeleted,
17
16
  onChartEdited,
18
- context,
19
17
  ...props
20
18
  }: Props = $props();
21
19
 
22
20
  const utils = props.utils;
23
21
  const { UpdateDetailViewButton, Button } = utils.components;
24
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
+ }
25
32
  let dataPromise = $state(getChartData());
26
33
 
27
34
  async function getChartData() {
@@ -30,7 +37,6 @@
30
37
  chartRecord.id,
31
38
  {
32
39
  action: "run_query",
33
- context: JSON.stringify(context),
34
40
  },
35
41
  );
36
42
 
@@ -117,9 +123,14 @@
117
123
  {:else if data.type === "metric"}
118
124
  <Metric input={data.input} />
119
125
  {:else}
120
- <div class="text-muted-foreground">
121
- The ({data.type}) chart type is unsupported
122
- </div>
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}
123
134
  {/if}
124
135
  {/await}
125
136
  </svelte:boundary>
@@ -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,33 +1,15 @@
1
1
  <script lang="ts">
2
- import type { ExtensionProps } from "@lobb-js/studio";
3
- import type { DateRange } from "bits-ui";
4
2
  import { onMount } from "svelte";
5
3
  import GridStackComponent from "../../../gridStack.svelte";
6
4
  import Chart from "./chart.svelte";
5
+ import ExportButton from "./exportButton.svelte";
7
6
 
8
- interface Props extends ExtensionProps {
9
- reportId: string;
10
- }
11
-
12
- const props: Props = $props();
13
- const { intlDate } = props.utils;
7
+ const { reportId, utils, ...props }: { reportId: string; utils: any; [key: string]: any } = $props();
14
8
  const {
15
9
  SidebarTrigger,
16
10
  CreateDetailViewButton,
17
11
  Icons,
18
- RangeCalendarButton
19
- } = props.utils.components;
20
-
21
- let dateRangeValue: DateRange = $state({
22
- start: intlDate.today(intlDate.getLocalTimeZone()).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
15
  let charts: any[] = $state([]);
@@ -35,13 +17,13 @@
35
17
 
36
18
  async function loadCharts() {
37
19
  loading = true;
38
- const reportsRes = await props.utils.lobb.findAll("reports_dashboards", {
20
+ const reportsRes = await utils.lobb.findAll("reports_dashboards", {
39
21
  sort: "id",
40
- filter: { id: props.reportId },
22
+ filter: { id: reportId },
41
23
  });
42
24
  report = (await reportsRes.json()).data[0];
43
25
 
44
- const chartsRes = await props.utils.lobb.findAll("reports_charts", {
26
+ const chartsRes = await utils.lobb.findAll("reports_charts", {
45
27
  filter: { report_id: report.id },
46
28
  });
47
29
  charts = (await chartsRes.json()).data;
@@ -50,7 +32,7 @@
50
32
 
51
33
  async function handleLayoutChange(changes: { id: string; order: number; w: number; h: number }[]) {
52
34
  for (const change of changes) {
53
- await props.utils.lobb.updateOne("reports_charts", change.id, {
35
+ await utils.lobb.updateOne("reports_charts", change.id, {
54
36
  sort_order: change.order,
55
37
  col_span: change.w,
56
38
  row_span: change.h,
@@ -67,7 +49,7 @@
67
49
 
68
50
  <div class="report-layout">
69
51
  {#if loading}
70
- <div class="flex h-full w-full flex-col items-center justify-center gap-4">
52
+ <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
71
53
  <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
72
54
  <div class="flex flex-col items-center justify-center">
73
55
  <div class="text-muted-foreground">Loading the dashboard...</div>
@@ -84,7 +66,7 @@
84
66
  </div>
85
67
  </div>
86
68
  <div class="flex gap-2 self-end">
87
- <RangeCalendarButton bind:value={dateRangeValue} />
69
+ <ExportButton {report} {utils} />
88
70
  <CreateDetailViewButton
89
71
  collectionName="reports_charts"
90
72
  values={{ report_id: { id: props.reportId, name: report.name } }}
@@ -98,7 +80,7 @@
98
80
  </div>
99
81
  </div>
100
82
 
101
- <div class="overflow-auto p-2">
83
+ <div class="charts-export-container overflow-auto p-2">
102
84
  {#if charts.length === 0}
103
85
  <div class="flex w-full flex-col items-center justify-center gap-4 p-10 text-muted-foreground">
104
86
  <Icons.CircleSlash2 class="opacity-50" size="50" />
@@ -117,15 +99,13 @@
117
99
  {...gridItemAttrs(chart)}
118
100
  >
119
101
  <div class="grid-stack-item-content">
120
- {#key context}
121
- <Chart
122
- chartRecord={chart}
123
- onChartDeleted={async () => await loadCharts()}
124
- onChartEdited={async () => await loadCharts()}
125
- {context}
126
- {...props}
127
- />
128
- {/key}
102
+ <Chart
103
+ chartRecord={chart}
104
+ onChartDeleted={async () => await loadCharts()}
105
+ onChartEdited={async () => await loadCharts()}
106
+ {utils}
107
+ {...props}
108
+ />
129
109
  </div>
130
110
  </div>
131
111
  {/each}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobb-js/lobb-ext-reports",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -38,13 +38,15 @@
38
38
  "chart.js": "^4.4.8",
39
39
  "gridstack": "^12.6.0",
40
40
  "hono": "^4.7.0",
41
+ "html-to-image": "^1.11.13",
42
+ "jspdf": "^4.2.1",
41
43
  "lodash-es": "^4.17.21",
42
44
  "openapi-types": "^12.1.3"
43
45
  },
44
46
  "devDependencies": {
45
47
  "@faker-js/faker": "^9.6.0",
46
48
  "@playwright/test": "^1.58.2",
47
- "@lobb-js/studio": "^0.18.1",
49
+ "@lobb-js/studio": "^0.19.1",
48
50
  "@lucide/svelte": "^0.563.1",
49
51
  "@sveltejs/adapter-node": "^5.5.4",
50
52
  "@sveltejs/kit": "^2.55.0",