@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
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
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";
|
|
4
|
+
export { default as ReportBody } from "./lib/components/reportBody.svelte";
|
|
3
5
|
export default function extension(utils) {
|
|
4
6
|
return {
|
|
5
7
|
name: "reports",
|
|
6
8
|
components: {
|
|
7
9
|
"dvFields.topRight.reports_charts.query": QueryAiButton,
|
|
8
|
-
"pages.
|
|
10
|
+
"pages.dashboards": Reports,
|
|
11
|
+
// Lives at /studio/public/reports/shared_report — no auth required, the
|
|
12
|
+
// page itself swaps the LobbClient bearer to the share_token in the URL.
|
|
13
|
+
"publicPages.shared_report": SharedReport,
|
|
9
14
|
},
|
|
10
15
|
dashboardNavs: {
|
|
11
16
|
middle: [
|
|
12
17
|
{
|
|
13
|
-
href: "/studio/extensions/reports/
|
|
18
|
+
href: "/studio/extensions/reports/dashboards",
|
|
14
19
|
icon: utils.components.Icons.ChartNoAxesCombined,
|
|
15
|
-
label: "
|
|
20
|
+
label: "Dashboards",
|
|
21
|
+
represents: "reports_dashboards",
|
|
16
22
|
},
|
|
17
23
|
],
|
|
18
24
|
},
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
column?: number;
|
|
10
10
|
cellHeight?: string;
|
|
11
11
|
editable?: boolean;
|
|
12
|
+
allowEdit?: boolean;
|
|
12
13
|
onLayoutChange?: (layout: LayoutItem[]) => void;
|
|
13
14
|
children: Snippet;
|
|
14
15
|
}
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
column = 12,
|
|
18
19
|
cellHeight = "150px",
|
|
19
20
|
editable = $bindable(false),
|
|
21
|
+
allowEdit = false,
|
|
20
22
|
onLayoutChange,
|
|
21
23
|
children,
|
|
22
24
|
}: Props = $props();
|
|
@@ -29,7 +31,10 @@
|
|
|
29
31
|
let containerWidth = $state(0);
|
|
30
32
|
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
// Edit mode only ever turns on when the parent explicitly opts in.
|
|
35
|
+
// Without allowEdit, container width is irrelevant — the grid stays
|
|
36
|
+
// disabled forever.
|
|
37
|
+
let editMode = $derived(allowEdit && containerWidth >= 1200);
|
|
33
38
|
|
|
34
39
|
// Keep editable in sync so parent can read it
|
|
35
40
|
$effect(() => { editable = editMode; });
|
|
@@ -112,8 +117,10 @@
|
|
|
112
117
|
});
|
|
113
118
|
</script>
|
|
114
119
|
|
|
115
|
-
<div class="grid-stack"
|
|
116
|
-
{
|
|
120
|
+
<div class="grid-stack-wrap">
|
|
121
|
+
<div class="grid-stack" bind:this={gridEl}>
|
|
122
|
+
{@render children()}
|
|
123
|
+
</div>
|
|
117
124
|
</div>
|
|
118
125
|
|
|
119
126
|
<style>
|
|
@@ -122,9 +129,14 @@
|
|
|
122
129
|
position: absolute;
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
.grid-stack-wrap {
|
|
133
|
+
overflow-x: hidden;
|
|
134
|
+
width: 100%;
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
:global(.grid-stack) {
|
|
126
138
|
background: transparent;
|
|
127
|
-
|
|
139
|
+
margin: 0 -0.5rem;
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
:global(.grid-stack-placeholder > .placeholder-content) {
|
|
@@ -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";
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
|
|
14
15
|
interface Props extends ExtensionProps {
|
|
15
16
|
chartRecord: any;
|
|
17
|
+
showHeader?: boolean;
|
|
16
18
|
onChartDeleted?: () => Promise<void>;
|
|
17
19
|
onChartEdited?: () => Promise<void>;
|
|
18
20
|
onChartClick?: (event: ChartClickEvent) => void;
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
|
|
21
23
|
const {
|
|
22
24
|
chartRecord,
|
|
25
|
+
showHeader = true,
|
|
23
26
|
onChartDeleted,
|
|
24
27
|
onChartEdited,
|
|
25
28
|
onChartClick,
|
|
@@ -30,6 +33,27 @@
|
|
|
30
33
|
const { UpdateDetailViewButton, Button, Tooltip } = utils.components;
|
|
31
34
|
const icons = utils.components.Icons;
|
|
32
35
|
|
|
36
|
+
// Button visibility comes from the caller's actual permissions on
|
|
37
|
+
// reports_charts. A logged-in admin sees edit + delete; a viewer without
|
|
38
|
+
// those grants (including a share-token recipient) doesn't see either.
|
|
39
|
+
let canUpdate = $state(false);
|
|
40
|
+
let canDelete = $state(false);
|
|
41
|
+
|
|
42
|
+
onMount(async () => {
|
|
43
|
+
const [updateAllowed, deleteAllowed] = await Promise.all([
|
|
44
|
+
utils.emitEvent("auth.canAccess", {
|
|
45
|
+
collection: "reports_charts",
|
|
46
|
+
action: "update",
|
|
47
|
+
}),
|
|
48
|
+
utils.emitEvent("auth.canAccess", {
|
|
49
|
+
collection: "reports_charts",
|
|
50
|
+
action: "delete",
|
|
51
|
+
}),
|
|
52
|
+
]);
|
|
53
|
+
canUpdate = updateAllowed === true;
|
|
54
|
+
canDelete = deleteAllowed === true;
|
|
55
|
+
});
|
|
56
|
+
|
|
33
57
|
function findCustomChartComponent(type: string): any {
|
|
34
58
|
const extensions = utils.ctx?.extensions ?? {};
|
|
35
59
|
for (const ext of Object.values(extensions) as any[]) {
|
|
@@ -73,39 +97,47 @@
|
|
|
73
97
|
}
|
|
74
98
|
</script>
|
|
75
99
|
|
|
76
|
-
<div
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
<div
|
|
101
|
+
class="grid h-full rounded-md border bg-background {showHeader ? 'grid-rows-[auto_1fr]' : 'grid-rows-[1fr]'}"
|
|
102
|
+
>
|
|
103
|
+
{#if showHeader}
|
|
104
|
+
<div class="flex items-center justify-between border-b p-2 text-sm">
|
|
105
|
+
<div class="flex items-center gap-1.5">
|
|
106
|
+
<span>{chartRecord.title}</span>
|
|
107
|
+
{#if chartRecord.description}
|
|
108
|
+
<Tooltip.Root>
|
|
109
|
+
<Tooltip.Trigger>
|
|
110
|
+
<icons.CircleHelp class="text-muted-foreground" size={14} />
|
|
111
|
+
</Tooltip.Trigger>
|
|
112
|
+
<Tooltip.Content class="max-w-64 text-xs">
|
|
113
|
+
{chartRecord.description}
|
|
114
|
+
</Tooltip.Content>
|
|
115
|
+
</Tooltip.Root>
|
|
116
|
+
{/if}
|
|
117
|
+
</div>
|
|
118
|
+
<div class="flex">
|
|
119
|
+
{#if canUpdate}
|
|
120
|
+
<UpdateDetailViewButton
|
|
121
|
+
collectionName="reports_charts"
|
|
122
|
+
recordId={chartRecord.id}
|
|
123
|
+
variant="ghost"
|
|
124
|
+
class="h-7 w-7 text-muted-foreground hover:bg-transparent"
|
|
125
|
+
Icon={icons.Pencil}
|
|
126
|
+
onSuccessfullSave={handleEditOnSuccessfull}
|
|
127
|
+
></UpdateDetailViewButton>
|
|
128
|
+
{/if}
|
|
129
|
+
{#if canDelete}
|
|
130
|
+
<Button
|
|
131
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
132
|
+
variant="ghost"
|
|
133
|
+
size="icon"
|
|
134
|
+
Icon={icons.Trash}
|
|
135
|
+
onclick={handleChartDelete}
|
|
136
|
+
></Button>
|
|
137
|
+
{/if}
|
|
138
|
+
</div>
|
|
107
139
|
</div>
|
|
108
|
-
|
|
140
|
+
{/if}
|
|
109
141
|
<div class="flex min-h-0 items-center justify-center overflow-hidden">
|
|
110
142
|
<svelte:boundary>
|
|
111
143
|
{#snippet failed(error, reset)}
|
|
@@ -157,10 +189,3 @@
|
|
|
157
189
|
</svelte:boundary>
|
|
158
190
|
</div>
|
|
159
191
|
</div>
|
|
160
|
-
|
|
161
|
-
<style>
|
|
162
|
-
.gridContainer {
|
|
163
|
-
display: grid;
|
|
164
|
-
grid-template-rows: auto 1fr;
|
|
165
|
-
}
|
|
166
|
-
</style>
|
|
@@ -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>
|