create-ekka-desktop-app 0.2.2
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 +137 -0
- package/bin/cli.js +72 -0
- package/package.json +23 -0
- package/template/branding/app.json +6 -0
- package/template/branding/icon.icns +0 -0
- package/template/eslint.config.js +98 -0
- package/template/index.html +29 -0
- package/template/package.json +40 -0
- package/template/src/app/App.tsx +24 -0
- package/template/src/demo/DemoApp.tsx +260 -0
- package/template/src/demo/components/Banner.tsx +82 -0
- package/template/src/demo/components/EmptyState.tsx +61 -0
- package/template/src/demo/components/InfoPopover.tsx +171 -0
- package/template/src/demo/components/InfoTooltip.tsx +76 -0
- package/template/src/demo/components/LearnMore.tsx +98 -0
- package/template/src/demo/components/NodeCredentialsOnboarding.tsx +219 -0
- package/template/src/demo/components/SetupWizard.tsx +48 -0
- package/template/src/demo/components/StatusBadge.tsx +83 -0
- package/template/src/demo/components/index.ts +10 -0
- package/template/src/demo/hooks/index.ts +6 -0
- package/template/src/demo/hooks/useAuditEvents.ts +30 -0
- package/template/src/demo/layout/Shell.tsx +110 -0
- package/template/src/demo/layout/Sidebar.tsx +192 -0
- package/template/src/demo/pages/AuditLogPage.tsx +235 -0
- package/template/src/demo/pages/DocGenPage.tsx +874 -0
- package/template/src/demo/pages/HomeSetupPage.tsx +182 -0
- package/template/src/demo/pages/LoginPage.tsx +192 -0
- package/template/src/demo/pages/PathPermissionsPage.tsx +873 -0
- package/template/src/demo/pages/RunnerPage.tsx +445 -0
- package/template/src/demo/pages/SystemPage.tsx +557 -0
- package/template/src/demo/pages/VaultPage.tsx +805 -0
- package/template/src/ekka/__tests__/demo-backend.test.ts +187 -0
- package/template/src/ekka/audit/index.ts +7 -0
- package/template/src/ekka/audit/store.ts +68 -0
- package/template/src/ekka/audit/types.ts +22 -0
- package/template/src/ekka/auth/client.ts +212 -0
- package/template/src/ekka/auth/index.ts +30 -0
- package/template/src/ekka/auth/storage.ts +114 -0
- package/template/src/ekka/auth/types.ts +67 -0
- package/template/src/ekka/backend/demo.ts +151 -0
- package/template/src/ekka/backend/interface.ts +36 -0
- package/template/src/ekka/config.ts +48 -0
- package/template/src/ekka/constants.ts +143 -0
- package/template/src/ekka/errors.ts +54 -0
- package/template/src/ekka/index.ts +516 -0
- package/template/src/ekka/internal/backend.ts +156 -0
- package/template/src/ekka/internal/index.ts +7 -0
- package/template/src/ekka/ops/auth.ts +29 -0
- package/template/src/ekka/ops/debug.ts +68 -0
- package/template/src/ekka/ops/home.ts +101 -0
- package/template/src/ekka/ops/index.ts +16 -0
- package/template/src/ekka/ops/nodeCredentials.ts +131 -0
- package/template/src/ekka/ops/nodeSession.ts +145 -0
- package/template/src/ekka/ops/paths.ts +183 -0
- package/template/src/ekka/ops/runner.ts +86 -0
- package/template/src/ekka/ops/runtime.ts +31 -0
- package/template/src/ekka/ops/setup.ts +47 -0
- package/template/src/ekka/ops/vault.ts +459 -0
- package/template/src/ekka/ops/workflowRuns.ts +116 -0
- package/template/src/ekka/types.ts +82 -0
- package/template/src/ekka/utils/idempotency.ts +14 -0
- package/template/src/ekka/utils/index.ts +7 -0
- package/template/src/ekka/utils/time.ts +77 -0
- package/template/src/main.tsx +12 -0
- package/template/src/vite-env.d.ts +12 -0
- package/template/src-tauri/Cargo.toml +41 -0
- package/template/src-tauri/build.rs +3 -0
- package/template/src-tauri/capabilities/default.json +11 -0
- package/template/src-tauri/icons/icon.icns +0 -0
- package/template/src-tauri/icons/icon.png +0 -0
- package/template/src-tauri/resources/ekka-engine-bootstrap +0 -0
- package/template/src-tauri/src/bootstrap.rs +37 -0
- package/template/src-tauri/src/commands.rs +1215 -0
- package/template/src-tauri/src/device_secret.rs +111 -0
- package/template/src-tauri/src/engine_process.rs +538 -0
- package/template/src-tauri/src/grants.rs +129 -0
- package/template/src-tauri/src/handlers/home.rs +65 -0
- package/template/src-tauri/src/handlers/mod.rs +7 -0
- package/template/src-tauri/src/handlers/paths.rs +128 -0
- package/template/src-tauri/src/handlers/vault.rs +680 -0
- package/template/src-tauri/src/main.rs +243 -0
- package/template/src-tauri/src/node_auth.rs +858 -0
- package/template/src-tauri/src/node_credentials.rs +541 -0
- package/template/src-tauri/src/node_runner.rs +882 -0
- package/template/src-tauri/src/node_vault_crypto.rs +113 -0
- package/template/src-tauri/src/node_vault_store.rs +267 -0
- package/template/src-tauri/src/ops/auth.rs +50 -0
- package/template/src-tauri/src/ops/home.rs +251 -0
- package/template/src-tauri/src/ops/mod.rs +7 -0
- package/template/src-tauri/src/ops/runtime.rs +21 -0
- package/template/src-tauri/src/state.rs +639 -0
- package/template/src-tauri/src/types.rs +84 -0
- package/template/src-tauri/tauri.conf.json +41 -0
- package/template/tsconfig.json +26 -0
- package/template/tsconfig.tsbuildinfo +1 -0
- package/template/vite.config.ts +34 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path Permissions Page
|
|
3
|
+
*
|
|
4
|
+
* Demo tab showing how EKKA manages filesystem access using engine-signed grants.
|
|
5
|
+
* NO FILE I/O - only permission management demonstration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback, type CSSProperties, type ReactElement } from 'react';
|
|
9
|
+
import { ekka, advanced, type PathInfo, type PathType, type PathAccess, type PathGrantResult } from '../../ekka';
|
|
10
|
+
import { InfoPopover } from '../components/InfoPopover';
|
|
11
|
+
|
|
12
|
+
interface PathPermissionsPageProps {
|
|
13
|
+
darkMode: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PermissionCheck {
|
|
17
|
+
allowed: boolean;
|
|
18
|
+
reason: string;
|
|
19
|
+
pathType: PathType | null;
|
|
20
|
+
access: PathAccess | null;
|
|
21
|
+
grantedBy: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function PathPermissionsPage({ darkMode }: PathPermissionsPageProps): ReactElement {
|
|
25
|
+
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
|
26
|
+
const [pathInput, setPathInput] = useState('');
|
|
27
|
+
const [permissionStatus, setPermissionStatus] = useState<PermissionCheck | null>(null);
|
|
28
|
+
const [grantResult, setGrantResult] = useState<PathGrantResult | null>(null);
|
|
29
|
+
const [allPaths, setAllPaths] = useState<PathInfo[]>([]);
|
|
30
|
+
const [loading, setLoading] = useState(false);
|
|
31
|
+
const [error, setError] = useState<string | null>(null);
|
|
32
|
+
const [requestedAccess, setRequestedAccess] = useState<PathAccess>('READ_WRITE');
|
|
33
|
+
|
|
34
|
+
const colors = {
|
|
35
|
+
text: darkMode ? '#ffffff' : '#1d1d1f',
|
|
36
|
+
textMuted: darkMode ? '#98989d' : '#6e6e73',
|
|
37
|
+
textDim: darkMode ? '#636366' : '#aeaeb2',
|
|
38
|
+
bg: darkMode ? '#2c2c2e' : '#fafafa',
|
|
39
|
+
bgAlt: darkMode ? '#1c1c1e' : '#ffffff',
|
|
40
|
+
bgInput: darkMode ? '#3a3a3c' : '#ffffff',
|
|
41
|
+
border: darkMode ? '#3a3a3c' : '#e5e5e5',
|
|
42
|
+
accent: darkMode ? '#0a84ff' : '#007aff',
|
|
43
|
+
green: darkMode ? '#30d158' : '#34c759',
|
|
44
|
+
orange: darkMode ? '#ff9f0a' : '#ff9500',
|
|
45
|
+
red: darkMode ? '#ff453a' : '#ff3b30',
|
|
46
|
+
purple: darkMode ? '#bf5af2' : '#af52de',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const styles: Record<string, CSSProperties> = {
|
|
50
|
+
container: {
|
|
51
|
+
width: '100%',
|
|
52
|
+
},
|
|
53
|
+
header: {
|
|
54
|
+
marginBottom: '32px',
|
|
55
|
+
},
|
|
56
|
+
title: {
|
|
57
|
+
fontSize: '28px',
|
|
58
|
+
fontWeight: 700,
|
|
59
|
+
color: colors.text,
|
|
60
|
+
marginBottom: '8px',
|
|
61
|
+
letterSpacing: '-0.02em',
|
|
62
|
+
},
|
|
63
|
+
subtitle: {
|
|
64
|
+
fontSize: '14px',
|
|
65
|
+
color: colors.textMuted,
|
|
66
|
+
lineHeight: 1.6,
|
|
67
|
+
maxWidth: '600px',
|
|
68
|
+
},
|
|
69
|
+
section: {
|
|
70
|
+
marginBottom: '28px',
|
|
71
|
+
},
|
|
72
|
+
sectionHeader: {
|
|
73
|
+
display: 'flex',
|
|
74
|
+
alignItems: 'center',
|
|
75
|
+
gap: '8px',
|
|
76
|
+
marginBottom: '12px',
|
|
77
|
+
},
|
|
78
|
+
sectionTitle: {
|
|
79
|
+
fontSize: '11px',
|
|
80
|
+
fontWeight: 600,
|
|
81
|
+
color: colors.textMuted,
|
|
82
|
+
textTransform: 'uppercase' as const,
|
|
83
|
+
letterSpacing: '0.05em',
|
|
84
|
+
},
|
|
85
|
+
sectionLine: {
|
|
86
|
+
flex: 1,
|
|
87
|
+
height: '1px',
|
|
88
|
+
background: colors.border,
|
|
89
|
+
},
|
|
90
|
+
card: {
|
|
91
|
+
background: colors.bg,
|
|
92
|
+
border: `1px solid ${colors.border}`,
|
|
93
|
+
borderRadius: '12px',
|
|
94
|
+
padding: '20px',
|
|
95
|
+
},
|
|
96
|
+
pathSelector: {
|
|
97
|
+
display: 'flex',
|
|
98
|
+
gap: '12px',
|
|
99
|
+
alignItems: 'flex-start',
|
|
100
|
+
flexWrap: 'wrap' as const,
|
|
101
|
+
},
|
|
102
|
+
input: {
|
|
103
|
+
flex: '1 1 300px',
|
|
104
|
+
minWidth: '200px',
|
|
105
|
+
padding: '10px 14px',
|
|
106
|
+
fontSize: '13px',
|
|
107
|
+
fontFamily: 'SF Mono, Monaco, Consolas, monospace',
|
|
108
|
+
background: colors.bgInput,
|
|
109
|
+
border: `1px solid ${colors.border}`,
|
|
110
|
+
borderRadius: '8px',
|
|
111
|
+
color: colors.text,
|
|
112
|
+
outline: 'none',
|
|
113
|
+
},
|
|
114
|
+
button: {
|
|
115
|
+
padding: '10px 20px',
|
|
116
|
+
fontSize: '13px',
|
|
117
|
+
fontWeight: 600,
|
|
118
|
+
color: '#ffffff',
|
|
119
|
+
background: colors.accent,
|
|
120
|
+
border: 'none',
|
|
121
|
+
borderRadius: '8px',
|
|
122
|
+
cursor: 'pointer',
|
|
123
|
+
transition: 'opacity 0.15s ease',
|
|
124
|
+
whiteSpace: 'nowrap' as const,
|
|
125
|
+
},
|
|
126
|
+
buttonSecondary: {
|
|
127
|
+
padding: '10px 20px',
|
|
128
|
+
fontSize: '13px',
|
|
129
|
+
fontWeight: 600,
|
|
130
|
+
color: colors.accent,
|
|
131
|
+
background: darkMode ? 'rgba(10, 132, 255, 0.15)' : 'rgba(0, 122, 255, 0.1)',
|
|
132
|
+
border: 'none',
|
|
133
|
+
borderRadius: '8px',
|
|
134
|
+
cursor: 'pointer',
|
|
135
|
+
transition: 'opacity 0.15s ease',
|
|
136
|
+
whiteSpace: 'nowrap' as const,
|
|
137
|
+
},
|
|
138
|
+
buttonDanger: {
|
|
139
|
+
padding: '10px 20px',
|
|
140
|
+
fontSize: '13px',
|
|
141
|
+
fontWeight: 600,
|
|
142
|
+
color: colors.red,
|
|
143
|
+
background: darkMode ? 'rgba(255, 69, 58, 0.15)' : 'rgba(255, 59, 48, 0.1)',
|
|
144
|
+
border: 'none',
|
|
145
|
+
borderRadius: '8px',
|
|
146
|
+
cursor: 'pointer',
|
|
147
|
+
transition: 'opacity 0.15s ease',
|
|
148
|
+
whiteSpace: 'nowrap' as const,
|
|
149
|
+
},
|
|
150
|
+
buttonDisabled: {
|
|
151
|
+
opacity: 0.5,
|
|
152
|
+
cursor: 'not-allowed',
|
|
153
|
+
},
|
|
154
|
+
selectedPath: {
|
|
155
|
+
marginTop: '16px',
|
|
156
|
+
padding: '14px 16px',
|
|
157
|
+
background: darkMode ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.02)',
|
|
158
|
+
borderRadius: '8px',
|
|
159
|
+
},
|
|
160
|
+
selectedPathLabel: {
|
|
161
|
+
fontSize: '11px',
|
|
162
|
+
fontWeight: 600,
|
|
163
|
+
color: colors.textMuted,
|
|
164
|
+
textTransform: 'uppercase' as const,
|
|
165
|
+
letterSpacing: '0.04em',
|
|
166
|
+
marginBottom: '6px',
|
|
167
|
+
},
|
|
168
|
+
selectedPathValue: {
|
|
169
|
+
fontSize: '13px',
|
|
170
|
+
fontFamily: 'SF Mono, Monaco, Consolas, monospace',
|
|
171
|
+
color: colors.text,
|
|
172
|
+
wordBreak: 'break-all' as const,
|
|
173
|
+
},
|
|
174
|
+
statusCard: {
|
|
175
|
+
display: 'flex',
|
|
176
|
+
alignItems: 'center',
|
|
177
|
+
gap: '16px',
|
|
178
|
+
padding: '20px',
|
|
179
|
+
background: colors.bg,
|
|
180
|
+
border: `1px solid ${colors.border}`,
|
|
181
|
+
borderRadius: '12px',
|
|
182
|
+
},
|
|
183
|
+
statusIcon: {
|
|
184
|
+
width: '48px',
|
|
185
|
+
height: '48px',
|
|
186
|
+
borderRadius: '12px',
|
|
187
|
+
display: 'flex',
|
|
188
|
+
alignItems: 'center',
|
|
189
|
+
justifyContent: 'center',
|
|
190
|
+
flexShrink: 0,
|
|
191
|
+
},
|
|
192
|
+
statusContent: {
|
|
193
|
+
flex: 1,
|
|
194
|
+
},
|
|
195
|
+
statusTitle: {
|
|
196
|
+
fontSize: '16px',
|
|
197
|
+
fontWeight: 600,
|
|
198
|
+
color: colors.text,
|
|
199
|
+
marginBottom: '4px',
|
|
200
|
+
},
|
|
201
|
+
statusReason: {
|
|
202
|
+
fontSize: '13px',
|
|
203
|
+
color: colors.textMuted,
|
|
204
|
+
},
|
|
205
|
+
grantPanel: {
|
|
206
|
+
marginTop: '20px',
|
|
207
|
+
padding: '20px',
|
|
208
|
+
background: darkMode ? 'rgba(48, 209, 88, 0.08)' : 'rgba(52, 199, 89, 0.06)',
|
|
209
|
+
borderRadius: '12px',
|
|
210
|
+
border: `1px solid ${darkMode ? 'rgba(48, 209, 88, 0.2)' : 'rgba(52, 199, 89, 0.15)'}`,
|
|
211
|
+
},
|
|
212
|
+
grantTitle: {
|
|
213
|
+
fontSize: '15px',
|
|
214
|
+
fontWeight: 600,
|
|
215
|
+
color: colors.green,
|
|
216
|
+
marginBottom: '12px',
|
|
217
|
+
display: 'flex',
|
|
218
|
+
alignItems: 'center',
|
|
219
|
+
gap: '8px',
|
|
220
|
+
},
|
|
221
|
+
grantDetails: {
|
|
222
|
+
display: 'grid',
|
|
223
|
+
gap: '8px',
|
|
224
|
+
},
|
|
225
|
+
grantRow: {
|
|
226
|
+
display: 'flex',
|
|
227
|
+
gap: '12px',
|
|
228
|
+
fontSize: '13px',
|
|
229
|
+
},
|
|
230
|
+
grantLabel: {
|
|
231
|
+
color: colors.textMuted,
|
|
232
|
+
minWidth: '100px',
|
|
233
|
+
},
|
|
234
|
+
grantValue: {
|
|
235
|
+
color: colors.text,
|
|
236
|
+
fontFamily: 'SF Mono, Monaco, Consolas, monospace',
|
|
237
|
+
wordBreak: 'break-all' as const,
|
|
238
|
+
},
|
|
239
|
+
table: {
|
|
240
|
+
width: '100%',
|
|
241
|
+
minWidth: '600px',
|
|
242
|
+
borderCollapse: 'collapse' as const,
|
|
243
|
+
},
|
|
244
|
+
tableHeader: {
|
|
245
|
+
textAlign: 'left' as const,
|
|
246
|
+
padding: '12px 16px',
|
|
247
|
+
fontSize: '11px',
|
|
248
|
+
fontWeight: 600,
|
|
249
|
+
color: colors.textMuted,
|
|
250
|
+
textTransform: 'uppercase' as const,
|
|
251
|
+
letterSpacing: '0.04em',
|
|
252
|
+
borderBottom: `1px solid ${colors.border}`,
|
|
253
|
+
},
|
|
254
|
+
tableCell: {
|
|
255
|
+
padding: '14px 16px',
|
|
256
|
+
fontSize: '13px',
|
|
257
|
+
color: colors.text,
|
|
258
|
+
borderBottom: `1px solid ${colors.border}`,
|
|
259
|
+
},
|
|
260
|
+
tableCellMono: {
|
|
261
|
+
padding: '14px 16px',
|
|
262
|
+
fontSize: '12px',
|
|
263
|
+
fontFamily: 'SF Mono, Monaco, Consolas, monospace',
|
|
264
|
+
color: colors.text,
|
|
265
|
+
borderBottom: `1px solid ${colors.border}`,
|
|
266
|
+
maxWidth: '300px',
|
|
267
|
+
overflow: 'hidden',
|
|
268
|
+
textOverflow: 'ellipsis',
|
|
269
|
+
whiteSpace: 'nowrap' as const,
|
|
270
|
+
},
|
|
271
|
+
badge: {
|
|
272
|
+
display: 'inline-flex',
|
|
273
|
+
alignItems: 'center',
|
|
274
|
+
gap: '6px',
|
|
275
|
+
padding: '4px 10px',
|
|
276
|
+
borderRadius: '6px',
|
|
277
|
+
fontSize: '11px',
|
|
278
|
+
fontWeight: 600,
|
|
279
|
+
},
|
|
280
|
+
badgeGreen: {
|
|
281
|
+
background: darkMode ? 'rgba(48, 209, 88, 0.15)' : 'rgba(52, 199, 89, 0.12)',
|
|
282
|
+
color: colors.green,
|
|
283
|
+
},
|
|
284
|
+
badgeBlue: {
|
|
285
|
+
background: darkMode ? 'rgba(10, 132, 255, 0.15)' : 'rgba(0, 122, 255, 0.12)',
|
|
286
|
+
color: colors.accent,
|
|
287
|
+
},
|
|
288
|
+
badgeOrange: {
|
|
289
|
+
background: darkMode ? 'rgba(255, 159, 10, 0.15)' : 'rgba(255, 149, 0, 0.12)',
|
|
290
|
+
color: colors.orange,
|
|
291
|
+
},
|
|
292
|
+
emptyState: {
|
|
293
|
+
textAlign: 'center' as const,
|
|
294
|
+
padding: '40px 20px',
|
|
295
|
+
color: colors.textMuted,
|
|
296
|
+
fontSize: '14px',
|
|
297
|
+
},
|
|
298
|
+
error: {
|
|
299
|
+
marginBottom: '20px',
|
|
300
|
+
padding: '12px 14px',
|
|
301
|
+
background: darkMode ? '#3c1618' : '#fef2f2',
|
|
302
|
+
border: `1px solid ${darkMode ? '#7f1d1d' : '#fecaca'}`,
|
|
303
|
+
borderRadius: '8px',
|
|
304
|
+
fontSize: '13px',
|
|
305
|
+
color: darkMode ? '#fca5a5' : '#991b1b',
|
|
306
|
+
},
|
|
307
|
+
actions: {
|
|
308
|
+
display: 'flex',
|
|
309
|
+
gap: '12px',
|
|
310
|
+
marginTop: '20px',
|
|
311
|
+
flexWrap: 'wrap' as const,
|
|
312
|
+
alignItems: 'center',
|
|
313
|
+
},
|
|
314
|
+
accessSelector: {
|
|
315
|
+
display: 'flex',
|
|
316
|
+
gap: '8px',
|
|
317
|
+
padding: '16px',
|
|
318
|
+
marginTop: '16px',
|
|
319
|
+
background: darkMode ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.02)',
|
|
320
|
+
borderRadius: '8px',
|
|
321
|
+
},
|
|
322
|
+
accessOption: {
|
|
323
|
+
display: 'flex',
|
|
324
|
+
alignItems: 'center',
|
|
325
|
+
gap: '8px',
|
|
326
|
+
padding: '10px 16px',
|
|
327
|
+
background: 'transparent',
|
|
328
|
+
border: `1px solid ${colors.border}`,
|
|
329
|
+
borderRadius: '8px',
|
|
330
|
+
cursor: 'pointer',
|
|
331
|
+
fontSize: '13px',
|
|
332
|
+
fontWeight: 500,
|
|
333
|
+
color: colors.text,
|
|
334
|
+
transition: 'all 0.15s ease',
|
|
335
|
+
},
|
|
336
|
+
accessOptionSelected: {
|
|
337
|
+
background: colors.accent,
|
|
338
|
+
borderColor: colors.accent,
|
|
339
|
+
color: '#ffffff',
|
|
340
|
+
},
|
|
341
|
+
tableDangerButton: {
|
|
342
|
+
display: 'flex',
|
|
343
|
+
alignItems: 'center',
|
|
344
|
+
justifyContent: 'center',
|
|
345
|
+
width: '32px',
|
|
346
|
+
height: '32px',
|
|
347
|
+
padding: 0,
|
|
348
|
+
background: 'transparent',
|
|
349
|
+
border: 'none',
|
|
350
|
+
borderRadius: '6px',
|
|
351
|
+
cursor: 'pointer',
|
|
352
|
+
color: colors.red,
|
|
353
|
+
opacity: 0.7,
|
|
354
|
+
transition: 'opacity 0.15s ease, background 0.15s ease',
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// Load all paths on mount and after changes
|
|
359
|
+
const loadPaths = useCallback(async () => {
|
|
360
|
+
try {
|
|
361
|
+
const paths = await ekka.paths.list();
|
|
362
|
+
setAllPaths(paths);
|
|
363
|
+
} catch {
|
|
364
|
+
// Ignore errors when loading paths
|
|
365
|
+
}
|
|
366
|
+
}, []);
|
|
367
|
+
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
void loadPaths();
|
|
370
|
+
}, [loadPaths]);
|
|
371
|
+
|
|
372
|
+
// Check permission status when path is selected
|
|
373
|
+
const checkPermission = useCallback(async (path: string) => {
|
|
374
|
+
setLoading(true);
|
|
375
|
+
setError(null);
|
|
376
|
+
setGrantResult(null);
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
// Use advanced API to get detailed check result
|
|
380
|
+
const result = await advanced.paths.check(path, 'read');
|
|
381
|
+
setPermissionStatus(result);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
const message = err instanceof Error ? err.message : 'Failed to check permission';
|
|
384
|
+
setError(message);
|
|
385
|
+
setPermissionStatus(null);
|
|
386
|
+
} finally {
|
|
387
|
+
setLoading(false);
|
|
388
|
+
}
|
|
389
|
+
}, []);
|
|
390
|
+
|
|
391
|
+
// Handle folder selection
|
|
392
|
+
const handleBrowseFolder = async () => {
|
|
393
|
+
setError(null);
|
|
394
|
+
try {
|
|
395
|
+
const { open } = await import('@tauri-apps/plugin-dialog');
|
|
396
|
+
const selected = await open({ directory: true, multiple: false });
|
|
397
|
+
if (selected && typeof selected === 'string') {
|
|
398
|
+
setSelectedPath(selected);
|
|
399
|
+
setPathInput(selected);
|
|
400
|
+
await checkPermission(selected);
|
|
401
|
+
}
|
|
402
|
+
} catch (err) {
|
|
403
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
404
|
+
setError(`Dialog error: ${message}`);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Handle file selection
|
|
409
|
+
const handleBrowseFile = async () => {
|
|
410
|
+
setError(null);
|
|
411
|
+
try {
|
|
412
|
+
const { open } = await import('@tauri-apps/plugin-dialog');
|
|
413
|
+
const selected = await open({ directory: false, multiple: false });
|
|
414
|
+
if (selected && typeof selected === 'string') {
|
|
415
|
+
setSelectedPath(selected);
|
|
416
|
+
setPathInput(selected);
|
|
417
|
+
await checkPermission(selected);
|
|
418
|
+
}
|
|
419
|
+
} catch (err) {
|
|
420
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
421
|
+
setError(`Dialog error: ${message}`);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Handle manual path entry
|
|
426
|
+
const handlePathSubmit = async () => {
|
|
427
|
+
if (pathInput.trim()) {
|
|
428
|
+
setSelectedPath(pathInput.trim());
|
|
429
|
+
await checkPermission(pathInput.trim());
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Request permission
|
|
434
|
+
const handleRequestPermission = async () => {
|
|
435
|
+
if (!selectedPath) return;
|
|
436
|
+
|
|
437
|
+
setLoading(true);
|
|
438
|
+
setError(null);
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const result = await ekka.paths.allow(selectedPath, {
|
|
442
|
+
pathType: 'WORKSPACE',
|
|
443
|
+
access: requestedAccess,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
setGrantResult(result);
|
|
447
|
+
|
|
448
|
+
if (result.success) {
|
|
449
|
+
// Re-check permission status
|
|
450
|
+
await checkPermission(selectedPath);
|
|
451
|
+
// Refresh paths list
|
|
452
|
+
await loadPaths();
|
|
453
|
+
} else if (result.error) {
|
|
454
|
+
setError(result.error);
|
|
455
|
+
}
|
|
456
|
+
} catch (err) {
|
|
457
|
+
const message = err instanceof Error ? err.message : 'Failed to request permission';
|
|
458
|
+
setError(message);
|
|
459
|
+
} finally {
|
|
460
|
+
setLoading(false);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Revoke permission for selected path (uses the granting path, not selected path)
|
|
465
|
+
const handleRevokePermission = async () => {
|
|
466
|
+
const pathToRevoke = permissionStatus?.grantedBy || selectedPath;
|
|
467
|
+
if (!pathToRevoke) return;
|
|
468
|
+
await handleRevokeByPath(pathToRevoke);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// Revoke permission by path
|
|
472
|
+
const handleRevokeByPath = async (path: string) => {
|
|
473
|
+
setLoading(true);
|
|
474
|
+
setError(null);
|
|
475
|
+
setGrantResult(null);
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
await ekka.paths.remove(path);
|
|
479
|
+
// Re-check permission status if we have a selected path
|
|
480
|
+
if (selectedPath) {
|
|
481
|
+
await checkPermission(selectedPath);
|
|
482
|
+
}
|
|
483
|
+
// Refresh paths list
|
|
484
|
+
await loadPaths();
|
|
485
|
+
} catch (err) {
|
|
486
|
+
const message = err instanceof Error ? err.message : 'Failed to revoke permission';
|
|
487
|
+
setError(message);
|
|
488
|
+
} finally {
|
|
489
|
+
setLoading(false);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const formatPathType = (type: PathType): string => {
|
|
494
|
+
return type.replace(/_/g, ' ');
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const formatAccess = (access: PathAccess): string => {
|
|
498
|
+
return access === 'READ_WRITE' ? 'Read/Write' : 'Read Only';
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const formatDate = (dateStr: string): string => {
|
|
502
|
+
try {
|
|
503
|
+
const date = new Date(dateStr);
|
|
504
|
+
return date.toLocaleDateString(undefined, {
|
|
505
|
+
year: 'numeric',
|
|
506
|
+
month: 'short',
|
|
507
|
+
day: 'numeric',
|
|
508
|
+
hour: '2-digit',
|
|
509
|
+
minute: '2-digit',
|
|
510
|
+
});
|
|
511
|
+
} catch {
|
|
512
|
+
return dateStr;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<div style={styles.container}>
|
|
518
|
+
<header style={styles.header}>
|
|
519
|
+
<h1 style={styles.title}>Path Permissions</h1>
|
|
520
|
+
<p style={styles.subtitle}>
|
|
521
|
+
EKKA requires explicit permission to access any path outside your home directory.
|
|
522
|
+
When you approve, EKKA requests an engine-signed grant that unlocks access.
|
|
523
|
+
</p>
|
|
524
|
+
</header>
|
|
525
|
+
|
|
526
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
527
|
+
|
|
528
|
+
{/* Section A: Path Selector */}
|
|
529
|
+
<div style={styles.section}>
|
|
530
|
+
<div style={styles.sectionHeader}>
|
|
531
|
+
<span style={styles.sectionTitle}>Select Path</span>
|
|
532
|
+
<div style={styles.sectionLine} />
|
|
533
|
+
</div>
|
|
534
|
+
<div style={styles.card}>
|
|
535
|
+
<div style={styles.pathSelector}>
|
|
536
|
+
<input
|
|
537
|
+
type="text"
|
|
538
|
+
value={pathInput}
|
|
539
|
+
onChange={(e) => setPathInput(e.target.value)}
|
|
540
|
+
onKeyDown={(e) => {
|
|
541
|
+
if (e.key === 'Enter') void handlePathSubmit();
|
|
542
|
+
}}
|
|
543
|
+
placeholder="/path/to/directory"
|
|
544
|
+
style={styles.input}
|
|
545
|
+
/>
|
|
546
|
+
<button
|
|
547
|
+
onClick={() => void handleBrowseFolder()}
|
|
548
|
+
style={styles.buttonSecondary}
|
|
549
|
+
disabled={loading}
|
|
550
|
+
title="Select folder"
|
|
551
|
+
>
|
|
552
|
+
📁
|
|
553
|
+
</button>
|
|
554
|
+
<button
|
|
555
|
+
onClick={() => void handleBrowseFile()}
|
|
556
|
+
style={styles.buttonSecondary}
|
|
557
|
+
disabled={loading}
|
|
558
|
+
title="Select file"
|
|
559
|
+
>
|
|
560
|
+
📄
|
|
561
|
+
</button>
|
|
562
|
+
<button
|
|
563
|
+
onClick={() => void handlePathSubmit()}
|
|
564
|
+
style={{
|
|
565
|
+
...styles.button,
|
|
566
|
+
...(loading || !pathInput.trim() ? styles.buttonDisabled : {}),
|
|
567
|
+
}}
|
|
568
|
+
disabled={loading || !pathInput.trim()}
|
|
569
|
+
>
|
|
570
|
+
Check Path
|
|
571
|
+
</button>
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
{/* Section B: Permission Status */}
|
|
578
|
+
{selectedPath && permissionStatus && (
|
|
579
|
+
<div style={styles.section}>
|
|
580
|
+
<div style={styles.sectionHeader}>
|
|
581
|
+
<span style={styles.sectionTitle}>Permission Status</span>
|
|
582
|
+
<div style={styles.sectionLine} />
|
|
583
|
+
</div>
|
|
584
|
+
<div
|
|
585
|
+
style={{
|
|
586
|
+
...styles.statusCard,
|
|
587
|
+
background: permissionStatus.allowed
|
|
588
|
+
? darkMode
|
|
589
|
+
? 'rgba(48, 209, 88, 0.08)'
|
|
590
|
+
: 'rgba(52, 199, 89, 0.06)'
|
|
591
|
+
: darkMode
|
|
592
|
+
? 'rgba(255, 69, 58, 0.08)'
|
|
593
|
+
: 'rgba(255, 59, 48, 0.06)',
|
|
594
|
+
borderColor: permissionStatus.allowed
|
|
595
|
+
? darkMode
|
|
596
|
+
? 'rgba(48, 209, 88, 0.2)'
|
|
597
|
+
: 'rgba(52, 199, 89, 0.15)'
|
|
598
|
+
: darkMode
|
|
599
|
+
? 'rgba(255, 69, 58, 0.2)'
|
|
600
|
+
: 'rgba(255, 59, 48, 0.15)',
|
|
601
|
+
}}
|
|
602
|
+
>
|
|
603
|
+
<div
|
|
604
|
+
style={{
|
|
605
|
+
...styles.statusIcon,
|
|
606
|
+
background: permissionStatus.allowed
|
|
607
|
+
? darkMode
|
|
608
|
+
? 'rgba(48, 209, 88, 0.15)'
|
|
609
|
+
: 'rgba(52, 199, 89, 0.12)'
|
|
610
|
+
: darkMode
|
|
611
|
+
? 'rgba(255, 69, 58, 0.15)'
|
|
612
|
+
: 'rgba(255, 59, 48, 0.12)',
|
|
613
|
+
color: permissionStatus.allowed ? colors.green : colors.red,
|
|
614
|
+
}}
|
|
615
|
+
>
|
|
616
|
+
{permissionStatus.allowed ? <CheckIcon /> : <DeniedIcon />}
|
|
617
|
+
</div>
|
|
618
|
+
<div style={styles.statusContent}>
|
|
619
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
620
|
+
<div style={styles.statusTitle}>
|
|
621
|
+
{permissionStatus.allowed ? 'Access Allowed' : 'Access Denied'}
|
|
622
|
+
</div>
|
|
623
|
+
{permissionStatus.allowed && (
|
|
624
|
+
<InfoPopover
|
|
625
|
+
darkMode={darkMode}
|
|
626
|
+
items={[
|
|
627
|
+
{ label: 'Grant Path', value: permissionStatus.grantedBy || selectedPath || '-', mono: true },
|
|
628
|
+
{ label: 'Selected Path', value: selectedPath || '-', mono: true },
|
|
629
|
+
{ label: 'Type', value: permissionStatus.pathType ? formatPathType(permissionStatus.pathType) : '-' },
|
|
630
|
+
{ label: 'Access', value: permissionStatus.access ? formatAccess(permissionStatus.access) : '-' },
|
|
631
|
+
]}
|
|
632
|
+
/>
|
|
633
|
+
)}
|
|
634
|
+
</div>
|
|
635
|
+
{permissionStatus.allowed && permissionStatus.pathType && (
|
|
636
|
+
<div style={{ marginTop: '8px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
637
|
+
<span style={{ ...styles.badge, ...styles.badgeBlue }}>
|
|
638
|
+
{formatPathType(permissionStatus.pathType)}
|
|
639
|
+
</span>
|
|
640
|
+
{permissionStatus.access && (
|
|
641
|
+
<span style={{ ...styles.badge, ...styles.badgeGreen }}>
|
|
642
|
+
{formatAccess(permissionStatus.access)}
|
|
643
|
+
</span>
|
|
644
|
+
)}
|
|
645
|
+
</div>
|
|
646
|
+
)}
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
|
|
650
|
+
{/* Section C: Access Selection & Grant */}
|
|
651
|
+
{!permissionStatus.allowed && (
|
|
652
|
+
<div style={styles.accessSelector}>
|
|
653
|
+
<span style={{ fontSize: '13px', color: colors.textMuted, marginRight: '8px' }}>
|
|
654
|
+
Access level:
|
|
655
|
+
</span>
|
|
656
|
+
<button
|
|
657
|
+
onClick={() => setRequestedAccess('READ_ONLY')}
|
|
658
|
+
style={{
|
|
659
|
+
...styles.accessOption,
|
|
660
|
+
...(requestedAccess === 'READ_ONLY' ? styles.accessOptionSelected : {}),
|
|
661
|
+
}}
|
|
662
|
+
>
|
|
663
|
+
<ReadIcon />
|
|
664
|
+
Read Only
|
|
665
|
+
</button>
|
|
666
|
+
<button
|
|
667
|
+
onClick={() => setRequestedAccess('READ_WRITE')}
|
|
668
|
+
style={{
|
|
669
|
+
...styles.accessOption,
|
|
670
|
+
...(requestedAccess === 'READ_WRITE' ? styles.accessOptionSelected : {}),
|
|
671
|
+
}}
|
|
672
|
+
>
|
|
673
|
+
<WriteIcon />
|
|
674
|
+
Read/Write
|
|
675
|
+
</button>
|
|
676
|
+
</div>
|
|
677
|
+
)}
|
|
678
|
+
|
|
679
|
+
{/* Section D: Grant/Revoke Actions */}
|
|
680
|
+
<div style={styles.actions}>
|
|
681
|
+
{!permissionStatus.allowed && (
|
|
682
|
+
<button
|
|
683
|
+
onClick={() => void handleRequestPermission()}
|
|
684
|
+
style={{
|
|
685
|
+
...styles.button,
|
|
686
|
+
...(loading ? styles.buttonDisabled : {}),
|
|
687
|
+
}}
|
|
688
|
+
disabled={loading}
|
|
689
|
+
>
|
|
690
|
+
{loading ? 'Requesting...' : `Grant ${requestedAccess === 'READ_WRITE' ? 'Read/Write' : 'Read Only'} Access`}
|
|
691
|
+
</button>
|
|
692
|
+
)}
|
|
693
|
+
{permissionStatus.allowed && (
|
|
694
|
+
<button
|
|
695
|
+
onClick={() => void handleRevokePermission()}
|
|
696
|
+
style={{
|
|
697
|
+
...styles.buttonDanger,
|
|
698
|
+
...(loading ? styles.buttonDisabled : {}),
|
|
699
|
+
}}
|
|
700
|
+
disabled={loading}
|
|
701
|
+
>
|
|
702
|
+
{loading ? 'Revoking...' : 'Revoke Permission'}
|
|
703
|
+
</button>
|
|
704
|
+
)}
|
|
705
|
+
</div>
|
|
706
|
+
|
|
707
|
+
{/* Grant Success Panel */}
|
|
708
|
+
{grantResult?.success && (
|
|
709
|
+
<div style={styles.grantPanel}>
|
|
710
|
+
<div style={styles.grantTitle}>
|
|
711
|
+
<CheckIcon />
|
|
712
|
+
Permission Granted
|
|
713
|
+
</div>
|
|
714
|
+
<div style={styles.grantDetails}>
|
|
715
|
+
<div style={styles.grantRow}>
|
|
716
|
+
<span style={styles.grantLabel}>Path</span>
|
|
717
|
+
<span style={styles.grantValue}>{selectedPath}</span>
|
|
718
|
+
</div>
|
|
719
|
+
<div style={styles.grantRow}>
|
|
720
|
+
<span style={styles.grantLabel}>Type</span>
|
|
721
|
+
<span style={styles.grantValue}>WORKSPACE</span>
|
|
722
|
+
</div>
|
|
723
|
+
<div style={styles.grantRow}>
|
|
724
|
+
<span style={styles.grantLabel}>Access</span>
|
|
725
|
+
<span style={styles.grantValue}>{requestedAccess}</span>
|
|
726
|
+
</div>
|
|
727
|
+
{grantResult.grantId && (
|
|
728
|
+
<div style={styles.grantRow}>
|
|
729
|
+
<span style={styles.grantLabel}>Grant ID</span>
|
|
730
|
+
<span style={styles.grantValue}>{grantResult.grantId}</span>
|
|
731
|
+
</div>
|
|
732
|
+
)}
|
|
733
|
+
<div style={styles.grantRow}>
|
|
734
|
+
<span style={styles.grantLabel}>Signed by</span>
|
|
735
|
+
<span style={styles.grantValue}>EKKA Engine</span>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
)}
|
|
740
|
+
</div>
|
|
741
|
+
)}
|
|
742
|
+
|
|
743
|
+
{/* Section E: Current Permissions Table */}
|
|
744
|
+
<div style={styles.section}>
|
|
745
|
+
<div style={styles.sectionHeader}>
|
|
746
|
+
<span style={styles.sectionTitle}>Current Permissions</span>
|
|
747
|
+
<div style={styles.sectionLine} />
|
|
748
|
+
</div>
|
|
749
|
+
<div style={{ ...styles.card, padding: 0, overflowX: 'auto' }}>
|
|
750
|
+
{allPaths.length === 0 ? (
|
|
751
|
+
<div style={styles.emptyState}>
|
|
752
|
+
No path permissions granted yet.
|
|
753
|
+
<br />
|
|
754
|
+
Select a path above to request access.
|
|
755
|
+
</div>
|
|
756
|
+
) : (
|
|
757
|
+
<table style={styles.table}>
|
|
758
|
+
<thead>
|
|
759
|
+
<tr>
|
|
760
|
+
<th style={styles.tableHeader}>Path Prefix</th>
|
|
761
|
+
<th style={styles.tableHeader}>Type</th>
|
|
762
|
+
<th style={styles.tableHeader}>Access</th>
|
|
763
|
+
<th style={styles.tableHeader}>Status</th>
|
|
764
|
+
<th style={{ ...styles.tableHeader, width: '80px', textAlign: 'right' }}>Actions</th>
|
|
765
|
+
</tr>
|
|
766
|
+
</thead>
|
|
767
|
+
<tbody>
|
|
768
|
+
{allPaths.map((path) => (
|
|
769
|
+
<tr key={path.grantId}>
|
|
770
|
+
<td style={styles.tableCellMono} title={path.path}>
|
|
771
|
+
{path.path}
|
|
772
|
+
</td>
|
|
773
|
+
<td style={styles.tableCell}>
|
|
774
|
+
<span style={{ ...styles.badge, ...styles.badgeBlue }}>
|
|
775
|
+
{formatPathType(path.pathType)}
|
|
776
|
+
</span>
|
|
777
|
+
</td>
|
|
778
|
+
<td style={styles.tableCell}>
|
|
779
|
+
<span style={{ ...styles.badge, ...styles.badgeGreen }}>
|
|
780
|
+
{formatAccess(path.access)}
|
|
781
|
+
</span>
|
|
782
|
+
</td>
|
|
783
|
+
<td style={styles.tableCell}>
|
|
784
|
+
<span
|
|
785
|
+
style={{
|
|
786
|
+
...styles.badge,
|
|
787
|
+
...(path.isValid ? styles.badgeGreen : styles.badgeOrange),
|
|
788
|
+
}}
|
|
789
|
+
>
|
|
790
|
+
{path.isValid ? 'Valid' : 'Expired'}
|
|
791
|
+
</span>
|
|
792
|
+
</td>
|
|
793
|
+
<td style={styles.tableCell}>
|
|
794
|
+
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
|
795
|
+
<InfoPopover
|
|
796
|
+
darkMode={darkMode}
|
|
797
|
+
items={[
|
|
798
|
+
{ label: 'Grant ID', value: path.grantId, mono: true },
|
|
799
|
+
{ label: 'Path', value: path.path, mono: true },
|
|
800
|
+
{ label: 'Type', value: formatPathType(path.pathType) },
|
|
801
|
+
{ label: 'Access', value: formatAccess(path.access) },
|
|
802
|
+
{ label: 'Issuer', value: path.issuer },
|
|
803
|
+
{ label: 'Issued At', value: formatDate(path.issuedAt) },
|
|
804
|
+
{ label: 'Expires At', value: path.expiresAt ? formatDate(path.expiresAt) : 'Never' },
|
|
805
|
+
{ label: 'User', value: path.subject },
|
|
806
|
+
{ label: 'Tenant', value: path.tenantId },
|
|
807
|
+
{ label: 'Purpose', value: path.purpose },
|
|
808
|
+
]}
|
|
809
|
+
/>
|
|
810
|
+
<button
|
|
811
|
+
onClick={() => void handleRevokeByPath(path.path)}
|
|
812
|
+
style={styles.tableDangerButton}
|
|
813
|
+
disabled={loading}
|
|
814
|
+
title="Revoke this permission"
|
|
815
|
+
>
|
|
816
|
+
<TrashIcon />
|
|
817
|
+
</button>
|
|
818
|
+
</div>
|
|
819
|
+
</td>
|
|
820
|
+
</tr>
|
|
821
|
+
))}
|
|
822
|
+
</tbody>
|
|
823
|
+
</table>
|
|
824
|
+
)}
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
827
|
+
</div>
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function CheckIcon(): ReactElement {
|
|
832
|
+
return (
|
|
833
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
834
|
+
<path d="M20 6L9 17l-5-5" />
|
|
835
|
+
</svg>
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function DeniedIcon(): ReactElement {
|
|
840
|
+
return (
|
|
841
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
842
|
+
<circle cx="12" cy="12" r="10" />
|
|
843
|
+
<path d="M15 9l-6 6M9 9l6 6" />
|
|
844
|
+
</svg>
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function ReadIcon(): ReactElement {
|
|
849
|
+
return (
|
|
850
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
851
|
+
<path d="M8 3.5a4.5 4.5 0 0 0-4.041 2.5h-.709a5.5 5.5 0 0 1 9.5 0h-.709A4.5 4.5 0 0 0 8 3.5z" />
|
|
852
|
+
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0z" />
|
|
853
|
+
<path d="M1.323 8.5l-.5-.866C1.89 6.482 4.2 4.5 8 4.5c3.8 0 6.11 1.982 7.177 3.134l-.5.866C13.69 7.475 11.541 5.5 8 5.5 4.459 5.5 2.31 7.475 1.323 8.5z" />
|
|
854
|
+
</svg>
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function WriteIcon(): ReactElement {
|
|
859
|
+
return (
|
|
860
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
861
|
+
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
|
|
862
|
+
</svg>
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function TrashIcon(): ReactElement {
|
|
867
|
+
return (
|
|
868
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
869
|
+
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
|
|
870
|
+
<path fillRule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" />
|
|
871
|
+
</svg>
|
|
872
|
+
);
|
|
873
|
+
}
|