@lobb-js/lobb-ext-reports 0.1.26
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/package.json +38 -0
- package/studio/README.md +29 -0
- package/studio/extension.types.ts +28 -0
- package/studio/index.html +13 -0
- package/studio/postcss.config.js +6 -0
- package/studio/src/dv_fields_buttons/query_ai_button/index.svelte +27 -0
- package/studio/src/index.ts +22 -0
- package/studio/src/main.ts +12 -0
- package/studio/src/pages/reports/components/chart.svelte +131 -0
- package/studio/src/pages/reports/components/charts/chartJs.svelte +42 -0
- package/studio/src/pages/reports/components/charts/table.svelte +36 -0
- package/studio/src/pages/reports/components/report.svelte +156 -0
- package/studio/src/pages/reports/index.svelte +142 -0
- package/studio/svelte.config.js +8 -0
- package/studio/tailwind.config.ts +92 -0
- package/studio/tsconfig.app.json +22 -0
- package/studio/tsconfig.json +7 -0
- package/studio/tsconfig.node.json +26 -0
- package/studio/vite.config.ts +13 -0
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lobb-js/lobb-ext-reports",
|
|
3
|
+
"version": "0.1.26",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"studio"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./studio/src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "cd studio && vite",
|
|
16
|
+
"build": "cd studio && vite build",
|
|
17
|
+
"preview": "cd studio && vite preview",
|
|
18
|
+
"check": "cd studio && svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@lobb-js/studio": "0.1.31",
|
|
22
|
+
"chart.js": "^4.4.8",
|
|
23
|
+
"lodash-es": "^4.17.21"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@sveltejs/vite-plugin-svelte": "6.2.1",
|
|
27
|
+
"@tsconfig/svelte": "^5.0.6",
|
|
28
|
+
"@types/lodash-es": "^4.17.12",
|
|
29
|
+
"@types/node": "^24.10.1",
|
|
30
|
+
"autoprefixer": "^10.4.23",
|
|
31
|
+
"svelte": "^5.43.8",
|
|
32
|
+
"svelte-check": "^4.3.4",
|
|
33
|
+
"tailwindcss": "^3.4.19",
|
|
34
|
+
"tailwindcss-animate": "^1.0.7",
|
|
35
|
+
"typescript": "~5.9.3",
|
|
36
|
+
"vite": "6.3.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/studio/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Reports Extension - Studio
|
|
2
|
+
|
|
3
|
+
This directory contains the frontend/dashboard interface for the reports extension.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
studio/
|
|
9
|
+
├── src/
|
|
10
|
+
│ ├── index.ts # Extension entry point
|
|
11
|
+
│ ├── main.ts # Vite app entry
|
|
12
|
+
│ ├── pages/ # UI pages
|
|
13
|
+
│ │ └── reports/ # Reports pages
|
|
14
|
+
│ └── dv_fields_buttons/ # Custom field buttons
|
|
15
|
+
├── public/ # Static assets
|
|
16
|
+
├── index.html # HTML entry point
|
|
17
|
+
├── vite.config.ts # Vite configuration
|
|
18
|
+
├── tailwind.config.ts # Tailwind CSS configuration
|
|
19
|
+
└── tsconfig.json # TypeScript configuration
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
The studio interface includes:
|
|
25
|
+
- Report builder UI with chart visualization
|
|
26
|
+
- Chart.js integration for various chart types
|
|
27
|
+
- Data table views
|
|
28
|
+
- Custom field buttons for AI-powered queries
|
|
29
|
+
- Report management interface
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// TODO: Import these types from @lobb-js/studio once available
|
|
2
|
+
export interface Extension {
|
|
3
|
+
name: string;
|
|
4
|
+
onStartup?: (utils: ExtensionUtils) => void;
|
|
5
|
+
components?: Record<string, any>;
|
|
6
|
+
dashboardNavs?: {
|
|
7
|
+
top?: NavItem[];
|
|
8
|
+
middle?: NavItem[];
|
|
9
|
+
bottom?: NavItem[];
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface NavItem {
|
|
14
|
+
label: string;
|
|
15
|
+
icon?: any;
|
|
16
|
+
href?: string;
|
|
17
|
+
onclick?: () => void;
|
|
18
|
+
navs?: NavItem[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ExtensionUtils {
|
|
22
|
+
components: {
|
|
23
|
+
Icons: Record<string, any>;
|
|
24
|
+
};
|
|
25
|
+
location: {
|
|
26
|
+
navigate: (path: string) => void;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Lobb Studio</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "src/extensions/extension.types";
|
|
3
|
+
|
|
4
|
+
let { utils, value = $bindable() }: ExtensionProps = $props();
|
|
5
|
+
|
|
6
|
+
let systemMessage = `You are an expert assistant specialized in generating PostgreSQL read-only queries for analytics and reporting purposes. Your job is to translate user requests written in natural language into accurate, efficient, and well-structured SQL SELECT queries. Only respond with a raw PostgreSQL SELECT query — do not include any markdown formatting, code blocks, or explanations.. This is the schema of the database: (${JSON.stringify(utils.ctx.meta.collections)})`;
|
|
7
|
+
|
|
8
|
+
const componentProps: any = {
|
|
9
|
+
title: "Generate the query with AI",
|
|
10
|
+
description: "Describe to the AI the query you want to generate",
|
|
11
|
+
class: "h-7 px-3 text-xs font-normal",
|
|
12
|
+
variant: "outline",
|
|
13
|
+
// format: {
|
|
14
|
+
// type: 'json_object'
|
|
15
|
+
// },
|
|
16
|
+
messages: [
|
|
17
|
+
{
|
|
18
|
+
role: "system",
|
|
19
|
+
content: systemMessage,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<utils.components.LlmButton bind:value {...componentProps}>
|
|
26
|
+
Generate with AI
|
|
27
|
+
</utils.components.LlmButton>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Extension, ExtensionUtils } from "src/extensions/extension.types";
|
|
2
|
+
import Reports from "./pages/reports/index.svelte";
|
|
3
|
+
import QueryAiButton from "./dv_fields_buttons/query_ai_button/index.svelte";
|
|
4
|
+
|
|
5
|
+
export function extension(utils: ExtensionUtils): Extension {
|
|
6
|
+
return {
|
|
7
|
+
name: "reports",
|
|
8
|
+
components: {
|
|
9
|
+
"dvFields.topRight.reports_charts.query": QueryAiButton,
|
|
10
|
+
"pages.analytics": Reports,
|
|
11
|
+
},
|
|
12
|
+
dashboardNavs: {
|
|
13
|
+
middle: [
|
|
14
|
+
{
|
|
15
|
+
href: "/extensions/reports/analytics",
|
|
16
|
+
icon: utils.components.Icons.ChartNoAxesCombined,
|
|
17
|
+
label: "Analytics",
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { mount } from "svelte";
|
|
2
|
+
import Studio from "@lobb-js/studio";
|
|
3
|
+
import { extension } from "./index.ts";
|
|
4
|
+
|
|
5
|
+
const app = mount(Studio, {
|
|
6
|
+
target: document.getElementById("app")!,
|
|
7
|
+
props: {
|
|
8
|
+
extensions: [extension],
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default app;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "src/extensions/extension.types";
|
|
3
|
+
import Table from "./charts/table.svelte";
|
|
4
|
+
import ChartJs from "./charts/chartJs.svelte";
|
|
5
|
+
|
|
6
|
+
interface Props extends ExtensionProps {
|
|
7
|
+
chartRecord: any;
|
|
8
|
+
onChartDeleted?: () => Promise<void>;
|
|
9
|
+
onChartEdited?: () => Promise<void>;
|
|
10
|
+
context?: Record<string, any>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
chartRecord,
|
|
15
|
+
onChartDeleted,
|
|
16
|
+
onChartEdited,
|
|
17
|
+
context,
|
|
18
|
+
...props
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
|
|
21
|
+
const utils = props.utils;
|
|
22
|
+
const { UpdateDetailViewButton, Button } = utils.components;
|
|
23
|
+
const icons = utils.components.Icons;
|
|
24
|
+
let dataPromise = $state(getChartData());
|
|
25
|
+
|
|
26
|
+
async function getChartData() {
|
|
27
|
+
const response = await utils.lobb.findOne(
|
|
28
|
+
"reports_charts",
|
|
29
|
+
chartRecord.id,
|
|
30
|
+
{
|
|
31
|
+
action: "run_query",
|
|
32
|
+
context: JSON.stringify(context),
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const result = await response.json();
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function handleChartDelete() {
|
|
41
|
+
const result = await utils.showDialog(
|
|
42
|
+
"Are you sure?",
|
|
43
|
+
"This will delete that chart you selected.",
|
|
44
|
+
);
|
|
45
|
+
if (result) {
|
|
46
|
+
await utils.lobb.deleteOne("reports_charts", chartRecord.id);
|
|
47
|
+
if (onChartDeleted) {
|
|
48
|
+
await onChartDeleted();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function handleEditOnSuccessfull() {
|
|
54
|
+
if (onChartEdited) {
|
|
55
|
+
await onChartEdited();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<div class="gridContainer h-96 rounded-md border bg-background">
|
|
61
|
+
<div class="flex items-center justify-between border-b p-2 text-sm">
|
|
62
|
+
<div>{chartRecord.title}</div>
|
|
63
|
+
<div class="flex">
|
|
64
|
+
<UpdateDetailViewButton
|
|
65
|
+
collectionName="reports_charts"
|
|
66
|
+
recordId={chartRecord.id}
|
|
67
|
+
variant="ghost"
|
|
68
|
+
class="h-7 w-7 text-muted-foreground hover:bg-transparent"
|
|
69
|
+
Icon={icons.Pencil}
|
|
70
|
+
onSuccessfullSave={handleEditOnSuccessfull}
|
|
71
|
+
></UpdateDetailViewButton>
|
|
72
|
+
<Button
|
|
73
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
74
|
+
variant="ghost"
|
|
75
|
+
size="icon"
|
|
76
|
+
Icon={icons.Trash}
|
|
77
|
+
onclick={handleChartDelete}
|
|
78
|
+
></Button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="flex items-center justify-center overflow-auto">
|
|
82
|
+
<svelte:boundary>
|
|
83
|
+
{#snippet failed(error, reset)}
|
|
84
|
+
<div
|
|
85
|
+
class="flex flex-col gap-2 p-2 bg-red-100 border border-red-500 h-full"
|
|
86
|
+
>
|
|
87
|
+
<div>{error}</div>
|
|
88
|
+
<button
|
|
89
|
+
class="border border-red-500 w-fit px-4 rounded-md hover:bg-red-500"
|
|
90
|
+
onclick={reset}>Reset</button
|
|
91
|
+
>
|
|
92
|
+
</div>
|
|
93
|
+
{/snippet}
|
|
94
|
+
{#await dataPromise}
|
|
95
|
+
<div
|
|
96
|
+
class="flex flex-col gap-4 h-full w-full absolute justify-center items-center"
|
|
97
|
+
>
|
|
98
|
+
<icons.LoaderCircle
|
|
99
|
+
class="animate-spin opacity-50"
|
|
100
|
+
size="50"
|
|
101
|
+
/>
|
|
102
|
+
<div class="flex flex-col items-center justify-center">
|
|
103
|
+
<div class="text-muted-foreground">
|
|
104
|
+
Loading the chart...
|
|
105
|
+
</div>
|
|
106
|
+
<div class="text-muted-foreground text-xs">
|
|
107
|
+
Loading chart's data
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
{:then data}
|
|
112
|
+
{#if data.type === "table"}
|
|
113
|
+
<Table input={data.input} {...props} />
|
|
114
|
+
{:else if data.type === "chartjs"}
|
|
115
|
+
<ChartJs input={data.input} {...props} />
|
|
116
|
+
{:else}
|
|
117
|
+
<div class="text-muted-foreground">
|
|
118
|
+
The ({data.type}) chart type is unsupported
|
|
119
|
+
</div>
|
|
120
|
+
{/if}
|
|
121
|
+
{/await}
|
|
122
|
+
</svelte:boundary>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<style>
|
|
127
|
+
.gridContainer {
|
|
128
|
+
display: grid;
|
|
129
|
+
grid-template-rows: auto 1fr;
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "src/extensions/extension.types";
|
|
3
|
+
|
|
4
|
+
import { onMount } from 'svelte';
|
|
5
|
+
import { Chart } from 'chart.js/auto';
|
|
6
|
+
import { merge } from 'lodash-es';
|
|
7
|
+
|
|
8
|
+
interface Props extends ExtensionProps {
|
|
9
|
+
input: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { input }: Props = $props();
|
|
13
|
+
|
|
14
|
+
let canvas: HTMLCanvasElement;
|
|
15
|
+
let chart: Chart;
|
|
16
|
+
|
|
17
|
+
onMount(() => {
|
|
18
|
+
if (canvas) {
|
|
19
|
+
const ctx = canvas.getContext('2d');
|
|
20
|
+
if (ctx) {
|
|
21
|
+
chart = new Chart(
|
|
22
|
+
ctx,
|
|
23
|
+
merge(
|
|
24
|
+
{
|
|
25
|
+
options: {
|
|
26
|
+
responsive: true,
|
|
27
|
+
maintainAspectRatio: false
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
$state.snapshot(input)
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
chart?.destroy();
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<canvas bind:this={canvas}></canvas>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "src/extensions/extension.types";
|
|
3
|
+
|
|
4
|
+
interface Props extends ExtensionProps {
|
|
5
|
+
input: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { input, utils }: Props = $props();
|
|
9
|
+
|
|
10
|
+
const { data } = input;
|
|
11
|
+
const { Table } = utils.components;
|
|
12
|
+
|
|
13
|
+
const columns = data.length
|
|
14
|
+
? Object.keys(data[0]).map((column) => {
|
|
15
|
+
return {
|
|
16
|
+
id: column,
|
|
17
|
+
};
|
|
18
|
+
})
|
|
19
|
+
: null;
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class="relative h-full w-full">
|
|
23
|
+
{#if columns}
|
|
24
|
+
<Table
|
|
25
|
+
{data}
|
|
26
|
+
{columns}
|
|
27
|
+
selectByColumn={null}
|
|
28
|
+
localSorting={true}
|
|
29
|
+
showLastRowBorder={true}
|
|
30
|
+
showLastColumnBorder={true}
|
|
31
|
+
unifiedBgColor="bg-background"
|
|
32
|
+
></Table>
|
|
33
|
+
{:else}
|
|
34
|
+
<div class="flex justify-center items-center h-full">No Results</div>
|
|
35
|
+
{/if}
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "src/extensions/extension.types";
|
|
3
|
+
import type { DateRange } from "bits-ui";
|
|
4
|
+
import Chart from "./chart.svelte";
|
|
5
|
+
|
|
6
|
+
interface Props extends ExtensionProps {
|
|
7
|
+
reportId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props: Props = $props();
|
|
11
|
+
const { intlDate } = props.utils;
|
|
12
|
+
const {
|
|
13
|
+
SidebarTrigger,
|
|
14
|
+
CreateDetailViewButton,
|
|
15
|
+
Icons,
|
|
16
|
+
RangeCalendarButton,
|
|
17
|
+
} = props.utils.components;
|
|
18
|
+
|
|
19
|
+
let dateRangeValue: DateRange = $state({
|
|
20
|
+
start: intlDate
|
|
21
|
+
.today(intlDate.getLocalTimeZone())
|
|
22
|
+
.subtract({ days: 30 }),
|
|
23
|
+
end: intlDate.today(intlDate.getLocalTimeZone()),
|
|
24
|
+
});
|
|
25
|
+
let context = $derived({
|
|
26
|
+
dateRange: {
|
|
27
|
+
start: dateRangeValue.start?.toString(),
|
|
28
|
+
end: dateRangeValue.end?.toString(),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let report: any = $state(null);
|
|
33
|
+
let chartsPromise = $state(getCharts());
|
|
34
|
+
|
|
35
|
+
async function getCharts() {
|
|
36
|
+
const reportsResponse = await props.utils.lobb.findAll(
|
|
37
|
+
"reports_dashboards",
|
|
38
|
+
{
|
|
39
|
+
sort: "id",
|
|
40
|
+
filter: {
|
|
41
|
+
id: props.reportId,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
report = (await reportsResponse.json()).data[0];
|
|
46
|
+
const chartsResponse = await props.utils.lobb.findAll(
|
|
47
|
+
"reports_charts",
|
|
48
|
+
{
|
|
49
|
+
filter: {
|
|
50
|
+
report_id: report.id,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
return (await chartsResponse.json()).data;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function reloadChart() {
|
|
58
|
+
chartsPromise = new Promise(() => {});
|
|
59
|
+
chartsPromise = Promise.resolve(await getCharts());
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<div class="gridContainer relative h-full w-full flex-col overflow-auto">
|
|
64
|
+
{#await chartsPromise}
|
|
65
|
+
<div
|
|
66
|
+
class="flex flex-col gap-4 h-full w-full absolute justify-center items-center"
|
|
67
|
+
>
|
|
68
|
+
<Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
|
|
69
|
+
<div class="flex flex-col items-center justify-center">
|
|
70
|
+
<div class="text-muted-foreground">
|
|
71
|
+
Loading the dashboard...
|
|
72
|
+
</div>
|
|
73
|
+
<div class="text-muted-foreground text-xs">
|
|
74
|
+
Loading all the charts of the selected dashboard
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
{:then charts}
|
|
79
|
+
<div
|
|
80
|
+
class="flex justify-between gap-2 overflow-auto border-b bg-background p-2"
|
|
81
|
+
>
|
|
82
|
+
<div class="flex gap-2">
|
|
83
|
+
<div class="mt-1">
|
|
84
|
+
<SidebarTrigger />
|
|
85
|
+
</div>
|
|
86
|
+
<div class="flex flex-col justify-center">
|
|
87
|
+
<h2 class="font-medium text-primary">{report.name}</h2>
|
|
88
|
+
<div class="text-xs text-muted-foreground">
|
|
89
|
+
{report.description}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="flex gap-2 self-end">
|
|
94
|
+
<RangeCalendarButton bind:value={dateRangeValue} />
|
|
95
|
+
<CreateDetailViewButton
|
|
96
|
+
collectionName="reports_charts"
|
|
97
|
+
values={{
|
|
98
|
+
report_id: {
|
|
99
|
+
id: props.reportId,
|
|
100
|
+
name: report.name,
|
|
101
|
+
},
|
|
102
|
+
}}
|
|
103
|
+
variant="default"
|
|
104
|
+
class="h-7 px-3 text-xs font-normal"
|
|
105
|
+
Icon={Icons.Plus}
|
|
106
|
+
onSuccessfullSave={async () => await reloadChart()}
|
|
107
|
+
>
|
|
108
|
+
Create chart
|
|
109
|
+
</CreateDetailViewButton>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="overflow-auto">
|
|
113
|
+
<div class="chartsGridContainer w-full flex-wrap gap-4 p-4">
|
|
114
|
+
{#if charts.length === 0}
|
|
115
|
+
<div
|
|
116
|
+
class="flex w-full flex-col items-center justify-center gap-4 p-10 text-muted-foreground"
|
|
117
|
+
>
|
|
118
|
+
<Icons.CircleSlash2 class="opacity-50" size="50" />
|
|
119
|
+
<div class="flex flex-col items-center justify-center">
|
|
120
|
+
<div>No charts available</div>
|
|
121
|
+
<div class="text-xs">
|
|
122
|
+
Create a new chart to fill this report page
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
{:else}
|
|
127
|
+
{#each charts as chart}
|
|
128
|
+
{#key context}
|
|
129
|
+
<Chart
|
|
130
|
+
chartRecord={chart}
|
|
131
|
+
onChartDeleted={async () => await reloadChart()}
|
|
132
|
+
onChartEdited={async () => await reloadChart()}
|
|
133
|
+
{context}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
{/key}
|
|
137
|
+
{/each}
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
{/await}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<style>
|
|
145
|
+
.gridContainer {
|
|
146
|
+
display: grid;
|
|
147
|
+
grid-template-columns: 1fr;
|
|
148
|
+
grid-template-rows: auto 1fr;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.chartsGridContainer {
|
|
152
|
+
display: grid;
|
|
153
|
+
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
|
|
154
|
+
grid-gap: 10px;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ExtensionProps } from "src/extensions/extension.types";
|
|
3
|
+
import type { SideBarData } from "$lib/components/sidebar/sidebarElements.svelte";
|
|
4
|
+
import type { Location } from "@wjfe/n-savant";
|
|
5
|
+
import Report from "./components/report.svelte";
|
|
6
|
+
|
|
7
|
+
const { utils, ...props }: ExtensionProps = $props();
|
|
8
|
+
|
|
9
|
+
let extensionPagePath = "/extensions/reports/analytics";
|
|
10
|
+
let reportId: string | null = $derived(getReportID(utils.location));
|
|
11
|
+
let sideBarData: any = $state(null);
|
|
12
|
+
const {
|
|
13
|
+
CreateDetailViewButton,
|
|
14
|
+
UpdateDetailViewButton,
|
|
15
|
+
Sidebar,
|
|
16
|
+
SidebarTrigger,
|
|
17
|
+
Button,
|
|
18
|
+
} = utils.components;
|
|
19
|
+
const icons = utils.components.Icons;
|
|
20
|
+
|
|
21
|
+
getSideBarData();
|
|
22
|
+
|
|
23
|
+
async function getSideBarData(): Promise<void> {
|
|
24
|
+
const response = await utils.lobb.findAll("reports_dashboards", {
|
|
25
|
+
sort: "id",
|
|
26
|
+
filter: {},
|
|
27
|
+
});
|
|
28
|
+
const allReports = (await response.json()).data;
|
|
29
|
+
|
|
30
|
+
const localSideBarData: SideBarData = [];
|
|
31
|
+
for (let index = 0; index < allReports.length; index++) {
|
|
32
|
+
const report = allReports[index];
|
|
33
|
+
localSideBarData.push({
|
|
34
|
+
name: report.name,
|
|
35
|
+
icon: icons.FileChartColumn,
|
|
36
|
+
onclick: () => {
|
|
37
|
+
utils.location.navigate(
|
|
38
|
+
`${extensionPagePath}/${report.id}`,
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
meta: {
|
|
42
|
+
reportId: report.id,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sideBarData = localSideBarData;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function handleReportDelete(recordId: any) {
|
|
51
|
+
const result = await utils.showDialog(
|
|
52
|
+
"Are you sure?",
|
|
53
|
+
"This will delete the report you selected.",
|
|
54
|
+
);
|
|
55
|
+
if (result) {
|
|
56
|
+
await utils.lobb.deleteOne("reports_dashboards", recordId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getReportID(location: Location) {
|
|
61
|
+
return location.url.pathname.split("/")[4];
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<Sidebar title="Reports" data={sideBarData}>
|
|
66
|
+
{#snippet belowSearch()}
|
|
67
|
+
<div class="p-2 pb-0">
|
|
68
|
+
<CreateDetailViewButton
|
|
69
|
+
collectionName="reports_dashboards"
|
|
70
|
+
variant="outline"
|
|
71
|
+
class="h-7 w-full px-3 text-xs font-normal"
|
|
72
|
+
Icon={icons.Plus}
|
|
73
|
+
onSuccessfullSave={async (record) => {
|
|
74
|
+
await getSideBarData();
|
|
75
|
+
reportId = record.id;
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
Create a report
|
|
79
|
+
</CreateDetailViewButton>
|
|
80
|
+
</div>
|
|
81
|
+
{/snippet}
|
|
82
|
+
{#snippet elementRightSide(item)}
|
|
83
|
+
{#if item.meta}
|
|
84
|
+
{@const reportId = item.meta.reportId}
|
|
85
|
+
<div>
|
|
86
|
+
<UpdateDetailViewButton
|
|
87
|
+
collectionName="reports_dashboards"
|
|
88
|
+
recordId={reportId}
|
|
89
|
+
variant="ghost"
|
|
90
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
91
|
+
Icon={icons.Pencil}
|
|
92
|
+
onSuccessfullSave={async () => {
|
|
93
|
+
await getSideBarData();
|
|
94
|
+
}}
|
|
95
|
+
></UpdateDetailViewButton>
|
|
96
|
+
<Button
|
|
97
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
98
|
+
variant="ghost"
|
|
99
|
+
size="icon"
|
|
100
|
+
Icon={icons.Trash}
|
|
101
|
+
onclick={async () => {
|
|
102
|
+
await handleReportDelete(reportId);
|
|
103
|
+
await getSideBarData();
|
|
104
|
+
}}
|
|
105
|
+
></Button>
|
|
106
|
+
</div>
|
|
107
|
+
{/if}
|
|
108
|
+
{/snippet}
|
|
109
|
+
<div class="relative h-full w-full bg-muted">
|
|
110
|
+
{#if reportId}
|
|
111
|
+
{#key reportId}
|
|
112
|
+
<Report {utils} {reportId} {...props} />
|
|
113
|
+
{/key}
|
|
114
|
+
{:else}
|
|
115
|
+
<div
|
|
116
|
+
class="flex h-full w-full flex-col items-center justify-center gap-4 text-muted-foreground"
|
|
117
|
+
>
|
|
118
|
+
<icons.CircleSlash2 class="opacity-50" size="50" />
|
|
119
|
+
<div class="flex flex-col items-center justify-center">
|
|
120
|
+
<div>No report selected</div>
|
|
121
|
+
<div class="text-xs">
|
|
122
|
+
Please select a report to view the details or create a
|
|
123
|
+
new one
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<CreateDetailViewButton
|
|
127
|
+
collectionName="reports_dashboards"
|
|
128
|
+
variant="default"
|
|
129
|
+
class="h-7 px-3 text-xs font-normal"
|
|
130
|
+
Icon={icons.Plus}
|
|
131
|
+
onSuccessfullSave={async (record) =>
|
|
132
|
+
utils.location.navigate(`?report=${record.id}`)}
|
|
133
|
+
>
|
|
134
|
+
Create a report
|
|
135
|
+
</CreateDetailViewButton>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="absolute top-0 left-0 p-2.5">
|
|
138
|
+
<SidebarTrigger />
|
|
139
|
+
</div>
|
|
140
|
+
{/if}
|
|
141
|
+
</div>
|
|
142
|
+
</Sidebar>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
|
2
|
+
|
|
3
|
+
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
|
|
4
|
+
export default {
|
|
5
|
+
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
|
6
|
+
// for more information about preprocessors
|
|
7
|
+
preprocess: vitePreprocess(),
|
|
8
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { fontFamily } from "tailwindcss/defaultTheme";
|
|
2
|
+
import type { Config } from "tailwindcss";
|
|
3
|
+
import tailwindcssAnimate from "tailwindcss-animate";
|
|
4
|
+
|
|
5
|
+
const config: Config = {
|
|
6
|
+
darkMode: ["class"],
|
|
7
|
+
content: [
|
|
8
|
+
"./src/**/*.{html,js,svelte,ts}",
|
|
9
|
+
"../../../packages/studio/src/**/*.{html,js,svelte,ts}",
|
|
10
|
+
],
|
|
11
|
+
safelist: ["dark"],
|
|
12
|
+
theme: {
|
|
13
|
+
container: {
|
|
14
|
+
center: true,
|
|
15
|
+
padding: "2rem",
|
|
16
|
+
screens: {
|
|
17
|
+
"2xl": "1400px",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
extend: {
|
|
21
|
+
colors: {
|
|
22
|
+
border: "hsl(var(--border) / <alpha-value>)",
|
|
23
|
+
input: "hsl(var(--input) / <alpha-value>)",
|
|
24
|
+
ring: "hsl(var(--ring) / <alpha-value>)",
|
|
25
|
+
background: "hsl(var(--background) / <alpha-value>)",
|
|
26
|
+
foreground: "hsl(var(--foreground) / <alpha-value>)",
|
|
27
|
+
primary: {
|
|
28
|
+
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
|
|
29
|
+
foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
|
|
30
|
+
},
|
|
31
|
+
secondary: {
|
|
32
|
+
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
|
|
33
|
+
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)",
|
|
34
|
+
},
|
|
35
|
+
destructive: {
|
|
36
|
+
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
|
|
37
|
+
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
|
|
38
|
+
},
|
|
39
|
+
soft: {
|
|
40
|
+
DEFAULT: "hsl(var(--soft) / <alpha-value>)",
|
|
41
|
+
},
|
|
42
|
+
muted: {
|
|
43
|
+
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
|
|
44
|
+
foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
|
|
45
|
+
},
|
|
46
|
+
accent: {
|
|
47
|
+
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
|
|
48
|
+
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
|
|
49
|
+
},
|
|
50
|
+
popover: {
|
|
51
|
+
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
|
|
52
|
+
foreground: "hsl(var(--popover-foreground) / <alpha-value>)",
|
|
53
|
+
},
|
|
54
|
+
card: {
|
|
55
|
+
DEFAULT: "hsl(var(--card) / <alpha-value>)",
|
|
56
|
+
foreground: "hsl(var(--card-foreground) / <alpha-value>)",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
borderRadius: {
|
|
60
|
+
xl: "calc(var(--radius) + 4px)",
|
|
61
|
+
lg: "var(--radius)",
|
|
62
|
+
md: "calc(var(--radius) - 2px)",
|
|
63
|
+
sm: "calc(var(--radius) - 4px)",
|
|
64
|
+
},
|
|
65
|
+
fontFamily: {
|
|
66
|
+
sans: [...fontFamily.sans],
|
|
67
|
+
},
|
|
68
|
+
keyframes: {
|
|
69
|
+
"accordion-down": {
|
|
70
|
+
from: { height: "0" },
|
|
71
|
+
to: { height: "var(--bits-accordion-content-height)" },
|
|
72
|
+
},
|
|
73
|
+
"accordion-up": {
|
|
74
|
+
from: { height: "var(--bits-accordion-content-height)" },
|
|
75
|
+
to: { height: "0" },
|
|
76
|
+
},
|
|
77
|
+
"caret-blink": {
|
|
78
|
+
"0%,70%,100%": { opacity: "1" },
|
|
79
|
+
"20%,50%": { opacity: "0" },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
animation: {
|
|
83
|
+
"accordion-down": "accordion-down 0.2s ease-out",
|
|
84
|
+
"accordion-up": "accordion-up 0.2s ease-out",
|
|
85
|
+
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
plugins: [tailwindcssAnimate],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default config;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@tsconfig/svelte/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"useDefineForClassFields": true,
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["svelte", "vite/client"],
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"allowArbitraryExtensions": true,
|
|
11
|
+
/**
|
|
12
|
+
* Typecheck JS in `.svelte` and `.js` files by default.
|
|
13
|
+
* Disable checkJs if you'd like to use dynamic types in JS.
|
|
14
|
+
* Note that setting allowJs false does not prevent the use
|
|
15
|
+
* of JS in `.svelte` files.
|
|
16
|
+
*/
|
|
17
|
+
"allowJs": true,
|
|
18
|
+
"checkJs": true,
|
|
19
|
+
"moduleDetection": "force"
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
// https://vite.dev/config/
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [svelte()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
$lib: path.resolve("../../../packages/studio/src/lib"),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|