@lobb-js/lobb-ext-reports 0.10.4 → 0.11.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.
Files changed (30) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +5 -0
  3. package/dist/lib/components/pages/reports/components/chart.svelte +41 -15
  4. package/dist/lib/components/pages/reports/components/charts/table.svelte +0 -1
  5. package/dist/lib/components/pages/reports/components/report.svelte +107 -102
  6. package/dist/lib/components/pages/reports/index.svelte +3 -9
  7. package/dist/lib/components/pages/shared_report/index.svelte +109 -0
  8. package/dist/lib/components/pages/shared_report/index.svelte.d.ts +14 -0
  9. package/dist/lib/components/reportBody.svelte +114 -0
  10. package/dist/lib/components/reportBody.svelte.d.ts +14 -0
  11. package/extensions/reports/index.ts +2 -2
  12. package/extensions/reports/studio/index.ts +5 -0
  13. package/extensions/reports/studio/lib/components/pages/reports/components/chart.svelte +41 -15
  14. package/extensions/reports/studio/lib/components/pages/reports/components/charts/table.svelte +0 -1
  15. package/extensions/reports/studio/lib/components/pages/reports/components/report.svelte +107 -102
  16. package/extensions/reports/studio/lib/components/pages/reports/index.svelte +3 -9
  17. package/extensions/reports/studio/lib/components/pages/shared_report/index.svelte +109 -0
  18. package/extensions/reports/studio/lib/components/reportBody.svelte +114 -0
  19. package/extensions/reports/workflows.ts +73 -0
  20. package/package.json +4 -7
  21. package/dist/tests/analytics.spec.d.ts +0 -1
  22. package/dist/tests/analytics.spec.js +0 -17
  23. package/dist/tests/package.json +0 -1
  24. package/dist/tests/playwright.config.cjs +0 -27
  25. package/dist/tests/playwright.config.d.cts +0 -2
  26. package/extensions/reports/collectionRoutes.ts +0 -10
  27. package/extensions/reports/controllers/chartsController.ts +0 -78
  28. package/extensions/reports/studio/tests/analytics.spec.ts +0 -26
  29. package/extensions/reports/studio/tests/package.json +0 -1
  30. package/extensions/reports/studio/tests/playwright.config.cjs +0 -27
package/README.md CHANGED
@@ -1 +1,2 @@
1
1
  # @lobb-js/lobb-ext-reports
2
+
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import Reports from "./lib/components/pages/reports/index.svelte";
2
+ import SharedReport from "./lib/components/pages/shared_report/index.svelte";
2
3
  import QueryAiButton from "./lib/components/dv_fields_buttons/query_ai_button/index.svelte";
