@nextclaw/ui 0.15.29 → 0.17.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/CHANGELOG.md +25 -0
- package/dist/assets/api-CVjdSvJk.js +13 -0
- package/dist/assets/{appearance-settings-page-Bpd2RhK5.js → appearance-settings-page-BOorfaUx.js} +1 -1
- package/dist/assets/{channels-list-page-CTblam4c.js → channels-list-page-DuZJvYB6.js} +1 -1
- package/dist/assets/{chat-page-Bg3_X8G9.js → chat-page-8PTMgI0t.js} +1 -1
- package/dist/assets/{config-split-page-ps1Aod3g.js → config-split-page-UuhsVcQD.js} +1 -1
- package/dist/assets/{confirm-dialog-DJSUNG33.js → confirm-dialog-Bnosa8UF.js} +1 -1
- package/dist/assets/{desktop-update-config-D9AyIhJZ.js → desktop-update-config-CM4lsX4R.js} +1 -1
- package/dist/assets/{dist-DkR7_Dpl.js → dist-D17RZAsz.js} +1 -1
- package/dist/assets/{dist-BkTYNzm_.js → dist-_zZ8P38T.js} +1 -1
- package/dist/assets/{doc-browser-Q67TOGVS.js → doc-browser-CiLvybSC.js} +1 -1
- package/dist/assets/{doc-browser-context-BQim8SqD.js → doc-browser-context-CerkiK_N.js} +1 -1
- package/dist/assets/doc-browser-z7vdH3Ez.js +1 -0
- package/dist/assets/{form-actions-B2-bpFie.js → form-actions-Cmv23dN4.js} +1 -1
- package/dist/assets/index-CWdgBpSc.css +1 -0
- package/dist/assets/index-DlL8uKXu.js +109 -0
- package/dist/assets/loader-circle-vUsFtTfb.js +1 -0
- package/dist/assets/mcp-marketplace-page-BV8WB3_L.js +1 -0
- package/dist/assets/{mcp-marketplace-page-w8panjZG.js → mcp-marketplace-page-DN_fqcLd.js} +1 -1
- package/dist/assets/{model-config-page-B8Aubkwx.js → model-config-page-BxaeCMbJ.js} +1 -1
- package/dist/assets/{navigation-link-mpHFXwRr.js → navigation-link-k9rsmtC4.js} +1 -1
- package/dist/assets/plus-Dc760kHj.js +1 -0
- package/dist/assets/{provider-scoped-model-input-BRnIH-5O.js → provider-scoped-model-input-DLSAB6a5.js} +1 -1
- package/dist/assets/{providers-config-page-0r9AZNNZ.js → providers-config-page-DM5N0aeS.js} +1 -1
- package/dist/assets/{react-BlXwvOLo.js → react-BgRLh3ee.js} +1 -1
- package/dist/assets/{refresh-cw-JFuyDj_P.js → refresh-cw-axvJLmRG.js} +1 -1
- package/dist/assets/remote-Cj1GBMaN.js +1 -0
- package/dist/assets/rotate-cw-CFByP2O8.js +1 -0
- package/dist/assets/{runtime-config-page-BgeYpvMK.js → runtime-config-page-WNzZyjMW.js} +1 -1
- package/dist/assets/{save-BL8P8CLX.js → save-DbR4SGk8.js} +1 -1
- package/dist/assets/{search-bZL75Pgc.js → search-M9lrb8id.js} +1 -1
- package/dist/assets/{search-config-page-CzNzLyd0.js → search-config-page-DmsID8FQ.js} +1 -1
- package/dist/assets/{secrets-config-page-C4k3ik0v.js → secrets-config-page-C10OwI0b.js} +1 -1
- package/dist/assets/security-config-CvU5JxDT.js +1 -0
- package/dist/assets/{select-rUZ8nIZu.js → select-D_YBzMdv.js} +1 -1
- package/dist/assets/{settings-2-BI4SCkxQ.js → settings-2-zy40UwTo.js} +1 -1
- package/dist/assets/skeleton-BmAtzjDM.js +1 -0
- package/dist/assets/{tag-chip-BdI_yIOA.js → tag-chip-D7QRdcph.js} +1 -1
- package/dist/assets/x-D9Y9hf_r.js +1 -0
- package/dist/index.html +19 -19
- package/package.json +5 -5
- package/src/features/app-data/components/app-data-removal-choice.tsx +43 -0
- package/src/features/app-data/components/app-retained-data-section.tsx +179 -0
- package/src/features/app-data/components/app-storage-usage.tsx +59 -0
- package/src/features/app-data/hooks/use-app-data.ts +26 -0
- package/src/features/app-data/index.ts +8 -0
- package/src/features/apps/components/__tests__/app-packages-panel.test.tsx +108 -1
- package/src/features/apps/components/app-marketplace-detail.tsx +19 -2
- package/src/features/apps/components/app-package-card.tsx +70 -44
- package/src/features/apps/components/app-packages-panel.tsx +19 -2
- package/src/features/apps/hooks/use-app-packages.ts +2 -0
- package/src/features/apps/types/app-marketplace.types.ts +3 -2
- package/src/features/service-apps/components/__tests__/service-apps-panel.test.tsx +59 -2
- package/src/features/service-apps/components/service-app-delete-dialog.tsx +122 -0
- package/src/features/service-apps/components/service-app-list-item.tsx +24 -9
- package/src/features/service-apps/components/service-apps-panel.tsx +15 -1
- package/src/features/service-apps/hooks/use-service-apps.ts +4 -1
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +29 -1
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +29 -1
- package/dist/assets/api-a_GaOFc7.js +0 -13
- package/dist/assets/doc-browser-DahzJEWj.js +0 -1
- package/dist/assets/index-DSLK7zgR.css +0 -1
- package/dist/assets/index-VQAVqwG8.js +0 -109
- package/dist/assets/loader-circle-DyXZU92l.js +0 -1
- package/dist/assets/mcp-marketplace-page-B0dBE5CV.js +0 -1
- package/dist/assets/plus-Dhb4KuMH.js +0 -1
- package/dist/assets/remote-DQzE55WW.js +0 -1
- package/dist/assets/rotate-cw-DjbbiIW_.js +0 -1
- package/dist/assets/security-config-DmJw9QZA.js +0 -1
- package/dist/assets/skeleton-CVmEPG43.js +0 -1
- package/dist/assets/x-BcEayhV6.js +0 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AppDataEntry } from '@nextclaw/client-sdk';
|
|
2
|
+
import { HardDrive } from 'lucide-react';
|
|
3
|
+
import { t } from '@/shared/lib/i18n';
|
|
4
|
+
|
|
5
|
+
type Storage = AppDataEntry['storage'];
|
|
6
|
+
type Usage = AppDataEntry['usage'];
|
|
7
|
+
|
|
8
|
+
export function AppStorageUsageDetails({
|
|
9
|
+
storage,
|
|
10
|
+
usage,
|
|
11
|
+
}: {
|
|
12
|
+
storage: Storage;
|
|
13
|
+
usage: Usage;
|
|
14
|
+
}) {
|
|
15
|
+
const rows = [
|
|
16
|
+
[t('appDataClassData'), usage.dataBytes],
|
|
17
|
+
[t('appDataClassConfig'), usage.configBytes],
|
|
18
|
+
[t('appDataClassState'), usage.stateBytes],
|
|
19
|
+
[t('appDataClassCache'), usage.cacheBytes],
|
|
20
|
+
[t('appDataClassTemporary'), usage.temporaryBytes],
|
|
21
|
+
[t('appDataClassLogs'), usage.logsBytes],
|
|
22
|
+
] as const;
|
|
23
|
+
return (
|
|
24
|
+
<div className="rounded-xl bg-muted/45 px-3 py-2.5">
|
|
25
|
+
<div className="flex items-center justify-between gap-3 text-xs">
|
|
26
|
+
<span className="inline-flex items-center gap-1.5 font-medium text-foreground">
|
|
27
|
+
<HardDrive className="h-3.5 w-3.5" />
|
|
28
|
+
{t('appDataStorageTotal')}
|
|
29
|
+
</span>
|
|
30
|
+
<span className="tabular-nums text-foreground">{formatBytes(usage.totalBytes)}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<dl className="mt-2 grid grid-cols-2 gap-x-4 gap-y-1 text-[11px] sm:grid-cols-3">
|
|
33
|
+
{rows.map(([label, bytes]) => (
|
|
34
|
+
<div key={label} className="flex min-w-0 items-center justify-between gap-2">
|
|
35
|
+
<dt className="truncate text-muted-foreground">{label}</dt>
|
|
36
|
+
<dd className="shrink-0 tabular-nums text-foreground/80">{formatBytes(bytes)}</dd>
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</dl>
|
|
40
|
+
<div className="mt-2 border-t border-border/50 pt-2">
|
|
41
|
+
<div className="text-[10px] text-muted-foreground">{t('appDataStoragePath')}</div>
|
|
42
|
+
<code
|
|
43
|
+
aria-label={t('appDataStoragePath')}
|
|
44
|
+
className="mt-0.5 block select-all break-all font-mono text-[10px] leading-4 text-foreground/75"
|
|
45
|
+
tabIndex={0}
|
|
46
|
+
>
|
|
47
|
+
{storage.instanceDirectory}
|
|
48
|
+
</code>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatBytes(value: number): string {
|
|
55
|
+
if (value < 1024) return `${value} B`;
|
|
56
|
+
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
|
|
57
|
+
if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
|
58
|
+
return `${(value / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { toast } from 'sonner';
|
|
3
|
+
import { nextclawClient } from '@/shared/lib/api';
|
|
4
|
+
import { t } from '@/shared/lib/i18n';
|
|
5
|
+
|
|
6
|
+
export const APP_DATA_QUERY_KEY = ['app-data'] as const;
|
|
7
|
+
|
|
8
|
+
export function useAppData() {
|
|
9
|
+
return useQuery({
|
|
10
|
+
queryKey: APP_DATA_QUERY_KEY,
|
|
11
|
+
queryFn: () => nextclawClient.appData.list(),
|
|
12
|
+
staleTime: 0,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useDeleteRetainedAppData() {
|
|
17
|
+
const queryClient = useQueryClient();
|
|
18
|
+
return useMutation({
|
|
19
|
+
mutationFn: ({ dataId, confirmAppId }: { dataId: string; confirmAppId: string }) =>
|
|
20
|
+
nextclawClient.appData.deleteRetained(dataId, confirmAppId),
|
|
21
|
+
onSuccess: async () => {
|
|
22
|
+
await queryClient.invalidateQueries({ queryKey: APP_DATA_QUERY_KEY });
|
|
23
|
+
toast.success(t('appDataDeleteCompleted'));
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { AppDataRemovalChoice } from './components/app-data-removal-choice';
|
|
2
|
+
export { AppRetainedDataSection } from './components/app-retained-data-section';
|
|
3
|
+
export { AppStorageUsageDetails, formatBytes } from './components/app-storage-usage';
|
|
4
|
+
export {
|
|
5
|
+
APP_DATA_QUERY_KEY,
|
|
6
|
+
useAppData,
|
|
7
|
+
useDeleteRetainedAppData,
|
|
8
|
+
} from './hooks/use-app-data';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
1
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import type { AppPackageOperationView } from '@nextclaw/client-sdk';
|
|
4
4
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
@@ -15,9 +15,29 @@ const mocks = vi.hoisted(() => ({
|
|
|
15
15
|
refetchOperations: vi.fn(),
|
|
16
16
|
refetchPackages: vi.fn(),
|
|
17
17
|
refetchPanels: vi.fn(),
|
|
18
|
+
refetchAppData: vi.fn(),
|
|
19
|
+
deleteRetained: vi.fn(),
|
|
20
|
+
deleteRetainedReset: vi.fn(),
|
|
21
|
+
retainedEntries: [] as Array<Record<string, unknown>>,
|
|
18
22
|
requestAuthorization: vi.fn(async () => true),
|
|
19
23
|
}));
|
|
20
24
|
|
|
25
|
+
vi.mock('@/features/app-data/hooks/use-app-data', () => ({
|
|
26
|
+
useAppData: () => ({
|
|
27
|
+
data: { entries: mocks.retainedEntries, diagnostics: [] },
|
|
28
|
+
error: null,
|
|
29
|
+
isLoading: false,
|
|
30
|
+
refetch: mocks.refetchAppData,
|
|
31
|
+
}),
|
|
32
|
+
useDeleteRetainedAppData: () => ({
|
|
33
|
+
error: null,
|
|
34
|
+
isError: false,
|
|
35
|
+
isPending: false,
|
|
36
|
+
mutate: mocks.deleteRetained,
|
|
37
|
+
reset: mocks.deleteRetainedReset,
|
|
38
|
+
}),
|
|
39
|
+
}));
|
|
40
|
+
|
|
21
41
|
vi.mock('@/app/components/app-presenter-provider', () => ({
|
|
22
42
|
useAppPresenter: () => ({
|
|
23
43
|
appPackageOperationSettlementManager: { settle: vi.fn() },
|
|
@@ -65,6 +85,10 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
|
|
|
65
85
|
packageId: 'nextclaw.personal-organizer',
|
|
66
86
|
packageVersion: '0.1.0',
|
|
67
87
|
sourcePath: '/tmp/app/todos.panel',
|
|
88
|
+
instanceId: 'default',
|
|
89
|
+
isolation: 'full-user',
|
|
90
|
+
runtimeProfile: 'native-process',
|
|
91
|
+
storage: createStorageFixture(),
|
|
68
92
|
title: 'Todos',
|
|
69
93
|
},
|
|
70
94
|
{
|
|
@@ -75,6 +99,10 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
|
|
|
75
99
|
packageId: 'nextclaw.personal-organizer',
|
|
76
100
|
packageVersion: '0.1.0',
|
|
77
101
|
sourcePath: '/tmp/app/data',
|
|
102
|
+
instanceId: 'default',
|
|
103
|
+
isolation: 'full-user',
|
|
104
|
+
runtimeProfile: 'native-process',
|
|
105
|
+
storage: createStorageFixture(),
|
|
78
106
|
title: 'Personal data',
|
|
79
107
|
},
|
|
80
108
|
],
|
|
@@ -83,8 +111,21 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
|
|
|
83
111
|
enabled: mocks.enabled,
|
|
84
112
|
id: 'nextclaw.personal-organizer',
|
|
85
113
|
installedVersions: ['0.1.0'],
|
|
114
|
+
instanceId: 'default',
|
|
115
|
+
isolation: 'full-user',
|
|
86
116
|
name: 'Personal Space',
|
|
87
117
|
primaryPanelId: 'nextclaw-personal-organizer-todos',
|
|
118
|
+
runtimeProfile: 'native-process',
|
|
119
|
+
storage: createStorageFixture(),
|
|
120
|
+
storageUsage: {
|
|
121
|
+
dataBytes: 128,
|
|
122
|
+
configBytes: 0,
|
|
123
|
+
stateBytes: 0,
|
|
124
|
+
cacheBytes: 0,
|
|
125
|
+
temporaryBytes: 0,
|
|
126
|
+
logsBytes: 0,
|
|
127
|
+
totalBytes: 128,
|
|
128
|
+
},
|
|
88
129
|
}],
|
|
89
130
|
},
|
|
90
131
|
error: null,
|
|
@@ -94,6 +135,21 @@ vi.mock('@/features/apps/hooks/use-app-packages', () => ({
|
|
|
94
135
|
}),
|
|
95
136
|
}));
|
|
96
137
|
|
|
138
|
+
function createStorageFixture() {
|
|
139
|
+
return {
|
|
140
|
+
layout: 'instance-v1' as const,
|
|
141
|
+
layoutVersion: 1 as const,
|
|
142
|
+
instanceId: 'default',
|
|
143
|
+
instanceDirectory: '/tmp/instance',
|
|
144
|
+
dataDirectory: '/tmp/data',
|
|
145
|
+
configDirectory: '/tmp/config',
|
|
146
|
+
stateDirectory: '/tmp/state',
|
|
147
|
+
cacheDirectory: '/tmp/cache',
|
|
148
|
+
temporaryDirectory: '/tmp/tmp',
|
|
149
|
+
logsDirectory: '/tmp/logs',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
97
153
|
vi.mock('@/features/apps/hooks/use-app-marketplace', () => ({
|
|
98
154
|
useAppMarketplace: () => ({
|
|
99
155
|
data: {
|
|
@@ -191,6 +247,10 @@ describe('AppPackagesPanel', () => {
|
|
|
191
247
|
mocks.refetchPackages.mockReset();
|
|
192
248
|
mocks.refetchOperations.mockReset();
|
|
193
249
|
mocks.refetchPanels.mockReset();
|
|
250
|
+
mocks.refetchAppData.mockReset();
|
|
251
|
+
mocks.deleteRetained.mockReset();
|
|
252
|
+
mocks.deleteRetainedReset.mockReset();
|
|
253
|
+
mocks.retainedEntries = [];
|
|
194
254
|
mocks.requestAuthorization.mockReset();
|
|
195
255
|
mocks.requestAuthorization.mockResolvedValue(true);
|
|
196
256
|
});
|
|
@@ -264,8 +324,13 @@ describe('AppPackagesPanel', () => {
|
|
|
264
324
|
await user.click(screen.getByRole('button', { name: 'More app actions' }));
|
|
265
325
|
await user.click(screen.getByRole('button', { name: 'Uninstall' }));
|
|
266
326
|
expect(screen.getByRole('heading', { name: 'Uninstall this app?' })).toBeTruthy();
|
|
327
|
+
await waitFor(() => {
|
|
328
|
+
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Cancel' }));
|
|
329
|
+
});
|
|
267
330
|
expect(screen.getByRole('button', { name: /Keep personal data/ }).getAttribute('aria-pressed'))
|
|
268
331
|
.toBe('true');
|
|
332
|
+
expect(screen.getByText('Total app data')).toBeTruthy();
|
|
333
|
+
expect(screen.getByText('Managed instance path')).toBeTruthy();
|
|
269
334
|
|
|
270
335
|
await user.click(screen.getByRole('button', { name: 'Uninstall' }));
|
|
271
336
|
expect(mocks.lifecycleMutate).toHaveBeenCalledWith(
|
|
@@ -274,6 +339,48 @@ describe('AppPackagesPanel', () => {
|
|
|
274
339
|
);
|
|
275
340
|
});
|
|
276
341
|
|
|
342
|
+
it('shows retained data and requires a dedicated permanent-delete confirmation', async () => {
|
|
343
|
+
const user = userEvent.setup();
|
|
344
|
+
mocks.retainedEntries = [{
|
|
345
|
+
id: 'ad1.encoded',
|
|
346
|
+
appId: 'example.notes',
|
|
347
|
+
instanceId: 'default',
|
|
348
|
+
displayName: 'Notes',
|
|
349
|
+
source: 'package',
|
|
350
|
+
lifecycle: 'retained',
|
|
351
|
+
storage: createStorageFixture(),
|
|
352
|
+
usage: {
|
|
353
|
+
dataBytes: 128,
|
|
354
|
+
configBytes: 3,
|
|
355
|
+
stateBytes: 2,
|
|
356
|
+
cacheBytes: 1,
|
|
357
|
+
temporaryBytes: 0,
|
|
358
|
+
logsBytes: 4,
|
|
359
|
+
totalBytes: 138,
|
|
360
|
+
},
|
|
361
|
+
createdAt: '2026-08-14T00:00:00.000Z',
|
|
362
|
+
actions: { deleteRetainedData: true },
|
|
363
|
+
}];
|
|
364
|
+
const view = render(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
365
|
+
|
|
366
|
+
expect(screen.getByRole('heading', { name: 'Saved app data' })).toBeTruthy();
|
|
367
|
+
await user.click(screen.getByRole('button', { name: 'Delete data' }));
|
|
368
|
+
expect(screen.getByRole('heading', { name: 'Permanently delete this app data?' })).toBeTruthy();
|
|
369
|
+
|
|
370
|
+
await user.click(screen.getByRole('button', { name: 'Permanently delete data' }));
|
|
371
|
+
expect(mocks.deleteRetained).toHaveBeenCalledWith(
|
|
372
|
+
{ dataId: 'ad1.encoded', confirmAppId: 'example.notes' },
|
|
373
|
+
{ onSuccess: expect.any(Function) },
|
|
374
|
+
);
|
|
375
|
+
const onSuccess = mocks.deleteRetained.mock.calls[0]?.[1].onSuccess as () => void;
|
|
376
|
+
mocks.retainedEntries = [];
|
|
377
|
+
view.rerender(<AppPackagesPanel onOpenPanelApp={mocks.onOpen} />);
|
|
378
|
+
await act(async () => onSuccess());
|
|
379
|
+
await waitFor(() => {
|
|
380
|
+
expect(document.activeElement).toBe(screen.getByRole('heading', { name: 'Your apps' }));
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
277
384
|
it('keeps an accepted install visible in the library and marketplace while it runs', async () => {
|
|
278
385
|
const user = userEvent.setup();
|
|
279
386
|
mocks.operations = [{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
2
|
-
import { ExternalLink, FileText, Server, ShieldCheck, type LucideIcon } from 'lucide-react';
|
|
2
|
+
import { ExternalLink, FileText, Server, ShieldAlert, ShieldCheck, type LucideIcon } from 'lucide-react';
|
|
3
3
|
import { AppArtwork } from '@/features/apps/components/app-artwork';
|
|
4
4
|
import { AppMarketplaceCover } from '@/features/apps/components/app-marketplace-cover';
|
|
5
5
|
import {
|
|
@@ -46,6 +46,8 @@ export function MarketplaceDetail({
|
|
|
46
46
|
const components = detail.manifest.components ?? [];
|
|
47
47
|
const panelCount = components.filter((entry) => entry.kind === 'panel').length;
|
|
48
48
|
const serviceCount = components.filter((entry) => entry.kind === 'service').length;
|
|
49
|
+
const runtimeProfile = detail.manifest.runtime?.profile
|
|
50
|
+
?? (serviceCount > 0 ? 'native-process' : 'panel-only');
|
|
49
51
|
const canUpdate = Boolean(
|
|
50
52
|
installedPackage && installedPackage.activeVersion !== detail.latestVersion,
|
|
51
53
|
);
|
|
@@ -97,6 +99,15 @@ export function MarketplaceDetail({
|
|
|
97
99
|
value={detail.manifest.engines.nextclaw}
|
|
98
100
|
/>
|
|
99
101
|
) : null}
|
|
102
|
+
<DetailFact
|
|
103
|
+
icon={runtimeProfile === 'native-process' ? ShieldAlert : ShieldCheck}
|
|
104
|
+
label={t('appPackagesIsolationLabel')}
|
|
105
|
+
value={runtimeProfile === 'native-process'
|
|
106
|
+
? t('appPackagesIsolationFullUser')
|
|
107
|
+
: runtimeProfile === 'wasi'
|
|
108
|
+
? t('appPackagesIsolationMediated')
|
|
109
|
+
: t('appPackagesIsolationPanel')}
|
|
110
|
+
/>
|
|
100
111
|
</div>
|
|
101
112
|
|
|
102
113
|
<section className="mt-7 border-t border-border/60 pt-6">
|
|
@@ -154,7 +165,13 @@ function readPermissionRows(detail: AppMarketplaceDetailView): string[] {
|
|
|
154
165
|
if (detail.permissions.capabilities?.hostBridge) {
|
|
155
166
|
rows.push(t('appPackagesHostBridgeAccess'));
|
|
156
167
|
}
|
|
157
|
-
if (detail.permissions.
|
|
168
|
+
if (detail.permissions.capabilities?.nativeProcess) {
|
|
169
|
+
rows.push(t('appPackagesNativeProcessAccess'));
|
|
170
|
+
}
|
|
171
|
+
if (detail.permissions.storage === true || (
|
|
172
|
+
typeof detail.permissions.storage === 'object' &&
|
|
173
|
+
detail.permissions.storage?.namespace
|
|
174
|
+
)) {
|
|
158
175
|
rows.push(t('appPackagesLocalStorageAccess'));
|
|
159
176
|
}
|
|
160
177
|
return rows.length > 0 ? rows : [t('appPackagesNoExtraAccess')];
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
2
|
import type { AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
3
3
|
import {
|
|
4
4
|
AlertCircle,
|
|
5
5
|
AppWindow,
|
|
6
6
|
Bookmark,
|
|
7
7
|
CalendarDays,
|
|
8
|
-
Check,
|
|
9
8
|
CheckSquare2,
|
|
10
9
|
ChevronRight,
|
|
11
10
|
MoreHorizontal,
|
|
@@ -15,9 +14,17 @@ import {
|
|
|
15
14
|
Server,
|
|
16
15
|
Trash2,
|
|
17
16
|
LoaderCircle,
|
|
17
|
+
HardDrive,
|
|
18
|
+
ShieldAlert,
|
|
19
|
+
ShieldCheck,
|
|
18
20
|
type LucideIcon,
|
|
19
21
|
} from 'lucide-react';
|
|
20
22
|
import { AppArtwork } from '@/features/apps/components/app-artwork';
|
|
23
|
+
import {
|
|
24
|
+
AppDataRemovalChoice,
|
|
25
|
+
AppStorageUsageDetails,
|
|
26
|
+
formatBytes,
|
|
27
|
+
} from '@/features/app-data';
|
|
21
28
|
import { isAppPackageOperationActive } from '@/features/apps/hooks/use-app-packages';
|
|
22
29
|
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
23
30
|
import { Button } from '@/shared/components/ui/button';
|
|
@@ -60,6 +67,8 @@ export function AppPackageCard({
|
|
|
60
67
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
61
68
|
const [uninstallOpen, setUninstallOpen] = useState(false);
|
|
62
69
|
const [purgeData, setPurgeData] = useState(false);
|
|
70
|
+
const skipMenuFocusRestoreRef = useRef(false);
|
|
71
|
+
const uninstallCancelRef = useRef<HTMLButtonElement>(null);
|
|
63
72
|
const panelComponents = appPackage.components.filter((component) => component.kind === 'panel');
|
|
64
73
|
const serviceCount = appPackage.components.length - panelComponents.length;
|
|
65
74
|
const rollbackVersions = appPackage.installedVersions.filter(
|
|
@@ -103,6 +112,31 @@ export function AppPackageCard({
|
|
|
103
112
|
{t('appPackagesLocalService')}
|
|
104
113
|
</span>
|
|
105
114
|
) : null}
|
|
115
|
+
<span className={cn(
|
|
116
|
+
'inline-flex items-center gap-1',
|
|
117
|
+
appPackage.isolation === 'full-user' && 'text-amber-700 dark:text-amber-300',
|
|
118
|
+
)}>
|
|
119
|
+
{appPackage.isolation === 'full-user'
|
|
120
|
+
? <ShieldAlert className="h-3 w-3" />
|
|
121
|
+
: <ShieldCheck className="h-3 w-3" />}
|
|
122
|
+
{appPackage.isolation === 'full-user'
|
|
123
|
+
? t('appPackagesIsolationFullUser')
|
|
124
|
+
: appPackage.isolation === 'host-mediated'
|
|
125
|
+
? t('appPackagesIsolationMediated')
|
|
126
|
+
: t('appPackagesIsolationPanel')}
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
<div
|
|
130
|
+
className="mt-2 flex min-w-0 items-center gap-1.5 rounded-md bg-muted/35 px-2 py-1 text-[10px] text-muted-foreground"
|
|
131
|
+
title={appPackage.storage.dataDirectory}
|
|
132
|
+
>
|
|
133
|
+
<HardDrive className="h-3 w-3 shrink-0" />
|
|
134
|
+
<span className="shrink-0">
|
|
135
|
+
{t('appPackagesDataUsage')} {formatBytes(appPackage.storageUsage.totalBytes)}
|
|
136
|
+
</span>
|
|
137
|
+
<code className="min-w-0 truncate font-mono text-[10px]">
|
|
138
|
+
{appPackage.storage.dataDirectory}
|
|
139
|
+
</code>
|
|
106
140
|
</div>
|
|
107
141
|
</div>
|
|
108
142
|
<div className="flex shrink-0 items-center gap-1">
|
|
@@ -132,7 +166,16 @@ export function AppPackageCard({
|
|
|
132
166
|
<MoreHorizontal className="h-4 w-4" />
|
|
133
167
|
</button>
|
|
134
168
|
</PopoverTrigger>
|
|
135
|
-
<PopoverContent
|
|
169
|
+
<PopoverContent
|
|
170
|
+
align="end"
|
|
171
|
+
className="w-52 rounded-xl p-1.5"
|
|
172
|
+
onCloseAutoFocus={(event) => {
|
|
173
|
+
if (skipMenuFocusRestoreRef.current) {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
skipMenuFocusRestoreRef.current = false;
|
|
176
|
+
}
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
136
179
|
<AppPackageMenuItem
|
|
137
180
|
icon={RefreshCw}
|
|
138
181
|
label={t('appPackagesCheckUpdate')}
|
|
@@ -157,7 +200,9 @@ export function AppPackageCard({
|
|
|
157
200
|
icon={Trash2}
|
|
158
201
|
label={t('appPackagesUninstall')}
|
|
159
202
|
onClick={() => {
|
|
203
|
+
skipMenuFocusRestoreRef.current = true;
|
|
160
204
|
setMenuOpen(false);
|
|
205
|
+
setPurgeData(false);
|
|
161
206
|
setUninstallOpen(true);
|
|
162
207
|
}}
|
|
163
208
|
/>
|
|
@@ -211,19 +256,25 @@ export function AppPackageCard({
|
|
|
211
256
|
) : null}
|
|
212
257
|
|
|
213
258
|
<Dialog open={uninstallOpen} onOpenChange={setUninstallOpen}>
|
|
214
|
-
<DialogContent
|
|
259
|
+
<DialogContent
|
|
260
|
+
className="max-w-md [&>:last-child]:hidden"
|
|
261
|
+
onOpenAutoFocus={(event) => {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
uninstallCancelRef.current?.focus();
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
215
266
|
<DialogHeader>
|
|
216
267
|
<DialogTitle>{t('appPackagesUninstallTitle')}</DialogTitle>
|
|
217
268
|
<DialogDescription>{t('appPackagesUninstallDescription')}</DialogDescription>
|
|
218
269
|
</DialogHeader>
|
|
219
270
|
<div className="mt-4 grid gap-2">
|
|
220
|
-
<
|
|
271
|
+
<AppDataRemovalChoice
|
|
221
272
|
checked={!purgeData}
|
|
222
273
|
description={t('appPackagesKeepDataDescription')}
|
|
223
274
|
label={t('appPackagesKeepData')}
|
|
224
275
|
onClick={() => setPurgeData(false)}
|
|
225
276
|
/>
|
|
226
|
-
<
|
|
277
|
+
<AppDataRemovalChoice
|
|
227
278
|
checked={purgeData}
|
|
228
279
|
destructive
|
|
229
280
|
description={t('appPackagesDeleteDataDescription')}
|
|
@@ -231,8 +282,20 @@ export function AppPackageCard({
|
|
|
231
282
|
onClick={() => setPurgeData(true)}
|
|
232
283
|
/>
|
|
233
284
|
</div>
|
|
285
|
+
<div className="mt-3">
|
|
286
|
+
<AppStorageUsageDetails
|
|
287
|
+
storage={appPackage.storage}
|
|
288
|
+
usage={appPackage.storageUsage}
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
234
291
|
<DialogFooter className="mt-5 gap-2 sm:gap-0">
|
|
235
|
-
<Button
|
|
292
|
+
<Button
|
|
293
|
+
ref={uninstallCancelRef}
|
|
294
|
+
autoFocus
|
|
295
|
+
type="button"
|
|
296
|
+
variant="outline"
|
|
297
|
+
onClick={() => setUninstallOpen(false)}
|
|
298
|
+
>
|
|
236
299
|
{t('cancel')}
|
|
237
300
|
</Button>
|
|
238
301
|
<Button
|
|
@@ -275,43 +338,6 @@ function renderPrimaryActionLabel({
|
|
|
275
338
|
return enabled ? t('appPackagesDisable') : t('appPackagesEnable');
|
|
276
339
|
}
|
|
277
340
|
|
|
278
|
-
function DataChoice({
|
|
279
|
-
checked,
|
|
280
|
-
description,
|
|
281
|
-
destructive = false,
|
|
282
|
-
label,
|
|
283
|
-
onClick,
|
|
284
|
-
}: {
|
|
285
|
-
checked: boolean;
|
|
286
|
-
description: string;
|
|
287
|
-
destructive?: boolean;
|
|
288
|
-
label: string;
|
|
289
|
-
onClick: () => void;
|
|
290
|
-
}) {
|
|
291
|
-
return (
|
|
292
|
-
<button
|
|
293
|
-
type="button"
|
|
294
|
-
aria-pressed={checked}
|
|
295
|
-
onClick={onClick}
|
|
296
|
-
className={cn(
|
|
297
|
-
'flex items-start gap-3 rounded-xl border px-3 py-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border',
|
|
298
|
-
checked ? 'border-foreground/25 bg-muted/55' : 'border-border/60 hover:bg-muted/35',
|
|
299
|
-
)}
|
|
300
|
-
>
|
|
301
|
-
<span className={cn(
|
|
302
|
-
'mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border',
|
|
303
|
-
checked ? 'border-foreground bg-foreground text-background' : 'border-border',
|
|
304
|
-
)}>
|
|
305
|
-
{checked ? <Check className="h-2.5 w-2.5" /> : null}
|
|
306
|
-
</span>
|
|
307
|
-
<span>
|
|
308
|
-
<span className={cn('block text-sm font-medium', destructive ? 'text-destructive' : 'text-foreground')}>{label}</span>
|
|
309
|
-
<span className="mt-0.5 block text-xs leading-5 text-muted-foreground">{description}</span>
|
|
310
|
-
</span>
|
|
311
|
-
</button>
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
341
|
function AppPackageMenuItem({
|
|
316
342
|
destructive = false,
|
|
317
343
|
icon: Icon,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useState, type FormEvent } from 'react';
|
|
1
|
+
import { useMemo, useRef, useState, type FormEvent } from 'react';
|
|
2
2
|
import type { AppPackageOperationView, AppPackageView } from '@nextclaw/client-sdk';
|
|
3
3
|
import { LoaderCircle, PackagePlus, RefreshCw, ShieldCheck, Store } from 'lucide-react';
|
|
4
4
|
import { AppArtwork } from '@/features/apps/components/app-artwork';
|
|
@@ -6,6 +6,7 @@ import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
|
6
6
|
import { useAppPresenter } from '@/app/components/app-presenter-provider';
|
|
7
7
|
import { AppMarketplaceDialog } from '@/features/apps/components/app-marketplace-dialog';
|
|
8
8
|
import { AppPackageCard } from '@/features/apps/components/app-package-card';
|
|
9
|
+
import { AppRetainedDataSection, useAppData } from '@/features/app-data';
|
|
9
10
|
import {
|
|
10
11
|
useAppPackageMutation,
|
|
11
12
|
useAppPackageOperationSettlement,
|
|
@@ -38,12 +39,14 @@ export function AppPackagesPanel({
|
|
|
38
39
|
onOpenPanelApp: (entry: PanelAppEntryView) => void;
|
|
39
40
|
}) {
|
|
40
41
|
const appPackages = useAppPackages();
|
|
42
|
+
const appData = useAppData();
|
|
41
43
|
const appPackageOperations = useAppPackageOperations();
|
|
42
44
|
const panelApps = usePanelApps();
|
|
43
45
|
const lifecycle = useAppPackageMutation();
|
|
44
46
|
const recordOpened = useRecordPanelAppOpened();
|
|
45
47
|
const grantClient = useGrantPanelAppClient();
|
|
46
48
|
const presenter = useAppPresenter();
|
|
49
|
+
const libraryTitleRef = useRef<HTMLHeadingElement>(null);
|
|
47
50
|
const [installOpen, setInstallOpen] = useState(false);
|
|
48
51
|
const [installSource, setInstallSource] = useState('');
|
|
49
52
|
const [marketplaceOpen, setMarketplaceOpen] = useState(false);
|
|
@@ -101,6 +104,7 @@ export function AppPackagesPanel({
|
|
|
101
104
|
void appPackages.refetch();
|
|
102
105
|
void appPackageOperations.refetch();
|
|
103
106
|
void panelApps.refetch();
|
|
107
|
+
void appData.refetch();
|
|
104
108
|
};
|
|
105
109
|
|
|
106
110
|
return (
|
|
@@ -144,7 +148,13 @@ export function AppPackagesPanel({
|
|
|
144
148
|
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto">
|
|
145
149
|
<div className="mx-auto w-full max-w-3xl p-3 sm:p-5">
|
|
146
150
|
<div className="mb-4 px-1">
|
|
147
|
-
<h1
|
|
151
|
+
<h1
|
|
152
|
+
ref={libraryTitleRef}
|
|
153
|
+
tabIndex={-1}
|
|
154
|
+
className="text-base font-semibold tracking-tight text-foreground"
|
|
155
|
+
>
|
|
156
|
+
{t('appPackagesLibraryTitle')}
|
|
157
|
+
</h1>
|
|
148
158
|
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
149
159
|
{t('appPackagesLibraryDescription')}
|
|
150
160
|
</p>
|
|
@@ -170,6 +180,13 @@ export function AppPackagesPanel({
|
|
|
170
180
|
onMutate={runMutation}
|
|
171
181
|
onOpenPanelApp={(entry) => void openPanelApp(entry)}
|
|
172
182
|
/>
|
|
183
|
+
<AppRetainedDataSection
|
|
184
|
+
diagnostics={appData.data?.diagnostics ?? []}
|
|
185
|
+
entries={appData.data?.entries ?? []}
|
|
186
|
+
error={appData.error}
|
|
187
|
+
isLoading={appData.isLoading}
|
|
188
|
+
onDeletionFocusReturn={() => libraryTitleRef.current?.focus()}
|
|
189
|
+
/>
|
|
173
190
|
</div>
|
|
174
191
|
</div>
|
|
175
192
|
|
|
@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
|
3
3
|
import type { AppPackageOperationStatus, AppPackageOperationView } from '@nextclaw/client-sdk';
|
|
4
4
|
import { nextclawClient } from '@/shared/lib/api';
|
|
5
5
|
import type { AppPackageOperationSettlementManager } from '@/features/apps/managers/app-package-operation-settlement.manager';
|
|
6
|
+
import { APP_DATA_QUERY_KEY } from '@/features/app-data';
|
|
6
7
|
|
|
7
8
|
const APP_PACKAGES_QUERY_KEY = ['app-packages'] as const;
|
|
8
9
|
const PANEL_APPS_QUERY_KEY = ['panel-apps'] as const;
|
|
@@ -108,6 +109,7 @@ export function useAppPackageMutation() {
|
|
|
108
109
|
queryClient.invalidateQueries({ queryKey: SERVICE_APPS_QUERY_KEY }),
|
|
109
110
|
queryClient.invalidateQueries({ queryKey: SERVICE_ACTIONS_QUERY_KEY }),
|
|
110
111
|
queryClient.invalidateQueries({ queryKey: APP_PACKAGE_OPERATIONS_QUERY_KEY }),
|
|
112
|
+
queryClient.invalidateQueries({ queryKey: APP_DATA_QUERY_KEY }),
|
|
111
113
|
]);
|
|
112
114
|
},
|
|
113
115
|
});
|
|
@@ -48,6 +48,7 @@ export type AppMarketplaceDetailView = AppMarketplaceItemView & {
|
|
|
48
48
|
schemaVersion: 1 | 2;
|
|
49
49
|
icon?: string;
|
|
50
50
|
engines?: { nextclaw?: string };
|
|
51
|
+
runtime?: { profile: 'panel-only' | 'wasi' | 'native-process' };
|
|
51
52
|
components?: Array<{
|
|
52
53
|
kind: 'panel' | 'service';
|
|
53
54
|
path: string;
|
|
@@ -60,8 +61,8 @@ export type AppMarketplaceDetailView = AppMarketplaceItemView & {
|
|
|
60
61
|
description?: string;
|
|
61
62
|
}>;
|
|
62
63
|
allowedDomains?: string[];
|
|
63
|
-
storage?: { namespace?: string };
|
|
64
|
-
capabilities?: { hostBridge?: boolean };
|
|
64
|
+
storage?: boolean | { namespace?: string };
|
|
65
|
+
capabilities?: { hostBridge?: boolean; nativeProcess?: boolean };
|
|
65
66
|
};
|
|
66
67
|
};
|
|
67
68
|
|