@lobb-js/lobb-ext-reports 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -3
- package/dist/lib/components/gridStack.svelte +16 -4
- package/dist/lib/components/pages/reports/components/chart.svelte +64 -39
- package/dist/lib/components/pages/reports/components/charts/table.svelte +0 -1
- package/dist/lib/components/pages/reports/components/report.svelte +115 -102
- package/dist/lib/components/pages/reports/index.svelte +5 -11
- package/dist/lib/components/pages/shared_report/index.svelte +109 -0
- package/dist/lib/components/pages/shared_report/index.svelte.d.ts +14 -0
- package/dist/lib/components/reportBody.svelte +119 -0
- package/dist/lib/components/reportBody.svelte.d.ts +14 -0
- package/extensions/reports/index.ts +33 -5
- package/extensions/reports/studio/index.ts +10 -3
- package/extensions/reports/studio/lib/components/gridStack.svelte +16 -4
- package/extensions/reports/studio/lib/components/pages/reports/components/chart.svelte +64 -39
- package/extensions/reports/studio/lib/components/pages/reports/components/charts/table.svelte +0 -1
- package/extensions/reports/studio/lib/components/pages/reports/components/report.svelte +115 -102
- package/extensions/reports/studio/lib/components/pages/reports/index.svelte +5 -11
- package/extensions/reports/studio/lib/components/pages/shared_report/index.svelte +109 -0
- package/extensions/reports/studio/lib/components/reportBody.svelte +119 -0
- package/extensions/reports/workflows.ts +73 -0
- package/package.json +3 -3
- package/extensions/reports/collectionRoutes.ts +0 -10
- package/extensions/reports/controllers/chartsController.ts +0 -78
|
@@ -1,38 +1,31 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from "svelte";
|
|
3
|
-
import
|
|
4
|
-
import Chart from "./chart.svelte";
|
|
3
|
+
import ReportBody from "../../../reportBody.svelte";
|
|
5
4
|
|
|
6
|
-
const { reportId, utils
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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,124 @@
|
|
|
62
51
|
});
|
|
63
52
|
}
|
|
64
53
|
|
|
65
|
-
|
|
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
|
+
const event = {
|
|
66
|
+
reportId: String(reportId),
|
|
67
|
+
report,
|
|
68
|
+
handled: false,
|
|
69
|
+
};
|
|
70
|
+
await utils.emitEvent("reports.share", event);
|
|
71
|
+
if (event.handled) return;
|
|
72
|
+
|
|
73
|
+
const basePermissions: Record<string, any> = {
|
|
74
|
+
reports_dashboards: { read: { filter: { id: Number(reportId) } } },
|
|
75
|
+
reports_charts: { read: { filter: { report_id: Number(reportId) } } },
|
|
76
|
+
};
|
|
77
|
+
const shareReadAccess = (utils.ctx.meta?.extensions?.reports?.shareReadAccess ?? {}) as Record<string, any>;
|
|
78
|
+
const extraPermissions: Record<string, any> = {};
|
|
79
|
+
for (const [collectionName, readGrant] of Object.entries(shareReadAccess)) {
|
|
80
|
+
extraPermissions[collectionName] = { read: readGrant };
|
|
81
|
+
}
|
|
82
|
+
const permissions = { ...basePermissions, ...extraPermissions };
|
|
83
|
+
const response = await utils.lobb.createOne("auth_shares", {
|
|
84
|
+
label: `Share for: ${report?.name ?? "report"}`,
|
|
85
|
+
permissions: JSON.stringify(permissions),
|
|
86
|
+
expires_in_seconds: 7 * 24 * 60 * 60,
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const body = await response.json().catch(() => null);
|
|
90
|
+
utils.toast.error(body?.message ?? "Failed to create share");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const body = await response.json();
|
|
94
|
+
const url = `${window.location.origin}/studio/public/reports/shared_report?report_id=${reportId}&share_token=${body.data.token}`;
|
|
95
|
+
await navigator.clipboard.writeText(url);
|
|
96
|
+
utils.toast.success("Share link copied to clipboard");
|
|
97
|
+
} catch (err) {
|
|
98
|
+
utils.toast.error(err instanceof Error ? err.message : "Failed to create share");
|
|
99
|
+
} finally {
|
|
100
|
+
sharing = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
onMount(() => {
|
|
105
|
+
checkSharePermission();
|
|
106
|
+
});
|
|
66
107
|
</script>
|
|
67
108
|
|
|
68
109
|
<div class="report-layout">
|
|
69
|
-
|
|
70
|
-
<div class="flex
|
|
71
|
-
<
|
|
72
|
-
<div class="flex flex-col
|
|
73
|
-
<
|
|
74
|
-
<div class="text-xs text-muted-foreground">
|
|
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>
|
|
110
|
+
<div class="flex shrink-0 items-center justify-between gap-2 border-b bg-background p-2">
|
|
111
|
+
<div class="flex gap-2">
|
|
112
|
+
<div class="mt-1"><SidebarTrigger /></div>
|
|
113
|
+
<div class="flex flex-col justify-center">
|
|
114
|
+
<h2 class="font-medium text-primary">{report?.name ?? ""}</h2>
|
|
115
|
+
<div class="text-xs text-muted-foreground">{report?.description ?? ""}</div>
|
|
83
116
|
</div>
|
|
84
117
|
</div>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<h2 class="font-medium text-primary">{report?.name}</h2>
|
|
91
|
-
<div class="text-xs text-muted-foreground">{report?.description}</div>
|
|
118
|
+
<div class="flex items-center gap-2 self-end">
|
|
119
|
+
{#if saving}
|
|
120
|
+
<div class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
121
|
+
<Icons.LoaderCircle class="animate-spin" size={12} />
|
|
122
|
+
Saving...
|
|
92
123
|
</div>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
{:else if editable}
|
|
125
|
+
<div class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
126
|
+
<div class="h-1.5 w-1.5 rounded-full bg-green-500"></div>
|
|
127
|
+
Layout editable
|
|
128
|
+
</div>
|
|
129
|
+
{:else}
|
|
130
|
+
<div class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
131
|
+
<div class="h-1.5 w-1.5 rounded-full bg-muted-foreground/40"></div>
|
|
132
|
+
Expand window to rearrange
|
|
133
|
+
</div>
|
|
134
|
+
{/if}
|
|
135
|
+
{#if canShare}
|
|
136
|
+
<Button
|
|
137
|
+
variant="outline"
|
|
138
|
+
class="h-7 px-3 text-xs font-normal"
|
|
139
|
+
Icon={sharing ? Icons.LoaderCircle : Icons.Share2}
|
|
140
|
+
disabled={sharing}
|
|
141
|
+
onclick={handleShare}
|
|
142
|
+
>
|
|
143
|
+
{sharing ? "Sharing..." : "Share"}
|
|
144
|
+
</Button>
|
|
145
|
+
{/if}
|
|
146
|
+
{#if report}
|
|
111
147
|
<CreateDetailViewButton
|
|
112
148
|
collectionName="reports_charts"
|
|
113
149
|
values={{ report_id: { id: reportId, name: report.name } }}
|
|
114
150
|
variant="default"
|
|
115
151
|
class="h-7 px-3 text-xs font-normal"
|
|
116
152
|
Icon={Icons.Plus}
|
|
117
|
-
onSuccessfullSave={async () => await
|
|
153
|
+
onSuccessfullSave={async () => await body?.reload()}
|
|
118
154
|
>
|
|
119
155
|
Create chart
|
|
120
156
|
</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
157
|
{/if}
|
|
157
158
|
</div>
|
|
158
|
-
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="charts-export-container overflow-auto p-2">
|
|
162
|
+
<ReportBody
|
|
163
|
+
{reportId}
|
|
164
|
+
{utils}
|
|
165
|
+
allowEdit
|
|
166
|
+
onLayoutChange={handleLayoutChange}
|
|
167
|
+
onChartClick={handleChartClick}
|
|
168
|
+
onReportLoaded={handleReportLoaded}
|
|
169
|
+
onReady={handleReady}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
159
172
|
</div>
|
|
160
173
|
|
|
161
174
|
<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
|
-
let extensionPagePath = "/studio/extensions/reports/
|
|
9
|
-
let reportId: string | null = $derived(
|
|
7
|
+
let extensionPagePath = "/studio/extensions/reports/dashboards";
|
|
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.
|
|
38
|
-
`${extensionPagePath}/${report.id}`,
|
|
39
|
-
);
|
|
36
|
+
utils.goto(`${extensionPagePath}/${report.id}`);
|
|
40
37
|
},
|
|
41
38
|
meta: {
|
|
42
39
|
reportId: report.id,
|
|
@@ -57,12 +54,9 @@
|
|
|
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
|
-
<Sidebar title="
|
|
59
|
+
<Sidebar title="Dashboards" data={sideBarData}>
|
|
66
60
|
{#snippet belowSearch()}
|
|
67
61
|
<div class="p-2">
|
|
68
62
|
<CreateDetailViewButton
|
|
@@ -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.
|
|
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,119 @@
|
|
|
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
|
+
allowEdit?: boolean;
|
|
11
|
+
showChartHeaders?: boolean;
|
|
12
|
+
onLayoutChange?: (changes: any[]) => void;
|
|
13
|
+
onChartClick?: (event: any, elements: any[], chart: any) => void;
|
|
14
|
+
onReportLoaded?: (report: any) => void;
|
|
15
|
+
onReady?: (api: { reload: () => Promise<void> }) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
reportId,
|
|
20
|
+
editable = false,
|
|
21
|
+
allowEdit = false,
|
|
22
|
+
showChartHeaders = true,
|
|
23
|
+
onLayoutChange,
|
|
24
|
+
onChartClick,
|
|
25
|
+
onReportLoaded,
|
|
26
|
+
onReady,
|
|
27
|
+
...props
|
|
28
|
+
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
const utils = props.utils;
|
|
31
|
+
|
|
32
|
+
const { Icons } = utils.components;
|
|
33
|
+
|
|
34
|
+
let report: any = $state(null);
|
|
35
|
+
let charts: any[] = $state([]);
|
|
36
|
+
let loading = $state(true);
|
|
37
|
+
|
|
38
|
+
async function loadCharts({ showLoading = true } = {}) {
|
|
39
|
+
if (showLoading) loading = true;
|
|
40
|
+
const reportsRes = await utils.lobb.findAll("reports_dashboards", {
|
|
41
|
+
sort: "id",
|
|
42
|
+
filter: { id: reportId },
|
|
43
|
+
});
|
|
44
|
+
report = (await reportsRes.json()).data[0];
|
|
45
|
+
if (onReportLoaded) onReportLoaded(report);
|
|
46
|
+
if (!report) {
|
|
47
|
+
if (showLoading) loading = false;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const chartsRes = await utils.lobb.findAll("reports_charts", {
|
|
52
|
+
filter: { report_id: report.id },
|
|
53
|
+
});
|
|
54
|
+
charts = (await chartsRes.json()).data;
|
|
55
|
+
if (showLoading) loading = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getChartGridSize(chart: any): Record<string, unknown> {
|
|
59
|
+
return {
|
|
60
|
+
"gs-w": chart.col_span ?? 6,
|
|
61
|
+
"gs-h": chart.row_span ?? 2,
|
|
62
|
+
"gs-auto-position": "true",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onMount(async () => {
|
|
67
|
+
await loadCharts();
|
|
68
|
+
if (onReady) onReady({ reload: () => loadCharts() });
|
|
69
|
+
});
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
{#if loading}
|
|
73
|
+
<div class="flex h-full w-full flex-col items-center justify-center gap-4">
|
|
74
|
+
<Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
|
|
75
|
+
<div class="flex flex-col items-center justify-center">
|
|
76
|
+
<div class="text-muted-foreground">Loading the dashboard...</div>
|
|
77
|
+
<div class="text-xs text-muted-foreground">Loading all the charts of the selected dashboard</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
{:else if !report}
|
|
81
|
+
<div class="flex h-full w-full flex-col items-center justify-center gap-4">
|
|
82
|
+
<Icons.CircleSlash2 class="opacity-50" size="50" />
|
|
83
|
+
<div class="flex flex-col items-center justify-center">
|
|
84
|
+
<div class="text-muted-foreground">Report not found</div>
|
|
85
|
+
<div class="text-xs text-muted-foreground">The report you're looking for doesn't exist or couldn't be loaded</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
{:else if charts.length === 0}
|
|
89
|
+
<div class="flex w-full flex-col items-center justify-center gap-4 p-10 text-muted-foreground">
|
|
90
|
+
<Icons.CircleSlash2 class="opacity-50" size="50" />
|
|
91
|
+
<div class="flex flex-col items-center justify-center">
|
|
92
|
+
<div>No charts available</div>
|
|
93
|
+
<div class="text-xs">Create a new chart to fill this report page</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
{:else}
|
|
97
|
+
{#key charts}
|
|
98
|
+
<GridStackComponent {editable} {allowEdit} {onLayoutChange}>
|
|
99
|
+
{#each [...charts].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) as chart (chart.id)}
|
|
100
|
+
<div
|
|
101
|
+
class="grid-stack-item"
|
|
102
|
+
data-gs-id={chart.id}
|
|
103
|
+
{...getChartGridSize(chart)}
|
|
104
|
+
>
|
|
105
|
+
<div class="grid-stack-item-content">
|
|
106
|
+
<Chart
|
|
107
|
+
chartRecord={chart}
|
|
108
|
+
showHeader={showChartHeaders}
|
|
109
|
+
onChartDeleted={async () => await loadCharts()}
|
|
110
|
+
onChartEdited={async () => await loadCharts()}
|
|
111
|
+
{onChartClick}
|
|
112
|
+
{utils}
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
{/each}
|
|
117
|
+
</GridStackComponent>
|
|
118
|
+
{/key}
|
|
119
|
+
{/if}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Lobb, Workflow } from "@lobb-js/core";
|
|
2
|
+
import type { Context } from "hono";
|
|
3
|
+
|
|
4
|
+
export const workflows: Workflow[] = [
|
|
5
|
+
{
|
|
6
|
+
// Intercept GET /api/collections/reports_charts/:id when ?action=run_query
|
|
7
|
+
// is present — runs the chart's parsed function and short-circuits the
|
|
8
|
+
// default findOne behaviour by throwing a Response.
|
|
9
|
+
name: "reports_chartsRunQueryHandler",
|
|
10
|
+
eventName: "core.controllers.preFindOne",
|
|
11
|
+
handler: async (input, ctx) => {
|
|
12
|
+
if (input.collectionName !== "reports_charts") return input;
|
|
13
|
+
|
|
14
|
+
const c = input.context as Context;
|
|
15
|
+
const actionQuery = c.req.query("action");
|
|
16
|
+
if (!actionQuery) return input;
|
|
17
|
+
|
|
18
|
+
if (actionQuery !== "run_query") {
|
|
19
|
+
throw new ctx.LobbError({
|
|
20
|
+
code: "BAD_REQUEST",
|
|
21
|
+
message: `The passed (${actionQuery}) action query param is not supported`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const lobb = c.get("lobb") as Lobb;
|
|
26
|
+
const reportRecordId = input.id;
|
|
27
|
+
const contextParam = c.req.query("context");
|
|
28
|
+
const context = JSON.parse(contextParam ?? "{}");
|
|
29
|
+
|
|
30
|
+
const driver = lobb.databaseService.getDriver();
|
|
31
|
+
const pool = driver.getConnection();
|
|
32
|
+
using client = await pool.connect();
|
|
33
|
+
|
|
34
|
+
const chartRecord = (await lobb.collectionService.findOne({
|
|
35
|
+
collectionName: "reports_charts",
|
|
36
|
+
client,
|
|
37
|
+
id: reportRecordId,
|
|
38
|
+
})).data;
|
|
39
|
+
|
|
40
|
+
if (!chartRecord) {
|
|
41
|
+
throw new ctx.LobbError({
|
|
42
|
+
code: "NOT_FOUND",
|
|
43
|
+
message: `The chart you are looking for does not exist.`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const pendingQueries: Promise<unknown>[] = [];
|
|
48
|
+
|
|
49
|
+
async function query(sql: string, args: Record<string, any>) {
|
|
50
|
+
const promise = client.query(sql, args).then((result) => result.rows);
|
|
51
|
+
pendingQueries.push(promise);
|
|
52
|
+
return await promise;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const chartReturnedValue = await lobb.utils.runParsedFunction(
|
|
57
|
+
chartRecord.chart,
|
|
58
|
+
query,
|
|
59
|
+
context,
|
|
60
|
+
);
|
|
61
|
+
await Promise.allSettled(pendingQueries);
|
|
62
|
+
throw new Response(JSON.stringify(chartReturnedValue), {
|
|
63
|
+
status: 200,
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof Response) throw error;
|
|
68
|
+
await Promise.allSettled(pendingQueries);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/lobb-ext-reports",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"package": "svelte-package --input extensions/reports/studio"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@lobb-js/core": "^0.
|
|
35
|
+
"@lobb-js/core": "^0.33.0",
|
|
36
36
|
"chart.js": "^4.4.8",
|
|
37
37
|
"gridstack": "^12.6.0",
|
|
38
38
|
"hono": "^4.7.0",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@faker-js/faker": "^9.6.0",
|
|
46
|
-
"@lobb-js/studio": "^0.
|
|
46
|
+
"@lobb-js/studio": "^0.31.0",
|
|
47
47
|
"@lucide/svelte": "^0.563.1",
|
|
48
48
|
"@sveltejs/adapter-node": "^5.5.4",
|
|
49
49
|
"@sveltejs/kit": "^2.60.1",
|