3
4
  export default function extension(utils) {
4
5
  return {
@@ -6,6 +7,9 @@ export default function extension(utils) {
6
7
  components: {
7
8
  "dvFields.topRight.reports_charts.query": QueryAiButton,
8
9
  "pages.analytics": Reports,
10
+ // Lives at /studio/public/reports/shared_report — no auth required, the
11
+ // page itself swaps the LobbClient bearer to the share_token in the URL.
12
+ "publicPages.shared_report": SharedReport,
9
13
  },
10
14
  dashboardNavs: {
11
15
  middle: [
@@ -13,6 +17,7 @@ export default function extension(utils) {
13
17
  href: "/studio/extensions/reports/analytics",
14
18
  icon: utils.components.Icons.ChartNoAxesCombined,
15
19
  label: "Analytics",
20
+ represents: "reports_dashboards",
16
21
  },
17
22
  ],
18
23
  },
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { onMount } from "svelte";
2
3
  import type { ExtensionProps } from "@lobb-js/studio";
3
4
  import Table from "./charts/table.svelte";
4
5
  import ChartJs from "./charts/chartJs.svelte";
@@ -30,6 +31,27 @@
30
31
  const { UpdateDetailViewButton, Button, Tooltip } = utils.components;
31
32
  const icons = utils.components.Icons;
32
33
 
34
+ // Button visibility comes from the caller's actual permissions on
35
+ // reports_charts. A logged-in admin sees edit + delete; a viewer without
36
+ // those grants (including a share-token recipient) doesn't see either.
37
+ let canUpdate = $state(false);
38
+ let canDelete = $state(false);
39
+
40
+ onMount(async () => {
41
+ const [updateAllowed, deleteAllowed] = await Promise.all([
42
+ utils.emitEvent("auth.canAccess", {
43
+ collection: "reports_charts",
44
+ action: "update",
45
+ }),
46
+ utils.emitEvent("auth.canAccess", {
47
+ collection: "reports_charts",
48
+ action: "delete",
49
+ }),
50
+ ]);
51
+ canUpdate = updateAllowed === true;
52
+ canDelete = deleteAllowed === true;
53
+ });
54
+
33
55
  function findCustomChartComponent(type: string): any {
34
56
  const extensions = utils.ctx?.extensions ?? {};
35
57
  for (const ext of Object.values(extensions) as any[]) {
@@ -89,21 +111,25 @@
89
111
  {/if}
90
112
  </div>
91
113
  <div class="flex">
92
- <UpdateDetailViewButton
93
- collectionName="reports_charts"
94
- recordId={chartRecord.id}
95
- variant="ghost"
96
- class="h-7 w-7 text-muted-foreground hover:bg-transparent"
97
- Icon={icons.Pencil}
98
- onSuccessfullSave={handleEditOnSuccessfull}
99
- ></UpdateDetailViewButton>
100
- <Button
101
- class="h-6 w-6 text-muted-foreground hover:bg-transparent"
102
- variant="ghost"
103
- size="icon"
104
- Icon={icons.Trash}
105
- onclick={handleChartDelete}
106
- ></Button>
114
+ {#if canUpdate}
115
+ <UpdateDetailViewButton
116
+ collectionName="reports_charts"
117
+ recordId={chartRecord.id}
118
+ variant="ghost"
119
+ class="h-7 w-7 text-muted-foreground hover:bg-transparent"
120
+ Icon={icons.Pencil}
121
+ onSuccessfullSave={handleEditOnSuccessfull}
122
+ ></UpdateDetailViewButton>
123
+ {/if}
124
+ {#if canDelete}
125
+ <Button
126
+ class="h-6 w-6 text-muted-foreground hover:bg-transparent"
127
+ variant="ghost"
128
+ size="icon"
129
+ Icon={icons.Trash}
130
+ onclick={handleChartDelete}
131
+ ></Button>
132
+ {/if}
107
133
  </div>
108
134
  </div>
109
135
  <div class="flex min-h-0 items-center justify-center overflow-hidden">
@@ -28,7 +28,6 @@
28
28
  localSorting={true}
29
29
  showLastRowBorder={true}
30
30
  showLastColumnBorder={true}
31
- unifiedBgColor="bg-background"
32
31
  ></Table>
33
32
  {:else}
34
33
  <div class="flex justify-center items-center h-full">No Results</div>
@@ -1,38 +1,31 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from "svelte";
3
- import GridStackComponent from "../../../gridStack.svelte";
4
- import Chart from "./chart.svelte";
3
+ import ReportBody from "../../../reportBody.svelte";
5
4
 
6
- const { reportId, utils, ...props }: { reportId: string; utils: any; [key: string]: any } = $props();
5
+ const { reportId, utils }: { reportId: string; utils: any } = $props();
7
6
  const {
8
7
  SidebarTrigger,
9
8
  CreateDetailViewButton,
9
+ Button,
10
10
  Icons,
11
11
  } = utils.components;
12
12
 
13
13
  let report: any = $state(null);
14
- let charts: any[] = $state([]);
15
- let loading = $state(true);
16
14
  let saving = $state(false);
17
15
  let editable = $state(false);
16
+ let canShare = $state(false);
17
+ let sharing = $state(false);
18
+ // Handle captured from ReportBody via its onReady callback — lets us
19
+ // trigger a reload of charts after creating a new one without duplicating
20
+ // the fetch logic here.
21
+ let body: { reload: () => Promise<void> } | undefined = $state();
18
22
 
19
- async function loadCharts({ showLoading = true } = {}) {
20
- if (showLoading) loading = true;
21
- const reportsRes = await utils.lobb.findAll("reports_dashboards", {
22
- sort: "id",
23
- filter: { id: reportId },
24
- });
25
- report = (await reportsRes.json()).data[0];
26
- if (!report) {
27
- if (showLoading) loading = false;
28
- return;
29
- }
23
+ function handleReportLoaded(r: any) {
24
+ report = r;
25
+ }
30
26
 
31
- const chartsRes = await utils.lobb.findAll("reports_charts", {
32
- filter: { report_id: report.id },
33
- });
34
- charts = (await chartsRes.json()).data;
35
- if (showLoading) loading = false;
27
+ function handleReady(api: { reload: () => Promise<void> }) {
28
+ body = api;
36
29
  }
37
30
 
38
31
  async function handleLayoutChange(changes: { id: string; order: number; w: number; h: number }[]) {
@@ -47,10 +40,6 @@
47
40
  saving = false;
48
41
  }
49
42
 
50
- function getChartGridSize(chart: any): Record<string, unknown> {
51
- return { "gs-w": chart.col_span ?? 6, "gs-h": chart.row_span ?? 2, "gs-auto-position": "true" };
52
- }
53
-
54
43
  function handleChartClick(_event: any, elements: any[], chart: any) {
55
44
  if (!elements.length) return;
56
45
  const { index, datasetIndex } = elements[0];
@@ -62,100 +51,116 @@
62
51
  });
63
52
  }
64
53
 
65
- onMount(() => loadCharts());
54
+ async function checkSharePermission() {
55
+ const allowed = await utils.emitEvent("auth.canAccess", {
56
+ collection: "auth_shares",
57
+ action: "create",
58
+ });
59
+ canShare = allowed === true;
60
+ }
61
+
62
+ async function handleShare() {
63
+ sharing = true;
64
+ try {
65
+ // Snapshot grants exactly the read access a recipient needs to view
66
+ // this one report — the dashboard row and the charts that belong to it.
67
+ const permissions = {
68
+ reports_dashboards: { read: { filter: { id: Number(reportId) } } },
69
+ reports_charts: { read: { filter: { report_id: Number(reportId) } } },
70
+ // TODO: the above ones are ok. the below ones are specific to only this risk project
71
+ // can you please add them using a different way
72
+ risk_config: true,
73
+ risks: true,
74
+ };
75
+ const response = await utils.lobb.createOne("auth_shares", {
76
+ label: `Share for: ${report?.name ?? "report"}`,
77
+ permissions: JSON.stringify(permissions),
78
+ expires_in_seconds: 7 * 24 * 60 * 60,
79
+ });
80
+ if (!response.ok) {
81
+ const body = await response.json().catch(() => null);
82
+ utils.toast.error(body?.message ?? "Failed to create share");
83
+ return;
84
+ }
85
+ const body = await response.json();
86
+ const url = `${window.location.origin}/studio/public/reports/shared_report?report_id=${reportId}&share_token=${body.data.token}`;
87
+ await navigator.clipboard.writeText(url);
88
+ utils.toast.success("Share link copied to clipboard");
89
+ } catch (err) {
90
+ utils.toast.error(err instanceof Error ? err.message : "Failed to create share");
91
+ } finally {
92
+ sharing = false;
93
+ }
94
+ }
95
+
96
+ onMount(() => {
97
+ checkSharePermission();
98
+ });
66
99
  </script>
67
100
 
68
101
  <div class="report-layout">
69
- {#if loading}
70
- <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
71
- <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
72
- <div class="flex flex-col items-center justify-center">
73
- <div class="text-muted-foreground">Loading the dashboard...</div>
74
- <div class="text-xs text-muted-foreground">Loading all the charts of the selected dashboard</div>
75
- </div>
76
- </div>
77
- {:else if !report}
78
- <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
79
- <Icons.CircleSlash2 class="opacity-50" size="50" />
80
- <div class="flex flex-col items-center justify-center">
81
- <div class="text-muted-foreground">Report not found</div>
82
- <div class="text-xs text-muted-foreground">The report you're looking for doesn't exist or couldn't be loaded</div>
102
+ <div class="flex shrink-0 items-center justify-between gap-2 border-b bg-background p-2">
103
+ <div class="flex gap-2">
104
+ <div class="mt-1"><SidebarTrigger /></div>
105
+ <div class="flex flex-col justify-center">
106
+ <h2 class="font-medium text-primary">{report?.name ?? ""}</h2>
107
+ <div class="text-xs text-muted-foreground">{report?.description ?? ""}</div>
83
108
  </div>
84
109
  </div>
85
- {:else}
86
- <div class="flex shrink-0 items-center justify-between gap-2 border-b bg-background p-2">
87
- <div class="flex gap-2">
88
- <div class="mt-1"><SidebarTrigger /></div>
89
- <div class="flex flex-col justify-center">
90
- <h2 class="font-medium text-primary">{report?.name}</h2>
91
- <div class="text-xs text-muted-foreground">{report?.description}</div>
110
+ <div class="flex items-center gap-2 self-end">
111
+ {#if saving}
112
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
113
+ <Icons.LoaderCircle class="animate-spin" size={12} />
114
+ Saving...
92
115
  </div>
93
- </div>
94
- <div class="flex items-center gap-2 self-end">
95
- {#if saving}
96
- <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
97
- <Icons.LoaderCircle class="animate-spin" size={12} />
98
- Saving...
99
- </div>
100
- {:else if editable}
101
- <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
102
- <div class="h-1.5 w-1.5 rounded-full bg-green-500"></div>
103
- Layout editable
104
- </div>
105
- {:else}
106
- <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
107
- <div class="h-1.5 w-1.5 rounded-full bg-muted-foreground/40"></div>
108
- Expand window to rearrange
109
- </div>
110
- {/if}
116
+ {:else if editable}
117
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
118
+ <div class="h-1.5 w-1.5 rounded-full bg-green-500"></div>
119
+ Layout editable
120
+ </div>
121
+ {:else}
122
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
123
+ <div class="h-1.5 w-1.5 rounded-full bg-muted-foreground/40"></div>
124
+ Expand window to rearrange
125
+ </div>
126
+ {/if}
127
+ {#if canShare}
128
+ <Button
129
+ variant="outline"
130
+ class="h-7 px-3 text-xs font-normal"
131
+ Icon={sharing ? Icons.LoaderCircle : Icons.Share2}
132
+ disabled={sharing}
133
+ onclick={handleShare}
134
+ >
135
+ {sharing ? "Sharing..." : "Share"}
136
+ </Button>
137
+ {/if}
138
+ {#if report}
111
139
  <CreateDetailViewButton
112
140
  collectionName="reports_charts"
113
141
  values={{ report_id: { id: reportId, name: report.name } }}
114
142
  variant="default"
115
143
  class="h-7 px-3 text-xs font-normal"
116
144
  Icon={Icons.Plus}
117
- onSuccessfullSave={async () => await loadCharts()}
145
+ onSuccessfullSave={async () => await body?.reload()}
118
146
  >
119
147
  Create chart
120
148
  </CreateDetailViewButton>
121
- </div>
122
- </div>
123
-
124
- <div class="charts-export-container overflow-auto p-2">
125
- {#if charts.length === 0}
126
- <div class="flex w-full flex-col items-center justify-center gap-4 p-10 text-muted-foreground">
127
- <Icons.CircleSlash2 class="opacity-50" size="50" />
128
- <div class="flex flex-col items-center justify-center">
129
- <div>No charts available</div>
130
- <div class="text-xs">Create a new chart to fill this report page</div>
131
- </div>
132
- </div>
133
- {:else}
134
- {#key charts}
135
- <GridStackComponent bind:editable onLayoutChange={handleLayoutChange}>
136
- {#each [...charts].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) as chart (chart.id)}
137
- <div
138
- class="grid-stack-item"
139
- data-gs-id={chart.id}
140
- {...getChartGridSize(chart)}
141
- >
142
- <div class="grid-stack-item-content">
143
- <Chart
144
- chartRecord={chart}
145
- onChartDeleted={async () => await loadCharts()}
146
- onChartEdited={async () => await loadCharts()}
147
- onChartClick={handleChartClick}
148
- {utils}
149
- {...props}
150
- />
151
- </div>
152
- </div>
153
- {/each}
154
- </GridStackComponent>
155
- {/key}
156
149
  {/if}
157
150
  </div>
158
- {/if}
151
+ </div>
152
+
153
+ <div class="charts-export-container overflow-auto p-2">
154
+ <ReportBody
155
+ {reportId}
156
+ {utils}
157
+ editable
158
+ onLayoutChange={handleLayoutChange}
159
+ onChartClick={handleChartClick}
160
+ onReportLoaded={handleReportLoaded}
161
+ onReady={handleReady}
162
+ />
163
+ </div>
159
164
  </div>
160
165
 
161
166
  <style>
@@ -1,12 +1,11 @@
1
1
  <script lang="ts">
2
2
  import type { ExtensionProps } from "@lobb-js/studio";
3
- import type { Location } from "@wjfe/n-savant";
4
3
  import Report from "./components/report.svelte";
5
4
 
6
5
  const { utils, ...props }: ExtensionProps = $props();
7
6
 
8
7
  let extensionPagePath = "/studio/extensions/reports/analytics";
9
- let reportId: string | null = $derived(getReportID(utils.location));
8
+ let reportId: string | null = $derived(utils.page.url.pathname.split("/")[5] ?? null);
10
9
  let sideBarData: any = $state(null);
11
10
  const {
12
11
  CreateDetailViewButton,
@@ -34,9 +33,7 @@
34
33
  name: report.name,
35
34
  icon: icons.FileChartColumn,
36
35
  onclick: () => {
37
- utils.location.navigate(
38
- `${extensionPagePath}/${report.id}`,
39
- );
36
+ utils.goto(`${extensionPagePath}/${report.id}`);
40
37
  },
41
38
  meta: {
42
39
  reportId: report.id,
@@ -57,9 +54,6 @@
57
54
  }
58
55
  }
59
56
 
60
- function getReportID(location: Location) {
61
- return location.url.pathname.split("/")[5];
62
- }
63
57
  </script>
64
58
 
65
59
  <Sidebar title="Reports" data={sideBarData}>
@@ -129,7 +123,7 @@
129
123
  class="h-7 px-3 text-xs font-normal"
130
124
  Icon={icons.Plus}
131
125
  onSuccessfullSave={async (record: any) =>
132
- utils.location.navigate(`?report=${record.id}`)}
126
+ utils.goto(`?report=${record.id}`)}
133
127
  >
134
128
  Create a report
135
129
  </CreateDetailViewButton>
@@ -0,0 +1,109 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import ReportBody from "../../reportBody.svelte";
4
+
5
+ const { utils }: { utils: any } = $props();
6
+ const { Icons } = utils.components;
7
+
8
+ // Pull share_token and report_id from the URL. The token is what
9
+ // authorises every request below — we install it as the LobbClient's
10
+ // Authorization header so the existing CRUD helpers work transparently.
11
+ // The report_id is what we render. Both come from the link minted by
12
+ // the "Share" button on the gated report view.
13
+ const params = new URLSearchParams(window.location.search);
14
+ const shareToken = params.get("share_token");
15
+ const reportId = params.get("report_id");
16
+
17
+ let ready = $state(false);
18
+ let report: any = $state(null);
19
+ let error: string | null = $state(null);
20
+
21
+ function handleReportLoaded(r: any) {
22
+ report = r;
23
+ if (!r) error = "The shared report could not be found.";
24
+ }
25
+
26
+ onMount(async () => {
27
+ if (!shareToken || !reportId) {
28
+ error = "This share link is missing required parameters.";
29
+ ready = true;
30
+ return;
31
+ }
32
+
33
+ // Swap the LobbClient's bearer to the share token for the duration
34
+ // of this page. The studio shell already short-circuited the auth
35
+ // gate for /studio/public/*, so there's no session header to clobber.
36
+ utils.lobb.setHeaders({ Authorization: `Bearer ${shareToken}` });
37
+
38
+ // Populate ctx.extensions.auth.permissions from the share's snapshot
39
+ // so client-side gates (auth.canAccess) match the server's enforcement.
40
+ // Reuses the same /me endpoint logged-in users hit — for a share
41
+ // bearer, meAlias short-circuits with { data: null, permissions }.
42
+ try {
43
+ const meRes = await utils.lobb.findOne("auth_users", "me");
44
+ if (meRes.ok) {
45
+ const meBody = await meRes.json();
46
+ if (!utils.ctx.extensions.auth) utils.ctx.extensions.auth = {};
47
+ utils.ctx.extensions.auth.permissions = meBody.permissions;
48
+ utils.ctx.extensions.auth.user = meBody.data;
49
+ }
50
+ } catch {
51
+ // Soft-fail — the page will still render, UI gates just default to
52
+ // hidden, and the server-side enforcement is the real safety net.
53
+ }
54
+
55
+ ready = true;
56
+ });
57
+ </script>
58
+
59
+ <div class="shared-report-layout">
60
+ {#if !ready}
61
+ <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
62
+ <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
63
+ <div class="flex flex-col items-center justify-center">
64
+ <div class="text-muted-foreground">Loading the shared report...</div>
65
+ </div>
66
+ </div>
67
+ {:else if error}
68
+ <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
69
+ <Icons.CircleSlash2 class="opacity-50" size="50" />
70
+ <div class="flex flex-col items-center justify-center">
71
+ <div class="text-muted-foreground">Unavailable</div>
72
+ <div class="text-xs text-muted-foreground">{error}</div>
73
+ </div>
74
+ </div>
75
+ {:else}
76
+ <div class="flex shrink-0 items-center justify-between gap-2 border-b bg-background p-2">
77
+ <div class="flex flex-col justify-center">
78
+ <h2 class="font-medium text-primary">{report?.name ?? ""}</h2>
79
+ <div class="text-xs text-muted-foreground">{report?.description ?? ""}</div>
80
+ </div>
81
+ <div class="text-xs text-muted-foreground">Shared view</div>
82
+ </div>
83
+
84
+ <div class="bg-muted overflow-auto p-2">
85
+ <ReportBody reportId={reportId ?? ""} {utils} onReportLoaded={handleReportLoaded} />
86
+ </div>
87
+ {/if}
88
+
89
+ <!-- Subtle attribution on shared / public pages only. Sits over the
90
+ charts area in the bottom-right; the gated dashboard never renders
91
+ this component so customers' main UI stays unbranded. -->
92
+ <a
93
+ href="https://lobb.app"
94
+ target="_blank"
95
+ rel="noopener"
96
+ class="fixed bottom-2 right-6 text-[10px] text-muted-foreground/60 hover:text-muted-foreground"
97
+ >
98
+ Made with Lobb
99
+ </a>
100
+ </div>
101
+
102
+ <style>
103
+ .shared-report-layout {
104
+ display: grid;
105
+ grid-template-rows: auto 1fr;
106
+ width: 100%;
107
+ height: 100%;
108
+ }
109
+ </style>
@@ -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 IndexProps = typeof __propDef.props;
10
+ export type IndexEvents = typeof __propDef.events;
11
+ export type IndexSlots = typeof __propDef.slots;
12
+ export default class Index extends SvelteComponentTyped<IndexProps, IndexEvents, IndexSlots> {
13
+ }
14
+ export {};
@@ -0,0 +1,114 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import type { ExtensionProps } from "@lobb-js/studio";
4
+ import GridStackComponent from "./gridStack.svelte";
5
+ import Chart from "./pages/reports/components/chart.svelte";
6
+
7
+ interface Props extends ExtensionProps {
8
+ reportId: string | number;
9
+ editable?: boolean;
10
+ onLayoutChange?: (changes: any[]) => void;
11
+ onChartClick?: (event: any, elements: any[], chart: any) => void;
12
+ onReportLoaded?: (report: any) => void;
13
+ onReady?: (api: { reload: () => Promise<void> }) => void;
14
+ }
15
+
16
+ const {
17
+ reportId,
18
+ editable = false,
19
+ onLayoutChange,
20
+ onChartClick,
21
+ onReportLoaded,
22
+ onReady,
23
+ ...props
24
+ }: Props = $props();
25
+
26
+ const utils = props.utils;
27
+
28
+ const { Icons } = utils.components;
29
+
30
+ let report: any = $state(null);
31
+ let charts: any[] = $state([]);
32
+ let loading = $state(true);
33
+
34
+ async function loadCharts({ showLoading = true } = {}) {
35
+ if (showLoading) loading = true;
36
+ const reportsRes = await utils.lobb.findAll("reports_dashboards", {
37
+ sort: "id",
38
+ filter: { id: reportId },
39
+ });
40
+ report = (await reportsRes.json()).data[0];
41
+ if (onReportLoaded) onReportLoaded(report);
42
+ if (!report) {
43
+ if (showLoading) loading = false;
44
+ return;
45
+ }
46
+
47
+ const chartsRes = await utils.lobb.findAll("reports_charts", {
48
+ filter: { report_id: report.id },
49
+ });
50
+ charts = (await chartsRes.json()).data;
51
+ if (showLoading) loading = false;
52
+ }
53
+
54
+ function getChartGridSize(chart: any): Record<string, unknown> {
55
+ return {
56
+ "gs-w": chart.col_span ?? 6,
57
+ "gs-h": chart.row_span ?? 2,
58
+ "gs-auto-position": "true",
59
+ };
60
+ }
61
+
62
+ onMount(async () => {
63
+ await loadCharts();
64
+ if (onReady) onReady({ reload: () => loadCharts() });
65
+ });
66
+ </script>
67
+
68
+ {#if loading}
69
+ <div class="flex h-full w-full flex-col items-center justify-center gap-4">
70
+ <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
71
+ <div class="flex flex-col items-center justify-center">
72
+ <div class="text-muted-foreground">Loading the dashboard...</div>
73
+ <div class="text-xs text-muted-foreground">Loading all the charts of the selected dashboard</div>
74
+ </div>
75
+ </div>
76
+ {:else if !report}
77
+ <div class="flex h-full w-full flex-col items-center justify-center gap-4">
78
+ <Icons.CircleSlash2 class="opacity-50" size="50" />
79
+ <div class="flex flex-col items-center justify-center">
80
+ <div class="text-muted-foreground">Report not found</div>
81
+ <div class="text-xs text-muted-foreground">The report you're looking for doesn't exist or couldn't be loaded</div>
82
+ </div>
83
+ </div>
84
+ {:else 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>
90
+ </div>
91
+ </div>
92
+ {:else}
93
+ {#key charts}
94
+ <GridStackComponent {editable} {onLayoutChange}>
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
+ {...getChartGridSize(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
+ {onChartClick}
107
+ {utils}
108
+ />
109
+ </div>
110
+ </div>
111
+ {/each}
112
+ </GridStackComponent>
113
+ {/key}
114
+ {/if}
@@ -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 ReportBodyProps = typeof __propDef.props;
10
+ export type ReportBodyEvents = typeof __propDef.events;
11
+ export type ReportBodySlots = typeof __propDef.slots;
12
+ export default class ReportBody extends SvelteComponentTyped<ReportBodyProps, ReportBodyEvents, ReportBodySlots> {
13
+ }
14
+ export {};