@nextclaw/ui 0.12.35-beta.3 → 0.12.35-beta.4
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 +63 -0
- package/dist/assets/api-BTZui7nj.js +15 -0
- package/dist/assets/{book-open--T9dJHnp.js → book-open-qc0UVn6m.js} +1 -1
- package/dist/assets/channels-list-page-DBtU_M1Y.js +8 -0
- package/dist/assets/{chat-page-DOKLoiqI.js → chat-page-BcoJYo91.js} +2 -2
- package/dist/assets/config-split-page-DW8tP9Bc.js +1 -0
- package/dist/assets/createLucideIcon-CA0sgoMJ.js +1 -0
- package/dist/assets/desktop-update-config-DARSwQG8.js +1 -0
- package/dist/assets/dist-D5zsy7pX.js +1 -0
- package/dist/assets/dist-DNxIa3pe.js +41 -0
- package/dist/assets/doc-browser-B9skxzVo.js +1 -0
- package/dist/assets/doc-browser-CwyITRXx.js +1 -0
- package/dist/assets/doc-browser-context-Do2qSrB8.js +1 -0
- package/dist/assets/{external-link-CZ9bQY0L.js → external-link-C9fbyi6k.js} +1 -1
- package/dist/assets/index-BYRP_BMj.css +1 -0
- package/dist/assets/index-DckPGTdM.js +103 -0
- package/dist/assets/mcp-marketplace-page-BiExW5wq.js +40 -0
- package/dist/assets/mcp-marketplace-page-Cj6MDSkE.js +1 -0
- package/dist/assets/model-config-DbdY7fF1.js +1 -0
- package/dist/assets/notice-card-CyiVcu_Z.js +5 -0
- package/dist/assets/play-D-s0C4MN.js +1 -0
- package/dist/assets/plus-DOwjM_kF.js +1 -0
- package/dist/assets/provider-scoped-model-input-Bk8jlpRm.js +1 -0
- package/dist/assets/providers-list-CJjNStDV.js +1 -0
- package/dist/assets/react-BaRotFd6.js +1 -0
- package/dist/assets/remote-gl8QckNc.js +1 -0
- package/dist/assets/runtime-config-page-BMFc1yzf.js +1 -0
- package/dist/assets/{save-B6wbBLkU.js → save-CG4Hb1u8.js} +1 -1
- package/dist/assets/{search-config-BFmG4_F7.js → search-config-DUOR26if.js} +1 -1
- package/dist/assets/{search-Czito3Dk.js → search-lWpOIHEn.js} +1 -1
- package/dist/assets/secrets-config-ZbLzjIRx.js +3 -0
- package/dist/assets/setting-row-CNfTKDjc.js +1 -0
- package/dist/assets/tabs-custom-BbZoysAl.js +1 -0
- package/dist/assets/tag-chip-Dl8JbhC7.js +1 -0
- package/dist/assets/x-D6GwT_8k.js +1 -0
- package/dist/index.html +15 -17
- package/package.json +8 -9
- package/src/app/components/app-presenter-provider.tsx +21 -0
- package/src/app/components/layout/sidebar.layout.test.tsx +2 -2
- package/src/app/components/layout/sidebar.tsx +3 -3
- package/src/app/index.tsx +5 -3
- package/src/app/presenters/app.presenter.ts +24 -0
- package/src/features/account/components/account-panel.tsx +11 -11
- package/src/features/account/index.ts +1 -4
- package/src/features/account/managers/account.manager.ts +52 -27
- package/src/features/account/stores/account.store.ts +0 -15
- package/src/features/apps/components/apps-panel.tsx +43 -0
- package/src/features/apps/index.ts +1 -0
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +8 -8
- package/src/features/chat/components/layout/chat-sidebar.tsx +3 -2
- package/src/features/chat/managers/chat-session-list.manager.test.ts +58 -0
- package/src/features/chat/stores/chat-session-list.store.ts +58 -7
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
- package/src/features/panel-apps/components/panel-apps-list.tsx +6 -2
- package/src/features/panel-apps/index.ts +3 -1
- package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +111 -0
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +247 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.test.ts +23 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +42 -16
- package/src/features/remote/components/remote-access-page.test.tsx +2 -2
- package/src/features/remote/components/remote-access-page.tsx +9 -9
- package/src/features/remote/managers/remote-access.manager.ts +24 -41
- package/src/features/service-apps/components/service-action-authorization-dialog.tsx +78 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +290 -0
- package/src/features/service-apps/hooks/use-service-apps.ts +66 -0
- package/src/features/service-apps/index.ts +3 -0
- package/src/features/service-apps/managers/service-action-authorization.manager.ts +10 -0
- package/src/features/service-apps/stores/service-action-authorization.store.ts +53 -0
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +150 -1
- package/src/shared/components/doc-browser/doc-browser-context.tsx +34 -443
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +7 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +16 -1
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +272 -0
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +140 -0
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +47 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +47 -0
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +95 -0
- package/src/shared/lib/api/managers/client.manager.ts +2 -1
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +31 -0
- package/src/shared/lib/transport/local-transport.service.ts +18 -2
- package/src/shared/lib/transport/remote-transport.service.ts +19 -3
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
- package/dist/assets/api-D3uoaOR8.js +0 -15
- package/dist/assets/channels-list-page-DHmz9iHg.js +0 -8
- package/dist/assets/config-split-page-Dutq3R5o.js +0 -1
- package/dist/assets/createLucideIcon-Cwr2o6Zq.js +0 -1
- package/dist/assets/desktop-update-config-ClbDsIOs.js +0 -1
- package/dist/assets/dist-e5WATjIW.js +0 -1
- package/dist/assets/doc-browser-ChY6hMn8.js +0 -1
- package/dist/assets/doc-browser-cR1vWRs1.js +0 -1
- package/dist/assets/doc-browser-context-CLczyX2Z.js +0 -1
- package/dist/assets/es2015-BJw-hrex.js +0 -41
- package/dist/assets/i18n-tWKGEGxB.js +0 -1
- package/dist/assets/index-DPz9DRZN.js +0 -103
- package/dist/assets/index-HvJDzfSU.css +0 -1
- package/dist/assets/key-round-B8tTb3ZJ.js +0 -1
- package/dist/assets/mcp-marketplace-page-BYPxLdUR.js +0 -40
- package/dist/assets/mcp-marketplace-page-DXCvk7wJ.js +0 -1
- package/dist/assets/model-config-DcnC_fSr.js +0 -1
- package/dist/assets/notice-card-DUDbOvqx.js +0 -5
- package/dist/assets/play-CV_kwi9X.js +0 -1
- package/dist/assets/plus-BqfNq7Tu.js +0 -1
- package/dist/assets/provider-scoped-model-input-eO4XY0iY.js +0 -1
- package/dist/assets/providers-list-BgvjRVmp.js +0 -1
- package/dist/assets/remote-BCuZ1JJe.js +0 -1
- package/dist/assets/rotate-cw-BKEhWuWp.js +0 -1
- package/dist/assets/runtime-config-page-B2XPXHrk.js +0 -1
- package/dist/assets/secrets-config-4uY5sk-V.js +0 -3
- package/dist/assets/select-CmmDgG8o.js +0 -1
- package/dist/assets/setting-row-BUO6FeN1.js +0 -1
- package/dist/assets/tabs-custom-D3IZL9WX.js +0 -1
- package/dist/assets/tag-chip-Dm_pOtE3.js +0 -1
- package/dist/assets/x-DBP3WiC9.js +0 -1
- package/src/app/components/app-manager-provider.tsx +0 -20
- package/src/app/managers/app.manager.ts +0 -12
- /package/dist/assets/{config-hints-DGfwbumO.js → config-hints-CTqBnCDp.js} +0 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { NextClawClientError } from '@nextclaw/client-sdk';
|
|
2
|
+
import type {
|
|
3
|
+
PanelAppBridgeSessionView,
|
|
4
|
+
ServiceActionGrantView,
|
|
5
|
+
ServiceActionInvokeResultView,
|
|
6
|
+
ServiceActionListView,
|
|
7
|
+
} from '@nextclaw/client-sdk';
|
|
8
|
+
import type { ServiceActionAuthorizationManager } from '@/features/service-apps';
|
|
9
|
+
import type { DocBrowserIframeMessageParams } from '@/shared/components/doc-browser/doc-browser-renderer.types';
|
|
10
|
+
import { nextclawClient } from '@/shared/lib/api';
|
|
11
|
+
|
|
12
|
+
type PanelAppBridgeRequest = {
|
|
13
|
+
type: 'nextclaw:panel-app-service-actions:request';
|
|
14
|
+
requestId: string;
|
|
15
|
+
method: 'invoke' | 'list' | 'requestGrant' | 'revokeGrant';
|
|
16
|
+
payload?: {
|
|
17
|
+
actionId?: string;
|
|
18
|
+
input?: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type PanelAppBridgeResponse =
|
|
23
|
+
| {
|
|
24
|
+
type: 'nextclaw:panel-app-service-actions:response';
|
|
25
|
+
requestId: string;
|
|
26
|
+
ok: true;
|
|
27
|
+
data: unknown;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
type: 'nextclaw:panel-app-service-actions:response';
|
|
31
|
+
requestId: string;
|
|
32
|
+
ok: false;
|
|
33
|
+
error: {
|
|
34
|
+
code?: string;
|
|
35
|
+
message: string;
|
|
36
|
+
details?: Record<string, unknown>;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export class PanelAppBridgeManager {
|
|
41
|
+
private readonly sessions = new Map<string, Promise<PanelAppBridgeSessionView>>();
|
|
42
|
+
|
|
43
|
+
constructor(private readonly authorizationManager: ServiceActionAuthorizationManager) {}
|
|
44
|
+
|
|
45
|
+
handleIframeMessage = ({ event, iframe, tab }: DocBrowserIframeMessageParams): void => {
|
|
46
|
+
if (!this.isBridgeRequest(event.data)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
void this.handleBridgeRequest({ event, iframe, tab }, event.data);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
private handleBridgeRequest = async (
|
|
56
|
+
params: DocBrowserIframeMessageParams,
|
|
57
|
+
request: PanelAppBridgeRequest,
|
|
58
|
+
): Promise<void> => {
|
|
59
|
+
try {
|
|
60
|
+
const session = await this.getSession(params);
|
|
61
|
+
const data = await this.dispatchRequest(session, request);
|
|
62
|
+
this.postResponse(params, {
|
|
63
|
+
type: 'nextclaw:panel-app-service-actions:response',
|
|
64
|
+
requestId: request.requestId,
|
|
65
|
+
ok: true,
|
|
66
|
+
data,
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
this.postResponse(params, {
|
|
70
|
+
type: 'nextclaw:panel-app-service-actions:response',
|
|
71
|
+
requestId: request.requestId,
|
|
72
|
+
ok: false,
|
|
73
|
+
error: this.toBridgeError(error),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
private dispatchRequest = async (
|
|
79
|
+
session: PanelAppBridgeSessionView,
|
|
80
|
+
request: PanelAppBridgeRequest,
|
|
81
|
+
): Promise<
|
|
82
|
+
| ServiceActionGrantView
|
|
83
|
+
| ServiceActionInvokeResultView
|
|
84
|
+
| ServiceActionListView
|
|
85
|
+
| { revoked: boolean }
|
|
86
|
+
> => {
|
|
87
|
+
switch (request.method) {
|
|
88
|
+
case 'list':
|
|
89
|
+
return await nextclawClient.serviceApps.listServiceActions({
|
|
90
|
+
bridgeSessionToken: session.token,
|
|
91
|
+
});
|
|
92
|
+
case 'requestGrant':
|
|
93
|
+
return await this.confirmAndGrant(session, this.requireActionId(request));
|
|
94
|
+
case 'revokeGrant':
|
|
95
|
+
return await nextclawClient.serviceApps.revokeServiceAction(
|
|
96
|
+
this.requireActionId(request),
|
|
97
|
+
{ bridgeSessionToken: session.token },
|
|
98
|
+
);
|
|
99
|
+
case 'invoke':
|
|
100
|
+
return await this.invokeWithAuthorization(session, request);
|
|
101
|
+
default:
|
|
102
|
+
throw new Error(`Unsupported panel bridge method: ${String(request.method)}`);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
private invokeWithAuthorization = async (
|
|
107
|
+
session: PanelAppBridgeSessionView,
|
|
108
|
+
request: PanelAppBridgeRequest,
|
|
109
|
+
): Promise<ServiceActionInvokeResultView> => {
|
|
110
|
+
const actionId = this.requireActionId(request);
|
|
111
|
+
try {
|
|
112
|
+
return await nextclawClient.serviceApps.invokeServiceAction(
|
|
113
|
+
actionId,
|
|
114
|
+
request.payload?.input,
|
|
115
|
+
{ bridgeSessionToken: session.token },
|
|
116
|
+
);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!(error instanceof NextClawClientError) || error.code !== 'AUTHORIZATION_REQUIRED') {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
await this.confirmAndGrant(session, actionId, request.payload?.input);
|
|
122
|
+
return await nextclawClient.serviceApps.invokeServiceAction(
|
|
123
|
+
actionId,
|
|
124
|
+
request.payload?.input,
|
|
125
|
+
{ bridgeSessionToken: session.token },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
private confirmAndGrant = async (
|
|
131
|
+
session: PanelAppBridgeSessionView,
|
|
132
|
+
actionId: string,
|
|
133
|
+
input?: Record<string, unknown>,
|
|
134
|
+
): Promise<ServiceActionGrantView> => {
|
|
135
|
+
const action = await this.findAction(session, actionId);
|
|
136
|
+
const allowed = await this.authorizationManager.requestAuthorization({
|
|
137
|
+
panelAppId: session.panelAppId,
|
|
138
|
+
actionId,
|
|
139
|
+
actionTitle: action?.title,
|
|
140
|
+
actionDescription: action?.description,
|
|
141
|
+
risk: action?.risk,
|
|
142
|
+
inputPreview: this.createInputPreview(input),
|
|
143
|
+
});
|
|
144
|
+
if (!allowed) {
|
|
145
|
+
throw new NextClawClientError({
|
|
146
|
+
code: 'AUTHORIZATION_REJECTED',
|
|
147
|
+
message: `Permission rejected for ${actionId}.`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return await nextclawClient.serviceApps.grantServiceAction(
|
|
151
|
+
actionId,
|
|
152
|
+
{ bridgeSessionToken: session.token },
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
private findAction = async (
|
|
157
|
+
session: PanelAppBridgeSessionView,
|
|
158
|
+
actionId: string,
|
|
159
|
+
): Promise<ServiceActionListView['actions'][number] | undefined> => {
|
|
160
|
+
const actions = await nextclawClient.serviceApps.listServiceActions({
|
|
161
|
+
bridgeSessionToken: session.token,
|
|
162
|
+
});
|
|
163
|
+
return actions.actions.find((action) => action.id === actionId);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
private createInputPreview = (input: Record<string, unknown> | undefined): string | undefined => {
|
|
167
|
+
if (!input || Object.keys(input).length === 0) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const value = JSON.stringify(input, null, 2);
|
|
172
|
+
return value.length > 500 ? `${value.slice(0, 500)}...` : value;
|
|
173
|
+
} catch {
|
|
174
|
+
return '[unserializable input]';
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
private getSession = async (
|
|
179
|
+
params: DocBrowserIframeMessageParams,
|
|
180
|
+
): Promise<PanelAppBridgeSessionView> => {
|
|
181
|
+
const panelAppId = this.readPanelAppId(params.tab.currentUrl);
|
|
182
|
+
const key = `${params.tab.id}:${panelAppId}`;
|
|
183
|
+
let session = this.sessions.get(key);
|
|
184
|
+
if (!session) {
|
|
185
|
+
session = nextclawClient.panelApps.createBridgeSession({
|
|
186
|
+
panelAppId,
|
|
187
|
+
tabId: params.tab.id,
|
|
188
|
+
});
|
|
189
|
+
this.sessions.set(key, session);
|
|
190
|
+
}
|
|
191
|
+
return await session;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
private readPanelAppId = (url: string): string => {
|
|
195
|
+
const parsed = new URL(url, window.location.origin);
|
|
196
|
+
const match = /^\/api\/panel-apps\/([^/]+)\/content$/.exec(parsed.pathname);
|
|
197
|
+
if (!match?.[1]) {
|
|
198
|
+
throw new Error('Current tab is not a Panel App.');
|
|
199
|
+
}
|
|
200
|
+
return decodeURIComponent(match[1]);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
private requireActionId = (request: PanelAppBridgeRequest): string => {
|
|
204
|
+
const actionId = request.payload?.actionId?.trim();
|
|
205
|
+
if (!actionId) {
|
|
206
|
+
throw new Error('Panel bridge actionId is required.');
|
|
207
|
+
}
|
|
208
|
+
return actionId;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
private postResponse = (
|
|
212
|
+
params: DocBrowserIframeMessageParams,
|
|
213
|
+
response: PanelAppBridgeResponse,
|
|
214
|
+
): void => {
|
|
215
|
+
params.iframe?.contentWindow?.postMessage(response, '*');
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
private toBridgeError = (
|
|
219
|
+
error: unknown,
|
|
220
|
+
): { code?: string; message: string; details?: Record<string, unknown> } => {
|
|
221
|
+
if (error instanceof NextClawClientError) {
|
|
222
|
+
return {
|
|
223
|
+
code: error.code,
|
|
224
|
+
message: error.message,
|
|
225
|
+
details: error.details,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
message: error instanceof Error ? error.message : String(error),
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
private isBridgeRequest = (value: unknown): value is PanelAppBridgeRequest => {
|
|
234
|
+
if (!value || typeof value !== 'object') {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const candidate = value as Partial<PanelAppBridgeRequest>;
|
|
238
|
+
return (
|
|
239
|
+
candidate.type === 'nextclaw:panel-app-service-actions:request' &&
|
|
240
|
+
typeof candidate.requestId === 'string' &&
|
|
241
|
+
(candidate.method === 'invoke' ||
|
|
242
|
+
candidate.method === 'list' ||
|
|
243
|
+
candidate.method === 'requestGrant' ||
|
|
244
|
+
candidate.method === 'revokeGrant')
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAppsPanelUrl,
|
|
3
|
+
getAppsPanelTabFromUrl,
|
|
4
|
+
} from '@/features/panel-apps/utils/panel-app-doc-browser.utils';
|
|
5
|
+
|
|
6
|
+
describe('panel app doc browser URL helpers', () => {
|
|
7
|
+
it('uses the root apps URL for the default panel apps tab', () => {
|
|
8
|
+
expect(createAppsPanelUrl('panel-apps')).toBe('nextclaw://apps');
|
|
9
|
+
expect(getAppsPanelTabFromUrl('nextclaw://apps')).toBe('panel-apps');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('round-trips the service apps tab through the hidden apps URL', () => {
|
|
13
|
+
const url = createAppsPanelUrl('service-apps');
|
|
14
|
+
|
|
15
|
+
expect(url).toBe('nextclaw://apps?tab=service-apps');
|
|
16
|
+
expect(getAppsPanelTabFromUrl(url)).toBe('service-apps');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('falls back to panel apps for unknown URL state', () => {
|
|
20
|
+
expect(getAppsPanelTabFromUrl('nextclaw://apps?tab=unknown')).toBe('panel-apps');
|
|
21
|
+
expect(getAppsPanelTabFromUrl('not a url')).toBe('panel-apps');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { AppWindow } from 'lucide-react';
|
|
1
|
+
import { AppWindow, Boxes } from 'lucide-react';
|
|
2
2
|
import type { DocBrowserContextValue } from '@/shared/components/doc-browser/doc-browser-context';
|
|
3
3
|
import type { DocBrowserCustomTabRenderers } from '@/shared/components/doc-browser/doc-browser-renderer.types';
|
|
4
|
+
import { getPresenter } from '@/app/presenters/app.presenter';
|
|
5
|
+
import { AppsPanel, type AppsPanelTab } from '@/features/apps';
|
|
4
6
|
import { PanelAppToolbar } from '@/features/panel-apps/components/panel-app-toolbar';
|
|
5
|
-
import { PanelAppsList } from '@/features/panel-apps/components/panel-apps-list';
|
|
6
7
|
import { t } from '@/shared/lib/i18n';
|
|
7
8
|
|
|
8
|
-
export const
|
|
9
|
+
export const APPS_TAB_KIND = 'apps';
|
|
9
10
|
export const PANEL_APP_TAB_KIND = 'panel-app';
|
|
10
|
-
const
|
|
11
|
+
const APPS_URL = 'nextclaw://apps';
|
|
12
|
+
const DEFAULT_APPS_PANEL_TAB: AppsPanelTab = 'panel-apps';
|
|
11
13
|
const PANEL_APP_IFRAME_SANDBOX = [
|
|
12
|
-
'allow-same-origin',
|
|
13
14
|
'allow-scripts',
|
|
14
15
|
'allow-forms',
|
|
15
16
|
'allow-modals',
|
|
@@ -20,20 +21,44 @@ const PANEL_APP_IFRAME_SANDBOX = [
|
|
|
20
21
|
'allow-presentation',
|
|
21
22
|
].join(' ');
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
function isAppsPanelTab(value: unknown): value is AppsPanelTab {
|
|
25
|
+
return value === 'panel-apps' || value === 'service-apps';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createAppsPanelUrl(tab: AppsPanelTab = DEFAULT_APPS_PANEL_TAB): string {
|
|
29
|
+
return tab === DEFAULT_APPS_PANEL_TAB ? APPS_URL : `${APPS_URL}?tab=${tab}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getAppsPanelTabFromUrl(url: string): AppsPanelTab {
|
|
33
|
+
try {
|
|
34
|
+
const tab = new URL(url).searchParams.get('tab');
|
|
35
|
+
return isAppsPanelTab(tab) ? tab : DEFAULT_APPS_PANEL_TAB;
|
|
36
|
+
} catch {
|
|
37
|
+
return DEFAULT_APPS_PANEL_TAB;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function openApps(docBrowser: Pick<DocBrowserContextValue, 'open'>): void {
|
|
42
|
+
docBrowser.open(createAppsPanelUrl(), {
|
|
43
|
+
kind: APPS_TAB_KIND,
|
|
44
|
+
title: t('appsTitle'),
|
|
45
|
+
dedupeKey: 'apps',
|
|
28
46
|
});
|
|
29
47
|
}
|
|
30
48
|
|
|
31
49
|
export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
|
|
32
|
-
[
|
|
33
|
-
getTitle: () => t('
|
|
34
|
-
renderIcon: () => <
|
|
35
|
-
renderContent: ({ open }) => (
|
|
36
|
-
<
|
|
50
|
+
[APPS_TAB_KIND]: {
|
|
51
|
+
getTitle: () => t('appsTitle'),
|
|
52
|
+
renderIcon: () => <Boxes className="w-4 h-4 text-primary shrink-0" />,
|
|
53
|
+
renderContent: ({ currentUrl, open }) => (
|
|
54
|
+
<AppsPanel
|
|
55
|
+
activeTab={getAppsPanelTabFromUrl(currentUrl)}
|
|
56
|
+
onActiveTabChange={(tab) => open(createAppsPanelUrl(tab), {
|
|
57
|
+
activate: false,
|
|
58
|
+
kind: APPS_TAB_KIND,
|
|
59
|
+
title: t('appsTitle'),
|
|
60
|
+
dedupeKey: 'apps',
|
|
61
|
+
})}
|
|
37
62
|
onOpenPanelApp={(entry) => open(entry.contentPath, {
|
|
38
63
|
kind: PANEL_APP_TAB_KIND,
|
|
39
64
|
title: entry.title,
|
|
@@ -45,10 +70,11 @@ export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
|
|
|
45
70
|
[PANEL_APP_TAB_KIND]: {
|
|
46
71
|
getIframeSandbox: () => PANEL_APP_IFRAME_SANDBOX,
|
|
47
72
|
getTitle: (tab) => tab.title || t('panelAppsTitle'),
|
|
73
|
+
onIframeMessage: (params) => getPresenter().panelAppBridgeManager.handleIframeMessage(params),
|
|
48
74
|
renderIcon: () => <AppWindow className="w-4 h-4 text-primary shrink-0" />,
|
|
49
75
|
renderToolbar: ({ open, refreshIframe }) => (
|
|
50
76
|
<PanelAppToolbar
|
|
51
|
-
|
|
77
|
+
onOpenApps={() => openApps({ open })}
|
|
52
78
|
onRefresh={refreshIframe}
|
|
53
79
|
/>
|
|
54
80
|
),
|
|
@@ -20,8 +20,8 @@ vi.mock("@/features/remote/hooks/use-remote-access", () => ({
|
|
|
20
20
|
useRemoteStatus: () => mocks.statusQuery,
|
|
21
21
|
}));
|
|
22
22
|
|
|
23
|
-
vi.mock("@/app/components/app-
|
|
24
|
-
|
|
23
|
+
vi.mock("@/app/components/app-presenter-provider", () => ({
|
|
24
|
+
useAppPresenter: () => ({
|
|
25
25
|
remoteAccessManager: {
|
|
26
26
|
reauthorizeRemoteAccess: mocks.reauthorizeRemoteAccess,
|
|
27
27
|
repairRemoteAccess: mocks.repairRemoteAccess,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@/shared/components/ui/card";
|
|
10
10
|
import { NoticeCard } from "@/shared/components/ui/notice-card";
|
|
11
11
|
import { StatusDot } from "@/shared/components/ui/status-dot";
|
|
12
|
-
import {
|
|
12
|
+
import { useAppPresenter } from "@/app/components/app-presenter-provider";
|
|
13
13
|
import {
|
|
14
14
|
buildRemoteAccessFeedbackView,
|
|
15
15
|
resolveRemoteWebBase,
|
|
@@ -45,7 +45,7 @@ function KeyValueRow(props: {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function RemoteAccessPage() {
|
|
48
|
-
const
|
|
48
|
+
const presenter = useAppPresenter();
|
|
49
49
|
const remoteStatus = useRemoteStatus();
|
|
50
50
|
const status = remoteStatus.data;
|
|
51
51
|
const actionLabel = useRemoteAccessStore((state) => state.actionLabel);
|
|
@@ -64,8 +64,8 @@ export function RemoteAccessPage() {
|
|
|
64
64
|
const { hero: heroView, issueHint } = feedbackView;
|
|
65
65
|
|
|
66
66
|
useEffect(() => {
|
|
67
|
-
|
|
68
|
-
}, [
|
|
67
|
+
presenter.remoteAccessManager.syncStatus(status);
|
|
68
|
+
}, [presenter, status]);
|
|
69
69
|
|
|
70
70
|
if (remoteStatus.isLoading && !status) {
|
|
71
71
|
return <div className="p-8 text-gray-400">{t("remoteLoading")}</div>;
|
|
@@ -117,18 +117,18 @@ export function RemoteAccessPage() {
|
|
|
117
117
|
<Button
|
|
118
118
|
onClick={() => {
|
|
119
119
|
if (feedbackView.primaryAction?.kind === "reauthorize") {
|
|
120
|
-
void
|
|
120
|
+
void presenter.remoteAccessManager.reauthorizeRemoteAccess(
|
|
121
121
|
status,
|
|
122
122
|
);
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
if (feedbackView.primaryAction?.kind === "repair") {
|
|
126
|
-
void
|
|
126
|
+
void presenter.remoteAccessManager.repairRemoteAccess(
|
|
127
127
|
status,
|
|
128
128
|
);
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
|
-
void
|
|
131
|
+
void presenter.remoteAccessManager.enableRemoteAccess(
|
|
132
132
|
status,
|
|
133
133
|
);
|
|
134
134
|
}}
|
|
@@ -143,7 +143,7 @@ export function RemoteAccessPage() {
|
|
|
143
143
|
|
|
144
144
|
<Button
|
|
145
145
|
variant="outline"
|
|
146
|
-
onClick={() => void
|
|
146
|
+
onClick={() => void presenter.accountManager.openNextClawWeb()}
|
|
147
147
|
disabled={busy || !canOpenDeviceList}
|
|
148
148
|
>
|
|
149
149
|
<SquareArrowOutUpRight className="mr-2 h-4 w-4" />
|
|
@@ -154,7 +154,7 @@ export function RemoteAccessPage() {
|
|
|
154
154
|
<Button
|
|
155
155
|
variant="outline"
|
|
156
156
|
onClick={() =>
|
|
157
|
-
void
|
|
157
|
+
void presenter.remoteAccessManager.disableRemoteAccess(
|
|
158
158
|
status,
|
|
159
159
|
)
|
|
160
160
|
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { controlRemoteService, fetchRemoteDoctor, updateRemoteSettings } from '@/shared/lib/api';
|
|
2
2
|
import type { RemoteAccessView } from '@/shared/lib/api';
|
|
3
|
-
import type { AccountManager
|
|
3
|
+
import type { AccountManager } from '@/features/account';
|
|
4
4
|
import { t } from '@/shared/lib/i18n';
|
|
5
5
|
import { toast } from 'sonner';
|
|
6
6
|
import { refreshRemoteStatus } from '@/features/remote/services/remote-access-query.service';
|
|
7
7
|
import { useRemoteAccessStore } from '@/features/remote/stores/remote-access.store';
|
|
8
8
|
|
|
9
9
|
export class RemoteAccessManager {
|
|
10
|
-
private accountManager: AccountManager
|
|
11
|
-
|
|
12
|
-
bindAccountManager = (accountManager: AccountManager) => {
|
|
13
|
-
this.accountManager = accountManager;
|
|
14
|
-
};
|
|
10
|
+
constructor(private readonly params: { accountManager: AccountManager }) {}
|
|
15
11
|
|
|
16
12
|
syncStatus = (status: RemoteAccessView | undefined) => {
|
|
17
13
|
if (!status) {
|
|
@@ -43,14 +39,8 @@ export class RemoteAccessManager {
|
|
|
43
39
|
enableRemoteAccess = async (status: RemoteAccessView | undefined) => {
|
|
44
40
|
const currentStatus = status ?? (await refreshRemoteStatus());
|
|
45
41
|
const draft = useRemoteAccessStore.getState();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
pendingAction: { type: 'enable-remote' },
|
|
49
|
-
apiBase: draft.platformApiBase
|
|
50
|
-
});
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
await this.applyEnabledState(true, currentStatus);
|
|
42
|
+
const readyStatus = await this.ensureRemoteAccount(currentStatus, draft.platformApiBase);
|
|
43
|
+
await this.applyEnabledState(true, readyStatus);
|
|
54
44
|
};
|
|
55
45
|
|
|
56
46
|
disableRemoteAccess = async (status: RemoteAccessView | undefined) => {
|
|
@@ -60,13 +50,8 @@ export class RemoteAccessManager {
|
|
|
60
50
|
|
|
61
51
|
repairRemoteAccess = async (status: RemoteAccessView | undefined) => {
|
|
62
52
|
const currentStatus = status ?? (await refreshRemoteStatus());
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
pendingAction: { type: 'enable-remote' }
|
|
66
|
-
});
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const action = currentStatus.service.running ? 'restart' : 'start';
|
|
53
|
+
const readyStatus = await this.ensureRemoteAccount(currentStatus);
|
|
54
|
+
const action = readyStatus.service.running ? 'restart' : 'start';
|
|
70
55
|
await this.runManagedAction({
|
|
71
56
|
actionLabel: action === 'restart' ? t('remoteActionRestarting') : t('remoteActionStarting'),
|
|
72
57
|
job: async () => {
|
|
@@ -80,11 +65,11 @@ export class RemoteAccessManager {
|
|
|
80
65
|
|
|
81
66
|
reauthorizeRemoteAccess = async (status: RemoteAccessView | undefined) => {
|
|
82
67
|
const currentStatus = status ?? (await refreshRemoteStatus());
|
|
83
|
-
await this.accountManager
|
|
68
|
+
const readyStatus = await this.params.accountManager.startBrowserSignInAndWait({
|
|
84
69
|
status: currentStatus,
|
|
85
|
-
apiBase: useRemoteAccessStore.getState().platformApiBase
|
|
86
|
-
pendingAction: { type: 'repair-remote' }
|
|
70
|
+
apiBase: useRemoteAccessStore.getState().platformApiBase
|
|
87
71
|
});
|
|
72
|
+
await this.repairRemoteAccess(readyStatus);
|
|
88
73
|
};
|
|
89
74
|
|
|
90
75
|
saveAdvancedSettings = async (status: RemoteAccessView | undefined) => {
|
|
@@ -128,19 +113,6 @@ export class RemoteAccessManager {
|
|
|
128
113
|
await this.runServiceAction('stop', t('remoteActionStopping'));
|
|
129
114
|
};
|
|
130
115
|
|
|
131
|
-
resumePendingActionAfterSignIn = async (action: AccountPendingAction, status: RemoteAccessView) => {
|
|
132
|
-
if (!action) {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (action.type === 'enable-remote') {
|
|
136
|
-
await this.applyEnabledState(true, status);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (action.type === 'repair-remote') {
|
|
140
|
-
await this.repairRemoteAccess(status);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
116
|
private applyEnabledState = async (enabled: boolean, status: RemoteAccessView) => {
|
|
145
117
|
const draft = useRemoteAccessStore.getState();
|
|
146
118
|
await this.runManagedAction({
|
|
@@ -166,6 +138,16 @@ export class RemoteAccessManager {
|
|
|
166
138
|
});
|
|
167
139
|
};
|
|
168
140
|
|
|
141
|
+
private ensureRemoteAccount = async (status: RemoteAccessView, apiBase?: string) => {
|
|
142
|
+
if (status.account.loggedIn) {
|
|
143
|
+
return status;
|
|
144
|
+
}
|
|
145
|
+
return await this.params.accountManager.ensureSignedIn({
|
|
146
|
+
apiBase,
|
|
147
|
+
status
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
169
151
|
private runServiceAction = async (action: 'start' | 'restart' | 'stop', actionLabel: string) => {
|
|
170
152
|
await this.runManagedAction({
|
|
171
153
|
actionLabel,
|
|
@@ -191,11 +173,12 @@ export class RemoteAccessManager {
|
|
|
191
173
|
job: () => Promise<void>;
|
|
192
174
|
successMessage?: string;
|
|
193
175
|
}) => {
|
|
194
|
-
|
|
176
|
+
const { actionLabel, job, successMessage } = params;
|
|
177
|
+
useRemoteAccessStore.getState().beginAction(actionLabel);
|
|
195
178
|
try {
|
|
196
|
-
await
|
|
197
|
-
if (
|
|
198
|
-
toast.success(
|
|
179
|
+
await job();
|
|
180
|
+
if (successMessage) {
|
|
181
|
+
toast.success(successMessage);
|
|
199
182
|
}
|
|
200
183
|
} catch (error) {
|
|
201
184
|
const message = error instanceof Error ? error.message : t('error');
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ShieldAlert } from 'lucide-react';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from '@/shared/components/ui/dialog';
|
|
10
|
+
import { Button } from '@/shared/components/ui/button';
|
|
11
|
+
import { t } from '@/shared/lib/i18n';
|
|
12
|
+
import { useServiceActionAuthorizationStore } from '@/features/service-apps/stores/service-action-authorization.store';
|
|
13
|
+
|
|
14
|
+
export function ServiceActionAuthorizationDialog() {
|
|
15
|
+
const pending = useServiceActionAuthorizationStore((state) => state.pending);
|
|
16
|
+
const resolveAuthorization = useServiceActionAuthorizationStore(
|
|
17
|
+
(state) => state.resolveAuthorization,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Dialog
|
|
22
|
+
open={Boolean(pending)}
|
|
23
|
+
onOpenChange={(open) => {
|
|
24
|
+
if (!open) {
|
|
25
|
+
resolveAuthorization(false);
|
|
26
|
+
}
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<DialogContent className="[&>:last-child]:hidden">
|
|
30
|
+
<DialogHeader>
|
|
31
|
+
<DialogTitle className="flex items-center gap-2">
|
|
32
|
+
<ShieldAlert className="h-4 w-4 text-amber-600" />
|
|
33
|
+
{t('serviceActionAuthorizationTitle')}
|
|
34
|
+
</DialogTitle>
|
|
35
|
+
<DialogDescription>
|
|
36
|
+
{t('serviceActionAuthorizationDescription')}
|
|
37
|
+
</DialogDescription>
|
|
38
|
+
</DialogHeader>
|
|
39
|
+
{pending ? (
|
|
40
|
+
<div className="space-y-3 rounded-md border border-gray-100 bg-gray-50 p-3 text-sm">
|
|
41
|
+
<AuthorizationField label={t('serviceActionAuthorizationSource')} value={pending.panelAppId} />
|
|
42
|
+
<AuthorizationField label={t('serviceActionAuthorizationAction')} value={pending.actionTitle ?? pending.actionId} />
|
|
43
|
+
{pending.actionDescription ? (
|
|
44
|
+
<AuthorizationField label={t('serviceActionAuthorizationPurpose')} value={pending.actionDescription} />
|
|
45
|
+
) : null}
|
|
46
|
+
<AuthorizationField label={t('serviceActionAuthorizationRisk')} value={pending.risk ?? 'dangerous'} />
|
|
47
|
+
{pending.inputPreview ? (
|
|
48
|
+
<AuthorizationField label={t('serviceActionAuthorizationInput')} value={pending.inputPreview} />
|
|
49
|
+
) : null}
|
|
50
|
+
</div>
|
|
51
|
+
) : null}
|
|
52
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
53
|
+
<Button type="button" variant="outline" onClick={() => resolveAuthorization(false)}>
|
|
54
|
+
{t('serviceActionAuthorizationReject')}
|
|
55
|
+
</Button>
|
|
56
|
+
<Button type="button" onClick={() => resolveAuthorization(true)}>
|
|
57
|
+
{t('serviceActionAuthorizationAllow')}
|
|
58
|
+
</Button>
|
|
59
|
+
</DialogFooter>
|
|
60
|
+
</DialogContent>
|
|
61
|
+
</Dialog>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function AuthorizationField({
|
|
66
|
+
label,
|
|
67
|
+
value,
|
|
68
|
+
}: {
|
|
69
|
+
label: string;
|
|
70
|
+
value: string;
|
|
71
|
+
}) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="grid grid-cols-[5rem_minmax(0,1fr)] gap-3">
|
|
74
|
+
<div className="text-xs font-medium text-gray-500">{label}</div>
|
|
75
|
+
<div className="min-w-0 break-words text-xs text-gray-900">{value}</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|