@lobb-js/lobb-ext-reports 0.11.0 → 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.
- package/README.md +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/components/pages/reports/components/chart.svelte +41 -15
- package/dist/lib/components/pages/reports/components/charts/table.svelte +0 -1
- package/dist/lib/components/pages/reports/components/report.svelte +107 -102
- package/dist/lib/components/pages/reports/index.svelte +3 -9
- 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 +114 -0
- package/dist/lib/components/reportBody.svelte.d.ts +14 -0
- package/extensions/reports/index.ts +2 -2
- package/extensions/reports/studio/index.ts +5 -0
- package/extensions/reports/studio/lib/components/pages/reports/components/chart.svelte +41 -15
- 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 +107 -102
- package/extensions/reports/studio/lib/components/pages/reports/index.svelte +3 -9
- package/extensions/reports/studio/lib/components/pages/shared_report/index.svelte +109 -0
- package/extensions/reports/studio/lib/components/reportBody.svelte +114 -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
package/README.md
CHANGED
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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">
|
|
@@ -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,116 @@
|
|
|
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
|
+
// 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
|
-
|
|
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>
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,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.
|
|
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 {};
|