@riligar/elysia-sqlite 1.1.0 → 1.1.5
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 +6 -3
- package/src/index.js +7 -1
- package/src/ui/dist/assets/_baseUniq-CtjpPx06.js +1 -0
- package/src/ui/dist/assets/arc-BFeA2lFC.js +1 -0
- package/src/ui/dist/assets/architectureDiagram-VXUJARFQ-cJh390m5.js +36 -0
- package/src/ui/dist/assets/blockDiagram-VD42YOAC-BhDUcRai.js +122 -0
- package/src/ui/dist/assets/c4Diagram-YG6GDRKO-CMTMP654.js +10 -0
- package/src/ui/dist/assets/channel-DVo75U_z.js +1 -0
- package/src/ui/dist/assets/chunk-4BX2VUAB-hf_dt5BT.js +1 -0
- package/src/ui/dist/assets/chunk-55IACEB6-xClE-JKq.js +1 -0
- package/src/ui/dist/assets/chunk-B4BG7PRW-D6pt13Ec.js +165 -0
- package/src/ui/dist/assets/chunk-DI55MBZ5-Dj9Qg80I.js +220 -0
- package/src/ui/dist/assets/chunk-FMBD7UC4-1ZlekxIE.js +15 -0
- package/src/ui/dist/assets/chunk-QN33PNHL-DAmen2cg.js +1 -0
- package/src/ui/dist/assets/chunk-QZHKN3VN-C-3XzKoW.js +1 -0
- package/src/ui/dist/assets/chunk-TZMSLE5B-bM4mxnvE.js +1 -0
- package/src/ui/dist/assets/classDiagram-2ON5EDUG-DQalC8tR.js +1 -0
- package/src/ui/dist/assets/classDiagram-v2-WZHVMYZB-DQalC8tR.js +1 -0
- package/src/ui/dist/assets/clone-VeB9KXOY.js +1 -0
- package/src/ui/dist/assets/cose-bilkent-S5V4N54A-BxFitB5E.js +1 -0
- package/src/ui/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/src/ui/dist/assets/dagre-6UL2VRFP-DZSkvetS.js +4 -0
- package/src/ui/dist/assets/defaultLocale-C4B-KCzX.js +1 -0
- package/src/ui/dist/assets/diagram-PSM6KHXK-CYnji4ok.js +24 -0
- package/src/ui/dist/assets/diagram-QEK2KX5R-BlF0GwV_.js +43 -0
- package/src/ui/dist/assets/diagram-S2PKOQOG-BMf9iQYN.js +24 -0
- package/src/ui/dist/assets/erDiagram-Q2GNP2WA-B9nG6AIS.js +60 -0
- package/src/ui/dist/assets/flowDiagram-NV44I4VS-cRvIpj4q.js +162 -0
- package/src/ui/dist/assets/ganttDiagram-JELNMOA3-D5PauD1v.js +267 -0
- package/src/ui/dist/assets/gitGraphDiagram-NY62KEGX-BPtCepD0.js +65 -0
- package/src/ui/dist/assets/graph-C9jtFm6q.js +1 -0
- package/src/ui/dist/assets/index-D564tWvR.js +525 -0
- package/src/ui/dist/assets/index-DXG21mfz.css +1 -0
- package/src/ui/dist/assets/infoDiagram-WHAUD3N6-0hbOLvdM.js +2 -0
- package/src/ui/dist/assets/init-Gi6I4Gst.js +1 -0
- package/src/ui/dist/assets/journeyDiagram-XKPGCS4Q-CBy5upkf.js +139 -0
- package/src/ui/dist/assets/kanban-definition-3W4ZIXB7-DrobDgMJ.js +89 -0
- package/src/ui/dist/assets/katex-Cu_Erd72.js +261 -0
- package/src/ui/dist/assets/layout-DM9qC5Az.js +1 -0
- package/src/ui/dist/assets/linear-Fky2CJQk.js +1 -0
- package/src/ui/dist/assets/min-Bjr2uZ4W.js +1 -0
- package/src/ui/dist/assets/mindmap-definition-VGOIOE7T-CIlFmeVI.js +68 -0
- package/src/ui/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/src/ui/dist/assets/pieDiagram-ADFJNKIX-DMIZSQUQ.js +30 -0
- package/src/ui/dist/assets/quadrantDiagram-AYHSOK5B-DBCuVneU.js +7 -0
- package/src/ui/dist/assets/requirementDiagram-UZGBJVZJ-Dc87Li5r.js +64 -0
- package/src/ui/dist/assets/sankeyDiagram-TZEHDZUN-SnjPjzIj.js +10 -0
- package/src/ui/dist/assets/sequenceDiagram-WL72ISMW-CegtRWdp.js +145 -0
- package/src/ui/dist/assets/stateDiagram-FKZM4ZOC-DRXLGy91.js +1 -0
- package/src/ui/dist/assets/stateDiagram-v2-4FDKWEC3-DfAIcXf2.js +1 -0
- package/src/ui/dist/assets/timeline-definition-IT6M3QCI-Dfw0Almx.js +61 -0
- package/src/ui/dist/assets/treemap-KMMF4GRG-HxWgHRrr.js +128 -0
- package/src/ui/dist/assets/xychartDiagram-PRI3JC2R-C86J7PBK.js +7 -0
- package/src/ui/{index.html → dist/index.html} +2 -1
- package/src/ui/bun.lock +0 -612
- package/src/ui/package.json +0 -29
- package/src/ui/postcss.config.cjs +0 -14
- package/src/ui/src/App.jsx +0 -2103
- package/src/ui/src/components/DataGrid.jsx +0 -122
- package/src/ui/src/components/EditableCell.jsx +0 -166
- package/src/ui/src/components/ExportButton.jsx +0 -95
- package/src/ui/src/components/FKPreview.jsx +0 -106
- package/src/ui/src/components/Filter.jsx +0 -302
- package/src/ui/src/components/HoldButton.jsx +0 -230
- package/src/ui/src/components/Login.jsx +0 -148
- package/src/ui/src/components/Onboarding.jsx +0 -127
- package/src/ui/src/components/Pagination.jsx +0 -35
- package/src/ui/src/components/SecuritySettings.jsx +0 -273
- package/src/ui/src/components/TableSelector.jsx +0 -75
- package/src/ui/src/hooks/useFilter.js +0 -120
- package/src/ui/src/index.css +0 -123
- package/src/ui/src/main.jsx +0 -115
- package/src/ui/vite.config.js +0 -19
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
Paper,
|
|
4
|
-
Title,
|
|
5
|
-
Text,
|
|
6
|
-
TextInput,
|
|
7
|
-
PasswordInput,
|
|
8
|
-
Button,
|
|
9
|
-
Container,
|
|
10
|
-
Group,
|
|
11
|
-
Stack,
|
|
12
|
-
ThemeIcon,
|
|
13
|
-
Box,
|
|
14
|
-
} from "@mantine/core";
|
|
15
|
-
import { IconDatabase, IconShieldLock } from "@tabler/icons-react";
|
|
16
|
-
import { notifications } from "@mantine/notifications";
|
|
17
|
-
|
|
18
|
-
export function Onboarding({ onConfigured }) {
|
|
19
|
-
const [username, setUsername] = useState("admin");
|
|
20
|
-
const [password, setPassword] = useState("");
|
|
21
|
-
const [loading, setLoading] = useState(false);
|
|
22
|
-
|
|
23
|
-
const handleSubmit = async (e) => {
|
|
24
|
-
e.preventDefault();
|
|
25
|
-
if (!password) {
|
|
26
|
-
notifications.show({
|
|
27
|
-
title: "Error",
|
|
28
|
-
message: "Password is required",
|
|
29
|
-
color: "red",
|
|
30
|
-
});
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
setLoading(true);
|
|
35
|
-
try {
|
|
36
|
-
const response = await fetch("/admin/api/setup", {
|
|
37
|
-
method: "POST",
|
|
38
|
-
headers: { "Content-Type": "application/json" },
|
|
39
|
-
body: JSON.stringify({ username, password }),
|
|
40
|
-
});
|
|
41
|
-
const data = await response.json();
|
|
42
|
-
|
|
43
|
-
if (data.success) {
|
|
44
|
-
notifications.show({
|
|
45
|
-
title: "Welcome!",
|
|
46
|
-
message: "System configured successfully",
|
|
47
|
-
color: "green",
|
|
48
|
-
});
|
|
49
|
-
onConfigured();
|
|
50
|
-
} else {
|
|
51
|
-
notifications.show({
|
|
52
|
-
title: "Error",
|
|
53
|
-
message: data.error,
|
|
54
|
-
color: "red",
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
} catch (error) {
|
|
58
|
-
notifications.show({
|
|
59
|
-
title: "Error",
|
|
60
|
-
message: "Failed to connect to server",
|
|
61
|
-
color: "red",
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
setLoading(false);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<Box
|
|
69
|
-
style={{
|
|
70
|
-
height: "100vh",
|
|
71
|
-
display: "flex",
|
|
72
|
-
alignItems: "center",
|
|
73
|
-
justifyContent: "center",
|
|
74
|
-
backgroundColor: "#FBFAF8",
|
|
75
|
-
}}
|
|
76
|
-
>
|
|
77
|
-
<Container size={420}>
|
|
78
|
-
<Stack align="center" mb="xl">
|
|
79
|
-
<ThemeIcon size={60} radius="md" color="dark">
|
|
80
|
-
<IconDatabase size={34} />
|
|
81
|
-
</ThemeIcon>
|
|
82
|
-
<Title order={1} fw={700}>
|
|
83
|
-
Welcome to SQLite Admin
|
|
84
|
-
</Title>
|
|
85
|
-
<Text c="dimmed" size="sm" ta="center">
|
|
86
|
-
Set up your administrator credentials to start managing your database.
|
|
87
|
-
</Text>
|
|
88
|
-
</Stack>
|
|
89
|
-
|
|
90
|
-
<Paper withBorder shadow="md" p={30} radius="md">
|
|
91
|
-
<form onSubmit={handleSubmit}>
|
|
92
|
-
<Stack>
|
|
93
|
-
<TextInput
|
|
94
|
-
label="Administrator Username"
|
|
95
|
-
placeholder="admin"
|
|
96
|
-
required
|
|
97
|
-
value={username}
|
|
98
|
-
onChange={(e) => setUsername(e.target.value)}
|
|
99
|
-
/>
|
|
100
|
-
<PasswordInput
|
|
101
|
-
label="Password"
|
|
102
|
-
placeholder="Choose a strong password"
|
|
103
|
-
required
|
|
104
|
-
value={password}
|
|
105
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
106
|
-
/>
|
|
107
|
-
<Group mt="lg" justify="flex-end">
|
|
108
|
-
<Button
|
|
109
|
-
type="submit"
|
|
110
|
-
color="dark"
|
|
111
|
-
fullWidth
|
|
112
|
-
loading={loading}
|
|
113
|
-
leftSection={<IconShieldLock size={18} />}
|
|
114
|
-
>
|
|
115
|
-
Save and Start
|
|
116
|
-
</Button>
|
|
117
|
-
</Group>
|
|
118
|
-
</Stack>
|
|
119
|
-
</form>
|
|
120
|
-
</Paper>
|
|
121
|
-
<Text c="dimmed" size="xs" ta="center" mt="xl">
|
|
122
|
-
This configuration will be saved in sqlite-admin-config.json
|
|
123
|
-
</Text>
|
|
124
|
-
</Container>
|
|
125
|
-
</Box>
|
|
126
|
-
);
|
|
127
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { Group, Text, ActionIcon } from "@mantine/core";
|
|
2
|
-
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
|
|
3
|
-
|
|
4
|
-
export const Pagination = ({
|
|
5
|
-
page,
|
|
6
|
-
total,
|
|
7
|
-
limit = 50,
|
|
8
|
-
onPageChange
|
|
9
|
-
}) => {
|
|
10
|
-
const totalPages = Math.ceil(total / limit);
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<Group justify="flex-end" gap="xs" mt="xs">
|
|
14
|
-
<Text size="xs" c="dimmed">
|
|
15
|
-
Page {page} of {totalPages} ({total} records)
|
|
16
|
-
</Text>
|
|
17
|
-
<ActionIcon
|
|
18
|
-
variant="default"
|
|
19
|
-
size="sm"
|
|
20
|
-
disabled={page <= 1}
|
|
21
|
-
onClick={() => onPageChange(page - 1)}
|
|
22
|
-
>
|
|
23
|
-
<IconChevronLeft size={14} />
|
|
24
|
-
</ActionIcon>
|
|
25
|
-
<ActionIcon
|
|
26
|
-
variant="default"
|
|
27
|
-
size="sm"
|
|
28
|
-
disabled={page >= totalPages}
|
|
29
|
-
onClick={() => onPageChange(page + 1)}
|
|
30
|
-
>
|
|
31
|
-
<IconChevronRight size={14} />
|
|
32
|
-
</ActionIcon>
|
|
33
|
-
</Group>
|
|
34
|
-
);
|
|
35
|
-
};
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
2
|
-
import {
|
|
3
|
-
Modal,
|
|
4
|
-
Button,
|
|
5
|
-
Text,
|
|
6
|
-
Stack,
|
|
7
|
-
Group,
|
|
8
|
-
Image,
|
|
9
|
-
TextInput,
|
|
10
|
-
ThemeIcon,
|
|
11
|
-
Paper,
|
|
12
|
-
Alert,
|
|
13
|
-
Loader,
|
|
14
|
-
CopyButton,
|
|
15
|
-
ActionIcon,
|
|
16
|
-
Tooltip,
|
|
17
|
-
} from "@mantine/core";
|
|
18
|
-
import { notifications } from "@mantine/notifications";
|
|
19
|
-
import {
|
|
20
|
-
IconDeviceMobile,
|
|
21
|
-
IconCheck,
|
|
22
|
-
IconX,
|
|
23
|
-
IconCopy,
|
|
24
|
-
IconShieldLock,
|
|
25
|
-
IconAlertCircle,
|
|
26
|
-
} from "@tabler/icons-react";
|
|
27
|
-
|
|
28
|
-
export function SecuritySettings({ opened, onClose }) {
|
|
29
|
-
const [status, setStatus] = useState(null); // { enabled: boolean }
|
|
30
|
-
const [loading, setLoading] = useState(true);
|
|
31
|
-
|
|
32
|
-
// Setup Flow State
|
|
33
|
-
const [setupStep, setSetupStep] = useState(0); // 0: idle, 1: generating, 2: verifying
|
|
34
|
-
const [qrData, setQrData] = useState(null); // { secret, qrCode }
|
|
35
|
-
const [verifyCode, setVerifyCode] = useState("");
|
|
36
|
-
const [verifying, setVerifying] = useState(false);
|
|
37
|
-
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
if (opened) loadStatus();
|
|
40
|
-
}, [opened]);
|
|
41
|
-
|
|
42
|
-
const loadStatus = async () => {
|
|
43
|
-
setLoading(true);
|
|
44
|
-
try {
|
|
45
|
-
const res = await fetch("/admin/auth/status");
|
|
46
|
-
const data = await res.json();
|
|
47
|
-
setStatus(data);
|
|
48
|
-
} catch (e) {
|
|
49
|
-
notifications.show({
|
|
50
|
-
title: "Error",
|
|
51
|
-
message: "Failed to load status",
|
|
52
|
-
color: "red",
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
setLoading(false);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const startSetup = async () => {
|
|
59
|
-
setSetupStep(1); // generating
|
|
60
|
-
try {
|
|
61
|
-
const res = await fetch("/admin/api/totp/generate", { method: "POST" });
|
|
62
|
-
const data = await res.json();
|
|
63
|
-
if (data.success) {
|
|
64
|
-
setQrData(data);
|
|
65
|
-
setSetupStep(2);
|
|
66
|
-
} else {
|
|
67
|
-
notifications.show({
|
|
68
|
-
title: "Error",
|
|
69
|
-
message: "Failed to generate QR",
|
|
70
|
-
color: "red",
|
|
71
|
-
});
|
|
72
|
-
setSetupStep(0);
|
|
73
|
-
}
|
|
74
|
-
} catch (e) {
|
|
75
|
-
setSetupStep(0);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const verifyAndEnable = async () => {
|
|
80
|
-
if (!verifyCode) return;
|
|
81
|
-
setVerifying(true);
|
|
82
|
-
try {
|
|
83
|
-
const res = await fetch("/admin/api/totp/verify", {
|
|
84
|
-
method: "POST",
|
|
85
|
-
headers: { "Content-Type": "application/json" },
|
|
86
|
-
body: JSON.stringify({ secret: qrData.secret, code: verifyCode }),
|
|
87
|
-
});
|
|
88
|
-
const data = await res.json();
|
|
89
|
-
if (data.success) {
|
|
90
|
-
notifications.show({
|
|
91
|
-
title: "Success",
|
|
92
|
-
message: "2FA Enabled!",
|
|
93
|
-
color: "green",
|
|
94
|
-
});
|
|
95
|
-
setSetupStep(0);
|
|
96
|
-
loadStatus();
|
|
97
|
-
} else {
|
|
98
|
-
notifications.show({
|
|
99
|
-
title: "Error",
|
|
100
|
-
message: "Invalid code",
|
|
101
|
-
color: "red",
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
} catch (e) {
|
|
105
|
-
notifications.show({
|
|
106
|
-
title: "Error",
|
|
107
|
-
message: "Request failed",
|
|
108
|
-
color: "red",
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
setVerifying(false);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const disable2FA = async () => {
|
|
115
|
-
// In a real app we might ask for code confirmation here too,
|
|
116
|
-
// but for simplicity we'll assume session auth is enough or ask for code in a prompt
|
|
117
|
-
// For this UI, let's ask for code to confirm disable
|
|
118
|
-
const code = prompt("Enter your 2FA code to confirm disabling:");
|
|
119
|
-
if (!code) return;
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const res = await fetch("/admin/api/totp/disable", {
|
|
123
|
-
method: "POST",
|
|
124
|
-
headers: { "Content-Type": "application/json" },
|
|
125
|
-
body: JSON.stringify({ code }),
|
|
126
|
-
});
|
|
127
|
-
const data = await res.json();
|
|
128
|
-
if (data.success) {
|
|
129
|
-
notifications.show({
|
|
130
|
-
title: "Success",
|
|
131
|
-
message: "2FA Disabled",
|
|
132
|
-
color: "gray",
|
|
133
|
-
});
|
|
134
|
-
loadStatus();
|
|
135
|
-
} else {
|
|
136
|
-
notifications.show({
|
|
137
|
-
title: "Error",
|
|
138
|
-
message: data.error || "Failed to disable",
|
|
139
|
-
color: "red",
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
} catch (e) {
|
|
143
|
-
notifications.show({
|
|
144
|
-
title: "Error",
|
|
145
|
-
message: "Request failed",
|
|
146
|
-
color: "red",
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
return (
|
|
152
|
-
<Modal
|
|
153
|
-
opened={opened}
|
|
154
|
-
onClose={onClose}
|
|
155
|
-
title="Security Settings"
|
|
156
|
-
size="lg"
|
|
157
|
-
>
|
|
158
|
-
{loading ? (
|
|
159
|
-
<Stack align="center" py="xl">
|
|
160
|
-
<Loader />
|
|
161
|
-
</Stack>
|
|
162
|
-
) : (
|
|
163
|
-
<Stack>
|
|
164
|
-
<Group justify="space-between" align="flex-start">
|
|
165
|
-
<Group>
|
|
166
|
-
<ThemeIcon
|
|
167
|
-
size="xl"
|
|
168
|
-
radius="md"
|
|
169
|
-
color={status?.totpEnabled ? "green" : "gray"}
|
|
170
|
-
variant="light"
|
|
171
|
-
>
|
|
172
|
-
<IconDeviceMobile size={28} />
|
|
173
|
-
</ThemeIcon>
|
|
174
|
-
<div>
|
|
175
|
-
<Text fw={600}>Two-Factor Authentication (2FA)</Text>
|
|
176
|
-
<Text size="sm" c="dimmed">
|
|
177
|
-
{status?.totpEnabled
|
|
178
|
-
? "Your account is secured with 2FA."
|
|
179
|
-
: "Add an extra layer of security to your account."}
|
|
180
|
-
</Text>
|
|
181
|
-
</div>
|
|
182
|
-
</Group>
|
|
183
|
-
{status?.totpEnabled ? (
|
|
184
|
-
<Button color="red" variant="subtle" onClick={disable2FA}>
|
|
185
|
-
Disable
|
|
186
|
-
</Button>
|
|
187
|
-
) : (
|
|
188
|
-
setupStep === 0 && (
|
|
189
|
-
<Button color="dark" onClick={startSetup}>
|
|
190
|
-
Enable 2FA
|
|
191
|
-
</Button>
|
|
192
|
-
)
|
|
193
|
-
)}
|
|
194
|
-
</Group>
|
|
195
|
-
|
|
196
|
-
{/* Setup Wizard */}
|
|
197
|
-
{setupStep === 2 && qrData && (
|
|
198
|
-
<Paper withBorder p="md" radius="md" bg="gray.0">
|
|
199
|
-
<Stack align="center">
|
|
200
|
-
<Text fw={600}>1. Scan QR Code</Text>
|
|
201
|
-
<Text size="sm" c="dimmed" ta="center">
|
|
202
|
-
Use your authenticator app (Google Authenticator, Authy, etc.)
|
|
203
|
-
to scan this code.
|
|
204
|
-
</Text>
|
|
205
|
-
|
|
206
|
-
<div
|
|
207
|
-
style={{
|
|
208
|
-
background: "white",
|
|
209
|
-
padding: "10px",
|
|
210
|
-
borderRadius: "8px",
|
|
211
|
-
}}
|
|
212
|
-
>
|
|
213
|
-
<Image src={qrData.qrCode} w={180} h={180} />
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
<Group gap="xs">
|
|
217
|
-
<Text size="xs" c="dimmed">
|
|
218
|
-
Secret: {qrData.secret}
|
|
219
|
-
</Text>
|
|
220
|
-
<CopyButton value={qrData.secret}>
|
|
221
|
-
{({ copied, copy }) => (
|
|
222
|
-
<ActionIcon
|
|
223
|
-
variant="subtle"
|
|
224
|
-
color={copied ? "green" : "gray"}
|
|
225
|
-
onClick={copy}
|
|
226
|
-
size="xs"
|
|
227
|
-
>
|
|
228
|
-
{copied ? (
|
|
229
|
-
<IconCheck size={12} />
|
|
230
|
-
) : (
|
|
231
|
-
<IconCopy size={12} />
|
|
232
|
-
)}
|
|
233
|
-
</ActionIcon>
|
|
234
|
-
)}
|
|
235
|
-
</CopyButton>
|
|
236
|
-
</Group>
|
|
237
|
-
|
|
238
|
-
<Text fw={600} mt="md">
|
|
239
|
-
2. Verify Code
|
|
240
|
-
</Text>
|
|
241
|
-
<Group align="flex-start">
|
|
242
|
-
<TextInput
|
|
243
|
-
placeholder="000 000"
|
|
244
|
-
value={verifyCode}
|
|
245
|
-
onChange={(e) => setVerifyCode(e.target.value)}
|
|
246
|
-
maxLength={6}
|
|
247
|
-
w={140}
|
|
248
|
-
/>
|
|
249
|
-
<Button
|
|
250
|
-
onClick={verifyAndEnable}
|
|
251
|
-
loading={verifying}
|
|
252
|
-
color="dark"
|
|
253
|
-
>
|
|
254
|
-
Verify
|
|
255
|
-
</Button>
|
|
256
|
-
</Group>
|
|
257
|
-
|
|
258
|
-
<Button
|
|
259
|
-
variant="subtle"
|
|
260
|
-
size="xs"
|
|
261
|
-
color="gray"
|
|
262
|
-
onClick={() => setSetupStep(0)}
|
|
263
|
-
>
|
|
264
|
-
Cancel
|
|
265
|
-
</Button>
|
|
266
|
-
</Stack>
|
|
267
|
-
</Paper>
|
|
268
|
-
)}
|
|
269
|
-
</Stack>
|
|
270
|
-
)}
|
|
271
|
-
</Modal>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { Stack, Text, NavLink, ScrollArea } from "@mantine/core";
|
|
2
|
-
import { IconTable } from "@tabler/icons-react";
|
|
3
|
-
|
|
4
|
-
export const TableSelector = ({
|
|
5
|
-
tables,
|
|
6
|
-
favorites,
|
|
7
|
-
currentTable,
|
|
8
|
-
onSelectTable
|
|
9
|
-
}) => {
|
|
10
|
-
return (
|
|
11
|
-
<ScrollArea style={{ flex: 1 }}>
|
|
12
|
-
<Stack gap={0}>
|
|
13
|
-
<Text
|
|
14
|
-
size="xs"
|
|
15
|
-
fw={500}
|
|
16
|
-
c="#91918E"
|
|
17
|
-
px="xs"
|
|
18
|
-
mb={4}
|
|
19
|
-
mt="xs"
|
|
20
|
-
style={{
|
|
21
|
-
textTransform: "uppercase",
|
|
22
|
-
fontSize: "11px",
|
|
23
|
-
letterSpacing: "0.03em",
|
|
24
|
-
}}
|
|
25
|
-
>
|
|
26
|
-
Favorites
|
|
27
|
-
</Text>
|
|
28
|
-
{favorites.map((name) => (
|
|
29
|
-
<NavLink
|
|
30
|
-
key={name}
|
|
31
|
-
label={name}
|
|
32
|
-
leftSection={<IconTable size={16} />}
|
|
33
|
-
active={currentTable === name}
|
|
34
|
-
onClick={() => onSelectTable(name)}
|
|
35
|
-
style={{ borderRadius: 6 }}
|
|
36
|
-
/>
|
|
37
|
-
))}
|
|
38
|
-
|
|
39
|
-
{favorites.length === 0 && (
|
|
40
|
-
<Text size="xs" c="dimmed" px="sm" py={2} fs="italic">
|
|
41
|
-
No favorites
|
|
42
|
-
</Text>
|
|
43
|
-
)}
|
|
44
|
-
|
|
45
|
-
<Text
|
|
46
|
-
size="xs"
|
|
47
|
-
fw={500}
|
|
48
|
-
c="#91918E"
|
|
49
|
-
px="xs"
|
|
50
|
-
mb={4}
|
|
51
|
-
mt="lg"
|
|
52
|
-
style={{
|
|
53
|
-
textTransform: "uppercase",
|
|
54
|
-
fontSize: "11px",
|
|
55
|
-
letterSpacing: "0.03em",
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
Tables
|
|
59
|
-
</Text>
|
|
60
|
-
{tables
|
|
61
|
-
.filter((t) => !favorites.includes(t.name))
|
|
62
|
-
.map((t) => (
|
|
63
|
-
<NavLink
|
|
64
|
-
key={t.name}
|
|
65
|
-
label={t.name}
|
|
66
|
-
leftSection={<IconTable size={16} />}
|
|
67
|
-
active={currentTable === t.name}
|
|
68
|
-
onClick={() => onSelectTable(t.name)}
|
|
69
|
-
style={{ borderRadius: 6 }}
|
|
70
|
-
/>
|
|
71
|
-
))}
|
|
72
|
-
</Stack>
|
|
73
|
-
</ScrollArea>
|
|
74
|
-
);
|
|
75
|
-
};
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { useState, useMemo } from "react";
|
|
2
|
-
|
|
3
|
-
export function useFilter(items, filterKeys = []) {
|
|
4
|
-
const [query, setQuery] = useState("");
|
|
5
|
-
const [activeFilters, setActiveFilters] = useState({});
|
|
6
|
-
const [logicalOperators, setLogicalOperators] = useState({}); // Stores AND/OR between filters
|
|
7
|
-
|
|
8
|
-
// Helper function to apply operator
|
|
9
|
-
const applyOperator = (elementValue, operator, filterValue) => {
|
|
10
|
-
const elemStr = String(elementValue).toLowerCase();
|
|
11
|
-
const filterStr = String(filterValue).toLowerCase();
|
|
12
|
-
const elemNum = Number(elementValue);
|
|
13
|
-
const filterNum = Number(filterValue);
|
|
14
|
-
|
|
15
|
-
switch (operator) {
|
|
16
|
-
case "=":
|
|
17
|
-
return elemStr === filterStr;
|
|
18
|
-
case "!=":
|
|
19
|
-
return elemStr !== filterStr;
|
|
20
|
-
case "contains":
|
|
21
|
-
return elemStr.includes(filterStr);
|
|
22
|
-
case "not_contains":
|
|
23
|
-
return !elemStr.includes(filterStr);
|
|
24
|
-
case ">":
|
|
25
|
-
return !isNaN(elemNum) && !isNaN(filterNum) && elemNum > filterNum;
|
|
26
|
-
case "<":
|
|
27
|
-
return !isNaN(elemNum) && !isNaN(filterNum) && elemNum < filterNum;
|
|
28
|
-
case ">=":
|
|
29
|
-
return !isNaN(elemNum) && !isNaN(filterNum) && elemNum >= filterNum;
|
|
30
|
-
case "<=":
|
|
31
|
-
return !isNaN(elemNum) && !isNaN(filterNum) && elemNum <= filterNum;
|
|
32
|
-
default:
|
|
33
|
-
return elemStr.includes(filterStr);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Filter elements based on active filters + query + logical operators
|
|
38
|
-
const filteredItems = useMemo(() => {
|
|
39
|
-
if (!items) return [];
|
|
40
|
-
|
|
41
|
-
return items.filter((element) => {
|
|
42
|
-
// Free text filter (query)
|
|
43
|
-
if (query.trim()) {
|
|
44
|
-
const searchTerm = query.toLowerCase();
|
|
45
|
-
// If filterKeys are provided, search only in those keys, otherwise search in all values
|
|
46
|
-
const searchKeys =
|
|
47
|
-
filterKeys.length > 0 ? filterKeys : Object.keys(element);
|
|
48
|
-
|
|
49
|
-
const matchesSearch = searchKeys.some((key) => {
|
|
50
|
-
const val = element[key];
|
|
51
|
-
return val && String(val).toLowerCase().includes(searchTerm);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (!matchesSearch) return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Key:operator:value filters (active chips)
|
|
58
|
-
const activeFilterKeys = Object.keys(activeFilters);
|
|
59
|
-
if (activeFilterKeys.length === 0) return true;
|
|
60
|
-
|
|
61
|
-
// Evaluate each filter
|
|
62
|
-
const filterResults = {};
|
|
63
|
-
for (const key of activeFilterKeys) {
|
|
64
|
-
const valueStr = activeFilters[key];
|
|
65
|
-
const values = valueStr.split(",").map((v) => v.trim());
|
|
66
|
-
let matches = false;
|
|
67
|
-
|
|
68
|
-
for (const value of values) {
|
|
69
|
-
let operator = "=";
|
|
70
|
-
let filterValue = value;
|
|
71
|
-
|
|
72
|
-
const opMatch = value.match(
|
|
73
|
-
/^([=!><]+|contains|not_contains)\s*(.*)$/
|
|
74
|
-
);
|
|
75
|
-
if (opMatch) {
|
|
76
|
-
operator = opMatch[1];
|
|
77
|
-
filterValue = opMatch[2];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const elementValue = element[key];
|
|
81
|
-
if (
|
|
82
|
-
elementValue !== undefined &&
|
|
83
|
-
applyOperator(elementValue, operator, filterValue)
|
|
84
|
-
) {
|
|
85
|
-
matches = true;
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
filterResults[key] = matches;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Apply logical operators between filters
|
|
93
|
-
let result = filterResults[activeFilterKeys[0]];
|
|
94
|
-
for (let i = 1; i < activeFilterKeys.length; i++) {
|
|
95
|
-
const logicalOp = logicalOperators[activeFilterKeys[i - 1]] || "AND";
|
|
96
|
-
if (logicalOp === "OR") {
|
|
97
|
-
result = result || filterResults[activeFilterKeys[i]];
|
|
98
|
-
} else {
|
|
99
|
-
result = result && filterResults[activeFilterKeys[i]];
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return result;
|
|
104
|
-
});
|
|
105
|
-
}, [items, query, activeFilters, logicalOperators, filterKeys]);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
filteredItems,
|
|
109
|
-
data: {
|
|
110
|
-
query,
|
|
111
|
-
activeFilters,
|
|
112
|
-
logicalOperators,
|
|
113
|
-
},
|
|
114
|
-
setData: {
|
|
115
|
-
setQuery,
|
|
116
|
-
setActiveFilters,
|
|
117
|
-
setLogicalOperators,
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
}
|