@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
|
@@ -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,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,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 {};
|
|
@@ -3,21 +3,49 @@ import type { Extension } from "@lobb-js/core";
|
|
|
3
3
|
import packageJson from "../../package.json" with { type: "json" };
|
|
4
4
|
import { collections } from "./collections/index.ts";
|
|
5
5
|
import { migrations } from "./migrations.ts";
|
|
6
|
-
import { meta } from "./meta.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { meta as buildMeta } from "./meta.ts";
|
|
7
|
+
import { workflows } from "./workflows.ts";
|
|
8
8
|
import { openapi } from "./openapi.ts";
|
|
9
9
|
import { relations } from "./relations.ts";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
type ShareReadPermission =
|
|
12
|
+
| true
|
|
13
|
+
| { filter?: Record<string, unknown>; fields?: Record<string, true> };
|
|
14
|
+
|
|
15
|
+
export interface ReportsExtensionConfig {
|
|
16
|
+
/**
|
|
17
|
+
* Domain-specific collections a share recipient needs read access to in
|
|
18
|
+
* addition to `reports_dashboards` + `reports_charts`. Reports often
|
|
19
|
+
* drill down into other collections (e.g. risk charts opening the
|
|
20
|
+
* `risks` table on click) — those collections have to be readable by
|
|
21
|
+
* the share token or the drill-down requests will 403.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* reports({
|
|
25
|
+
* shareReadAccess: {
|
|
26
|
+
* risks: true,
|
|
27
|
+
* risk_config: true,
|
|
28
|
+
* },
|
|
29
|
+
* })
|
|
30
|
+
*/
|
|
31
|
+
shareReadAccess?: Record<string, ShareReadPermission>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function extension(config: ReportsExtensionConfig = {}): Extension {
|
|
12
35
|
return {
|
|
13
36
|
version: packageJson.version,
|
|
14
37
|
name: "reports",
|
|
15
38
|
icon: "ChartNoAxesCombined",
|
|
16
|
-
|
|
39
|
+
workflows: workflows,
|
|
17
40
|
collections: collections,
|
|
18
41
|
relations: relations,
|
|
19
42
|
migrations: migrations,
|
|
20
|
-
meta
|
|
43
|
+
// Surface the config to the studio via meta so the Share button can
|
|
44
|
+
// include it in the per-report share permissions snapshot.
|
|
45
|
+
meta: async (lobb) => ({
|
|
46
|
+
...(await buildMeta(lobb)),
|
|
47
|
+
shareReadAccess: config.shareReadAccess ?? {},
|
|
48
|
+
}),
|
|
21
49
|
openapi: openapi,
|
|
22
50
|
};
|
|
23
51
|
}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import type { Extension, ExtensionUtils } from "@lobb-js/studio";
|
|
2
2
|
import Reports from "./lib/components/pages/reports/index.svelte";
|
|
3
|
+
import SharedReport from "./lib/components/pages/shared_report/index.svelte";
|
|
3
4
|
import QueryAiButton from "./lib/components/dv_fields_buttons/query_ai_button/index.svelte";
|
|
4
5
|
|
|
6
|
+
export { default as ReportBody } from "./lib/components/reportBody.svelte";
|
|
7
|
+
|
|
5
8
|
export default function extension(utils: ExtensionUtils): Extension {
|
|
6
9
|
return {
|
|
7
10
|
name: "reports",
|
|
8
11
|
components: {
|
|
9
12
|
"dvFields.topRight.reports_charts.query": QueryAiButton,
|
|
10
|
-
"pages.
|
|
13
|
+
"pages.dashboards": Reports,
|
|
14
|
+
// Lives at /studio/public/reports/shared_report — no auth required, the
|
|
15
|
+
// page itself swaps the LobbClient bearer to the share_token in the URL.
|
|
16
|
+
"publicPages.shared_report": SharedReport,
|
|
11
17
|
},
|
|
12
18
|
dashboardNavs: {
|
|
13
19
|
middle: [
|
|
14
20
|
{
|
|
15
|
-
href: "/studio/extensions/reports/
|
|
21
|
+
href: "/studio/extensions/reports/dashboards",
|
|
16
22
|
icon: utils.components.Icons.ChartNoAxesCombined,
|
|
17
|
-
label: "
|
|
23
|
+
label: "Dashboards",
|
|
24
|
+
represents: "reports_dashboards",
|
|
18
25
|
},
|
|
19
26
|
],
|
|
20
27
|
},
|
|
@@ -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>
|