@agent-native/dispatch 0.41.1-nightly-20261104110058 → 0.42.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 +26 -55
- package/dist/actions/get-hosted-apps-settings.d.ts +5 -0
- package/dist/actions/get-hosted-apps-settings.d.ts.map +1 -0
- package/dist/actions/get-hosted-apps-settings.js +25 -0
- package/dist/actions/get-hosted-apps-settings.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +4 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-connected-agents.d.ts +1 -0
- package/dist/actions/list-connected-agents.js +11 -10
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list_apps.js +3 -2
- package/dist/actions/list_apps.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +10 -10
- package/dist/actions/set-hosted-apps-settings.d.ts +7 -0
- package/dist/actions/set-hosted-apps-settings.d.ts.map +1 -0
- package/dist/actions/set-hosted-apps-settings.js +35 -0
- package/dist/actions/set-hosted-apps-settings.js.map +1 -0
- package/dist/components/app-card-tiers.d.ts +13 -0
- package/dist/components/app-card-tiers.d.ts.map +1 -0
- package/dist/components/app-card-tiers.js +22 -0
- package/dist/components/app-card-tiers.js.map +1 -0
- package/dist/components/app-list-row.d.ts +12 -4
- package/dist/components/app-list-row.d.ts.map +1 -1
- package/dist/components/app-list-row.js +19 -4
- package/dist/components/app-list-row.js.map +1 -1
- package/dist/components/app-open-actions.d.ts +4 -0
- package/dist/components/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +9 -0
- package/dist/components/app-open-actions.js.map +1 -1
- package/dist/components/app-pin-button.d.ts +9 -0
- package/dist/components/app-pin-button.d.ts.map +1 -0
- package/dist/components/app-pin-button.js +29 -0
- package/dist/components/app-pin-button.js.map +1 -0
- package/dist/components/app-tier-sections.d.ts +27 -0
- package/dist/components/app-tier-sections.d.ts.map +1 -0
- package/dist/components/app-tier-sections.js +44 -0
- package/dist/components/app-tier-sections.js.map +1 -0
- package/dist/components/chat-home-apps.d.ts.map +1 -1
- package/dist/components/chat-home-apps.js +19 -26
- package/dist/components/chat-home-apps.js.map +1 -1
- package/dist/components/connected-app-card.d.ts +3 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +6 -3
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/create-app-popover.d.ts +4 -0
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +6 -0
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +25 -35
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/hosted-apps-settings-group.d.ts +6 -0
- package/dist/components/hosted-apps-settings-group.d.ts.map +1 -0
- package/dist/components/hosted-apps-settings-group.js +76 -0
- package/dist/components/hosted-apps-settings-group.js.map +1 -0
- package/dist/components/layout/Layout.d.ts +6 -4
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +97 -80
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +9 -20
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-app-layout-notice.d.ts +9 -0
- package/dist/components/workspace-app-layout-notice.d.ts.map +1 -0
- package/dist/components/workspace-app-layout-notice.js +27 -0
- package/dist/components/workspace-app-layout-notice.js.map +1 -0
- package/dist/components/workspace-app-search.d.ts.map +1 -1
- package/dist/components/workspace-app-search.js +11 -5
- package/dist/components/workspace-app-search.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +7 -2
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/lib/other-apps.d.ts +10 -1
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js +7 -1
- package/dist/lib/other-apps.js.map +1 -1
- package/dist/lib/workspace-app-layout.d.ts +62 -6
- package/dist/lib/workspace-app-layout.d.ts.map +1 -1
- package/dist/lib/workspace-app-layout.js +196 -41
- package/dist/lib/workspace-app-layout.js.map +1 -1
- package/dist/lib/workspace-app-lists.d.ts +17 -0
- package/dist/lib/workspace-app-lists.d.ts.map +1 -0
- package/dist/lib/workspace-app-lists.js +21 -0
- package/dist/lib/workspace-app-lists.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +3 -1
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +5 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +23 -35
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/settings-page.d.ts.map +1 -1
- package/dist/routes/pages/settings-page.js +24 -3
- package/dist/routes/pages/settings-page.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +13 -18
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/app-roles.d.ts +2 -0
- package/dist/server/lib/app-roles.d.ts.map +1 -1
- package/dist/server/lib/app-roles.js +26 -7
- package/dist/server/lib/app-roles.js.map +1 -1
- package/dist/server/lib/hosted-apps-settings-schema.d.ts +6 -0
- package/dist/server/lib/hosted-apps-settings-schema.d.ts.map +1 -0
- package/dist/server/lib/hosted-apps-settings-schema.js +6 -0
- package/dist/server/lib/hosted-apps-settings-schema.js.map +1 -0
- package/dist/server/lib/hosted-apps-settings.d.ts +27 -0
- package/dist/server/lib/hosted-apps-settings.d.ts.map +1 -0
- package/dist/server/lib/hosted-apps-settings.js +72 -0
- package/dist/server/lib/hosted-apps-settings.js.map +1 -0
- package/dist/server/plugins/agent-chat.js +6 -3
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/integrations.js +3 -3
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/hosted-apps-settings.d.ts +3 -0
- package/dist/shared/hosted-apps-settings.d.ts.map +1 -0
- package/dist/shared/hosted-apps-settings.js +3 -0
- package/dist/shared/hosted-apps-settings.js.map +1 -0
- package/package.json +4 -4
- package/src/actions/get-hosted-apps-settings.ts +29 -0
- package/src/actions/hosted-apps-settings.spec.ts +100 -0
- package/src/actions/index.ts +4 -0
- package/src/actions/list-connected-agents.spec.ts +67 -0
- package/src/actions/list-connected-agents.ts +14 -15
- package/src/actions/list_apps.spec.ts +16 -0
- package/src/actions/list_apps.ts +5 -2
- package/src/actions/set-hosted-apps-settings.ts +42 -0
- package/src/components/app-card-tiers.tsx +85 -0
- package/src/components/app-list-row.spec.tsx +54 -0
- package/src/components/app-list-row.tsx +50 -8
- package/src/components/app-open-actions.tsx +16 -0
- package/src/components/app-pin-button.spec.tsx +68 -0
- package/src/components/app-pin-button.tsx +66 -0
- package/src/components/app-tier-sections.spec.tsx +155 -0
- package/src/components/app-tier-sections.tsx +158 -0
- package/src/components/chat-home-apps.spec.tsx +135 -29
- package/src/components/chat-home-apps.tsx +115 -111
- package/src/components/connected-app-card.spec.tsx +18 -2
- package/src/components/connected-app-card.tsx +18 -1
- package/src/components/create-app-popover.tsx +22 -0
- package/src/components/dispatch-control-plane.spec.tsx +89 -20
- package/src/components/dispatch-control-plane.tsx +91 -125
- package/src/components/hosted-apps-settings-group.spec.tsx +218 -0
- package/src/components/hosted-apps-settings-group.tsx +189 -0
- package/src/components/layout/Layout.spec.tsx +356 -49
- package/src/components/layout/Layout.tsx +189 -142
- package/src/components/workspace-app-card.spec.tsx +33 -29
- package/src/components/workspace-app-card.tsx +13 -31
- package/src/components/workspace-app-layout-notice.spec.tsx +106 -0
- package/src/components/workspace-app-layout-notice.tsx +65 -0
- package/src/components/workspace-app-search.spec.tsx +53 -0
- package/src/components/workspace-app-search.tsx +39 -32
- package/src/components/workspace-template-card.spec.tsx +10 -0
- package/src/components/workspace-template-card.tsx +7 -2
- package/src/db/index.spec.ts +9 -0
- package/src/db/index.ts +1 -0
- package/src/lib/other-apps.spec.ts +25 -0
- package/src/lib/other-apps.ts +9 -0
- package/src/lib/workspace-app-layout.spec.ts +61 -3
- package/src/lib/workspace-app-layout.store.spec.tsx +261 -0
- package/src/lib/workspace-app-layout.ts +265 -48
- package/src/lib/workspace-app-lists.ts +32 -0
- package/src/lib/workspace-apps.ts +10 -2
- package/src/routes/pages/apps.spec.tsx +133 -5
- package/src/routes/pages/apps.tsx +79 -140
- package/src/routes/pages/chat.spec.tsx +66 -49
- package/src/routes/pages/settings-page.tsx +31 -3
- package/src/routes/pages/settings.spec.tsx +37 -0
- package/src/server/lib/app-creation-store.spec.ts +14 -0
- package/src/server/lib/app-creation-store.ts +16 -29
- package/src/server/lib/app-creation-store.write-observation.spec.ts +216 -0
- package/src/server/lib/app-roles.spec.ts +48 -1
- package/src/server/lib/app-roles.ts +44 -12
- package/src/server/lib/hosted-apps-settings-schema.ts +6 -0
- package/src/server/lib/hosted-apps-settings.spec.ts +154 -0
- package/src/server/lib/hosted-apps-settings.ts +108 -0
- package/src/server/plugins/agent-chat.spec.ts +16 -0
- package/src/server/plugins/agent-chat.ts +7 -2
- package/src/server/plugins/integrations.ts +3 -3
- package/src/shared/hosted-apps-settings.ts +3 -0
- package/src/styles/dispatch.css +40 -11
- package/dist/components/available-apps-section.d.ts +0 -9
- package/dist/components/available-apps-section.d.ts.map +0 -1
- package/dist/components/available-apps-section.js +0 -97
- package/dist/components/available-apps-section.js.map +0 -1
- package/dist/components/other-apps-section.d.ts +0 -35
- package/dist/components/other-apps-section.d.ts.map +0 -1
- package/dist/components/other-apps-section.js +0 -79
- package/dist/components/other-apps-section.js.map +0 -1
- package/src/components/available-apps-section.tsx +0 -239
- package/src/components/other-apps-section.spec.ts +0 -50
- package/src/components/other-apps-section.tsx +0 -254
package/src/actions/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import getAppCreationSettings from "./get-app-creation-settings.js";
|
|
|
29
29
|
import getDispatchSettings from "./get-dispatch-settings.js";
|
|
30
30
|
import getDreamSettings from "./get-dream-settings.js";
|
|
31
31
|
import getDream from "./get-dream.js";
|
|
32
|
+
import getHostedAppsSettings from "./get-hosted-apps-settings.js";
|
|
32
33
|
import getVaultAccessSettings from "./get-vault-access-settings.js";
|
|
33
34
|
import getWorkspaceInfo from "./get-workspace-info.js";
|
|
34
35
|
import getWorkspaceResourceEffectiveContext from "./get-workspace-resource-effective-context.js";
|
|
@@ -96,6 +97,7 @@ import sendPlatformMessage from "./send-platform-message.js";
|
|
|
96
97
|
import setAppCreationSettings from "./set-app-creation-settings.js";
|
|
97
98
|
import setDispatchApprovalPolicy from "./set-dispatch-approval-policy.js";
|
|
98
99
|
import setDreamSettings from "./set-dream-settings.js";
|
|
100
|
+
import setHostedAppsSettings from "./set-hosted-apps-settings.js";
|
|
99
101
|
import setMcpAppAccess from "./set-mcp-app-access.js";
|
|
100
102
|
import setVaultAccessSettings from "./set-vault-access-settings.js";
|
|
101
103
|
import startWorkspaceAppCreation from "./start-workspace-app-creation.js";
|
|
@@ -137,6 +139,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
|
|
|
137
139
|
"get-dream": getDream,
|
|
138
140
|
"get-dream-settings": getDreamSettings,
|
|
139
141
|
"get-dispatch-settings": getDispatchSettings,
|
|
142
|
+
"get-hosted-apps-settings": getHostedAppsSettings,
|
|
140
143
|
"get-vault-access-settings": getVaultAccessSettings,
|
|
141
144
|
"get-workspace-resource-effective-context":
|
|
142
145
|
getWorkspaceResourceEffectiveContext,
|
|
@@ -206,6 +209,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
|
|
|
206
209
|
"set-app-creation-settings": setAppCreationSettings,
|
|
207
210
|
"set-dispatch-approval-policy": setDispatchApprovalPolicy,
|
|
208
211
|
"set-dream-settings": setDreamSettings,
|
|
212
|
+
"set-hosted-apps-settings": setHostedAppsSettings,
|
|
209
213
|
"set-mcp-app-access": setMcpAppAccess,
|
|
210
214
|
"set-vault-access-settings": setVaultAccessSettings,
|
|
211
215
|
"start-workspace-app-creation": startWorkspaceAppCreation,
|
|
@@ -13,6 +13,9 @@ const mocks = vi.hoisted(() => ({
|
|
|
13
13
|
loadWorkspaceAppsManifest: vi.fn(
|
|
14
14
|
async (): Promise<Array<{ id: string }> | null> => null,
|
|
15
15
|
),
|
|
16
|
+
getOrgSetting: vi.fn(
|
|
17
|
+
async (): Promise<Record<string, unknown> | null> => null,
|
|
18
|
+
),
|
|
16
19
|
}));
|
|
17
20
|
|
|
18
21
|
vi.mock("@agent-native/core", () => ({
|
|
@@ -33,9 +36,17 @@ vi.mock("@agent-native/core/resources/store", () => ({
|
|
|
33
36
|
}));
|
|
34
37
|
|
|
35
38
|
vi.mock("@agent-native/core/server", () => ({
|
|
39
|
+
getRequestOrgId: () => "org-1",
|
|
36
40
|
getRequestUserEmail: () => mocks.getRequestUserEmail(),
|
|
37
41
|
}));
|
|
38
42
|
|
|
43
|
+
vi.mock("@agent-native/core/settings", () => ({
|
|
44
|
+
getOrgSetting: (...args: unknown[]) => mocks.getOrgSetting(...args),
|
|
45
|
+
getUserSetting: vi.fn(),
|
|
46
|
+
putOrgSetting: vi.fn(),
|
|
47
|
+
putUserSetting: vi.fn(),
|
|
48
|
+
}));
|
|
49
|
+
|
|
39
50
|
vi.mock("@agent-native/core/server/agent-discovery", () => ({
|
|
40
51
|
discoverAgents: (...args: unknown[]) => mocks.discoverAgents(...args),
|
|
41
52
|
getBuiltinAgents: (...args: unknown[]) => mocks.getBuiltinAgents(...args),
|
|
@@ -83,6 +94,62 @@ describe("list-connected-agents", () => {
|
|
|
83
94
|
});
|
|
84
95
|
});
|
|
85
96
|
|
|
97
|
+
it("marks hosted apps hidden from launchers when the organization hides them", async () => {
|
|
98
|
+
const clips = {
|
|
99
|
+
id: "clips",
|
|
100
|
+
name: "Clips",
|
|
101
|
+
description: "",
|
|
102
|
+
url: "https://clips.agent-native.com",
|
|
103
|
+
color: "#000000",
|
|
104
|
+
};
|
|
105
|
+
const partner = { ...clips, id: "partner", url: "https://p.example.test" };
|
|
106
|
+
mocks.getBuiltinAgents.mockReturnValue([clips]);
|
|
107
|
+
mocks.discoverAgents.mockResolvedValue([clips, partner]);
|
|
108
|
+
mocks.resourceListAccessible.mockImplementation(async () => []);
|
|
109
|
+
mocks.getOrgSetting.mockResolvedValueOnce({ showHostedApps: false });
|
|
110
|
+
|
|
111
|
+
const { default: action } = await import("./list-connected-agents.js");
|
|
112
|
+
const agents = await action.run({});
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
agents.map((agent: { id: string; hiddenFromLaunchers?: boolean }) => [
|
|
116
|
+
agent.id,
|
|
117
|
+
agent.hiddenFromLaunchers === true,
|
|
118
|
+
]),
|
|
119
|
+
).toEqual([
|
|
120
|
+
["clips", true],
|
|
121
|
+
["partner", false],
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("hides hosted apps instead of failing the list when the stored setting is invalid", async () => {
|
|
126
|
+
const clips = {
|
|
127
|
+
id: "clips",
|
|
128
|
+
name: "Clips",
|
|
129
|
+
description: "",
|
|
130
|
+
url: "https://clips.agent-native.com",
|
|
131
|
+
color: "#000000",
|
|
132
|
+
};
|
|
133
|
+
const partner = { ...clips, id: "partner", url: "https://p.example.test" };
|
|
134
|
+
mocks.getBuiltinAgents.mockReturnValue([clips]);
|
|
135
|
+
mocks.discoverAgents.mockResolvedValue([clips, partner]);
|
|
136
|
+
mocks.resourceListAccessible.mockImplementation(async () => []);
|
|
137
|
+
mocks.getOrgSetting.mockResolvedValueOnce({ showHostedApps: "no" });
|
|
138
|
+
|
|
139
|
+
const { default: action } = await import("./list-connected-agents.js");
|
|
140
|
+
const agents = await action.run({});
|
|
141
|
+
|
|
142
|
+
expect(
|
|
143
|
+
agents.map((agent: { id: string; hiddenFromLaunchers?: boolean }) => [
|
|
144
|
+
agent.id,
|
|
145
|
+
agent.hiddenFromLaunchers === true,
|
|
146
|
+
]),
|
|
147
|
+
).toEqual([
|
|
148
|
+
["clips", true],
|
|
149
|
+
["partner", false],
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
|
|
86
153
|
it("does not surface seeded manifests for built-ins that are not enabled as custom agents", async () => {
|
|
87
154
|
mocks.getBuiltinAgents.mockReturnValue([]);
|
|
88
155
|
mocks.discoverAgents.mockResolvedValue([]);
|
|
@@ -11,38 +11,34 @@ import {
|
|
|
11
11
|
import { getRequestUserEmail } from "@agent-native/core/server";
|
|
12
12
|
import {
|
|
13
13
|
discoverAgents,
|
|
14
|
-
getBuiltinAgents,
|
|
15
14
|
isBuiltinAgentCatalogId,
|
|
16
|
-
loadWorkspaceAppsManifest,
|
|
17
15
|
normalizeAgentId,
|
|
18
16
|
shouldIncludeRemoteAgentManifest,
|
|
19
17
|
} from "@agent-native/core/server/agent-discovery";
|
|
20
18
|
import { z } from "zod";
|
|
21
19
|
|
|
20
|
+
import {
|
|
21
|
+
readHostedAppsSettings,
|
|
22
|
+
listHostedApps,
|
|
23
|
+
} from "../server/lib/hosted-apps-settings.js";
|
|
24
|
+
|
|
22
25
|
export default defineAction({
|
|
23
26
|
description:
|
|
24
|
-
"List agents available to dispatch for A2A delegation, including built-in apps and connected remote agents.",
|
|
27
|
+
"List agents available to dispatch for A2A delegation, including built-in apps and connected remote agents. When get-hosted-apps-settings reports showHostedApps false, built-in (hosted) apps are still listed but marked hiddenFromLaunchers: true.",
|
|
25
28
|
schema: z.object({}),
|
|
26
29
|
http: { method: "GET" },
|
|
27
30
|
run: async () => {
|
|
28
31
|
const { hiddenAgentIds = [] } = await import("../server/index.js").then(
|
|
29
32
|
(m) => m.getDispatchConfig(),
|
|
30
33
|
);
|
|
31
|
-
const [discovered,
|
|
34
|
+
const [discovered, hosted, hostedAppsSettings] = await Promise.all([
|
|
32
35
|
discoverAgents("dispatch"),
|
|
33
|
-
|
|
36
|
+
listHostedApps(),
|
|
37
|
+
readHostedAppsSettings(),
|
|
34
38
|
]);
|
|
35
|
-
|
|
36
|
-
// built-in config never applies to it.
|
|
37
|
-
const workspaceIds = new Set(
|
|
38
|
-
(workspaceApps ?? []).map((app) => normalizeAgentId(app.id)),
|
|
39
|
-
);
|
|
40
|
-
const builtins = getBuiltinAgents("dispatch");
|
|
41
|
-
const builtinIds = new Set(
|
|
42
|
-
builtins.map((agent) => agent.id).filter((id) => !workspaceIds.has(id)),
|
|
43
|
-
);
|
|
39
|
+
const builtinIds = new Set(hosted.map((agent) => agent.id));
|
|
44
40
|
const builtinHomeUrls = new Map(
|
|
45
|
-
|
|
41
|
+
hosted.map((agent) => [agent.id, agent.url]),
|
|
46
42
|
);
|
|
47
43
|
const ownerEmail = getRequestUserEmail();
|
|
48
44
|
if (!ownerEmail) {
|
|
@@ -97,6 +93,9 @@ export default defineAction({
|
|
|
97
93
|
return {
|
|
98
94
|
...agent,
|
|
99
95
|
...(homeUrl ? { homeUrl } : {}),
|
|
96
|
+
...(isBuiltin && !hostedAppsSettings.showHostedApps
|
|
97
|
+
? { hiddenFromLaunchers: true }
|
|
98
|
+
: {}),
|
|
100
99
|
source: isBuiltin ? "builtin" : custom ? "custom" : "workspace",
|
|
101
100
|
resourceId: custom?.resourceId,
|
|
102
101
|
path: custom?.path,
|
|
@@ -2,6 +2,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
const mocks = vi.hoisted(() => ({
|
|
4
4
|
listGrantedApps: vi.fn(),
|
|
5
|
+
hideHostedAppsWhenOff: vi.fn(async <T>(apps: T[]) => apps),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("../server/lib/hosted-apps-settings.js", () => ({
|
|
9
|
+
hideHostedAppsWhenOff: mocks.hideHostedAppsWhenOff,
|
|
5
10
|
}));
|
|
6
11
|
|
|
7
12
|
vi.mock("../server/lib/mcp-gateway.js", () => ({
|
|
@@ -12,6 +17,7 @@ import listApps from "./list_apps.js";
|
|
|
12
17
|
|
|
13
18
|
beforeEach(() => {
|
|
14
19
|
vi.clearAllMocks();
|
|
20
|
+
mocks.hideHostedAppsWhenOff.mockImplementation(async (apps) => apps);
|
|
15
21
|
mocks.listGrantedApps.mockResolvedValue([
|
|
16
22
|
{
|
|
17
23
|
id: "slides",
|
|
@@ -29,6 +35,16 @@ beforeEach(() => {
|
|
|
29
35
|
});
|
|
30
36
|
|
|
31
37
|
describe("list_apps", () => {
|
|
38
|
+
it("leaves out apps the hosted apps setting hides", async () => {
|
|
39
|
+
mocks.hideHostedAppsWhenOff.mockImplementation(async (apps) =>
|
|
40
|
+
apps.filter((app: { id: string }) => app.id !== "analytics"),
|
|
41
|
+
);
|
|
42
|
+
const result = await listApps.run({});
|
|
43
|
+
expect(result.apps.map((app: { id: string }) => app.id)).toEqual([
|
|
44
|
+
"slides",
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
32
48
|
it("returns a compact valid JSON message for MCP hosts", async () => {
|
|
33
49
|
const result = await listApps.run({});
|
|
34
50
|
|
package/src/actions/list_apps.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core/action";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
|
+
import { hideHostedAppsWhenOff } from "../server/lib/hosted-apps-settings.js";
|
|
4
5
|
import { listGrantedDispatchMcpApps } from "../server/lib/mcp-gateway.js";
|
|
5
6
|
|
|
6
7
|
export default defineAction({
|
|
7
8
|
description:
|
|
8
|
-
'List the apps this Dispatch MCP gateway can route to, including "dispatch" itself for Dispatch-owned pages such as extensions. The result is filtered by Dispatch\'s MCP app access policy.',
|
|
9
|
+
'List the apps this Dispatch MCP gateway can route to, including "dispatch" itself for Dispatch-owned pages such as extensions. The result is filtered by Dispatch\'s MCP app access policy, and leaves out Agent-Native.com hosted apps while the organization hides them.',
|
|
9
10
|
schema: z.object({}),
|
|
10
11
|
http: { method: "GET" },
|
|
11
12
|
readOnly: true,
|
|
12
13
|
parallelSafe: true,
|
|
13
14
|
run: async () => {
|
|
14
|
-
const apps = await
|
|
15
|
+
const apps = await hideHostedAppsWhenOff(
|
|
16
|
+
await listGrantedDispatchMcpApps(),
|
|
17
|
+
);
|
|
15
18
|
const appSummaries = apps.map((app) => ({
|
|
16
19
|
id: app.id,
|
|
17
20
|
name: app.name,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { authorizeOrgOwnerOrAdmin } from "../server/lib/app-roles.js";
|
|
5
|
+
import { recordAudit } from "../server/lib/dispatch-store.js";
|
|
6
|
+
import { hostedAppsSettingsViewSchema } from "../server/lib/hosted-apps-settings-schema.js";
|
|
7
|
+
import {
|
|
8
|
+
HOSTED_APPS_SETTINGS_KEY,
|
|
9
|
+
setHostedAppsSettings,
|
|
10
|
+
} from "../server/lib/hosted-apps-settings.js";
|
|
11
|
+
|
|
12
|
+
export default defineAction({
|
|
13
|
+
mcpAnnotations: {
|
|
14
|
+
readOnlyHint: false,
|
|
15
|
+
destructiveHint: false,
|
|
16
|
+
openWorldHint: false,
|
|
17
|
+
},
|
|
18
|
+
description:
|
|
19
|
+
"Show or hide Agent-Native.com hosted apps (built-in apps) in this organization's Dispatch app lists and agent context. Organization owners and admins only. Pins are kept while hidden.",
|
|
20
|
+
authorize: authorizeOrgOwnerOrAdmin,
|
|
21
|
+
schema: z.object({
|
|
22
|
+
showHostedApps: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.describe(
|
|
25
|
+
"true shows Agent-Native.com hosted apps; false hides them for everyone.",
|
|
26
|
+
),
|
|
27
|
+
}),
|
|
28
|
+
outputSchema: hostedAppsSettingsViewSchema,
|
|
29
|
+
run: async ({ showHostedApps }) => {
|
|
30
|
+
const saved = await setHostedAppsSettings({ showHostedApps });
|
|
31
|
+
await recordAudit({
|
|
32
|
+
action: "hosted-apps.updated",
|
|
33
|
+
targetType: "dispatch-settings",
|
|
34
|
+
targetId: HOSTED_APPS_SETTINGS_KEY,
|
|
35
|
+
summary: showHostedApps
|
|
36
|
+
? "Showed Agent-Native.com hosted apps"
|
|
37
|
+
: "Hid Agent-Native.com hosted apps",
|
|
38
|
+
metadata: saved,
|
|
39
|
+
});
|
|
40
|
+
return saved;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ConnectedAppSummary } from "../lib/other-apps";
|
|
4
|
+
import {
|
|
5
|
+
tierWorkspaceApps,
|
|
6
|
+
useWorkspaceAppLayout,
|
|
7
|
+
} from "../lib/workspace-app-layout";
|
|
8
|
+
import type { WorkspaceAppSummary } from "../lib/workspace-apps";
|
|
9
|
+
import { AppList } from "./app-list-row";
|
|
10
|
+
import { AppTierSections } from "./app-tier-sections";
|
|
11
|
+
import { ConnectedAppCard } from "./connected-app-card";
|
|
12
|
+
import { WorkspaceAppCard } from "./workspace-app-card";
|
|
13
|
+
|
|
14
|
+
type AppCardEntry =
|
|
15
|
+
| { id: string; name: string; source?: undefined; app: WorkspaceAppSummary }
|
|
16
|
+
| {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
source: "builtin";
|
|
20
|
+
hosted: ConnectedAppSummary;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Home and All apps: workspace app cards and hosted app cards, tiered. */
|
|
24
|
+
export function AppCardTiers({
|
|
25
|
+
workspaceApps,
|
|
26
|
+
hostedApps,
|
|
27
|
+
appsTitle,
|
|
28
|
+
actions,
|
|
29
|
+
hasPinnedApps,
|
|
30
|
+
appsFallback,
|
|
31
|
+
}: {
|
|
32
|
+
workspaceApps: readonly WorkspaceAppSummary[];
|
|
33
|
+
hostedApps: readonly ConnectedAppSummary[];
|
|
34
|
+
appsTitle: string;
|
|
35
|
+
actions?: ReactNode;
|
|
36
|
+
hasPinnedApps?: boolean;
|
|
37
|
+
appsFallback?: ReactNode;
|
|
38
|
+
}) {
|
|
39
|
+
const { layout, togglePinned } = useWorkspaceAppLayout();
|
|
40
|
+
const tiers = tierWorkspaceApps<AppCardEntry>(
|
|
41
|
+
[
|
|
42
|
+
...workspaceApps.map((app) => ({ id: app.id, name: app.name, app })),
|
|
43
|
+
...hostedApps.map((hosted) => ({
|
|
44
|
+
id: hosted.id,
|
|
45
|
+
name: hosted.name,
|
|
46
|
+
source: "builtin" as const,
|
|
47
|
+
hosted,
|
|
48
|
+
})),
|
|
49
|
+
],
|
|
50
|
+
layout,
|
|
51
|
+
);
|
|
52
|
+
const isPinned = (id: string) =>
|
|
53
|
+
layout.pinnedIds.includes(id.trim().toLowerCase());
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<AppTierSections
|
|
57
|
+
tiers={tiers}
|
|
58
|
+
appsTitle={appsTitle}
|
|
59
|
+
actions={actions}
|
|
60
|
+
hasPinnedApps={hasPinnedApps}
|
|
61
|
+
appsFallback={appsFallback}
|
|
62
|
+
renderItems={(entries) => (
|
|
63
|
+
<AppList layout="grid">
|
|
64
|
+
{entries.map((entry) =>
|
|
65
|
+
"app" in entry ? (
|
|
66
|
+
<WorkspaceAppCard
|
|
67
|
+
key={entry.id}
|
|
68
|
+
app={entry.app}
|
|
69
|
+
isPinned={isPinned(entry.id)}
|
|
70
|
+
onTogglePinned={() => togglePinned(entry.id)}
|
|
71
|
+
/>
|
|
72
|
+
) : (
|
|
73
|
+
<ConnectedAppCard
|
|
74
|
+
key={entry.id}
|
|
75
|
+
app={entry.hosted}
|
|
76
|
+
isPinned={isPinned(entry.id)}
|
|
77
|
+
onTogglePinned={() => togglePinned(entry.id)}
|
|
78
|
+
/>
|
|
79
|
+
),
|
|
80
|
+
)}
|
|
81
|
+
</AppList>
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
// oracle-change(packages/dispatch/src/components/other-apps-section.spec.ts): OtherAppsSection had no importers and was deleted with its spec.
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { AppList, AppListRow } from "./app-list-row";
|
|
8
|
+
|
|
9
|
+
describe("AppList", () => {
|
|
10
|
+
let container: HTMLDivElement;
|
|
11
|
+
let root: Root;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
15
|
+
container = document.createElement("div");
|
|
16
|
+
document.body.appendChild(container);
|
|
17
|
+
root = createRoot(container);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
act(() => root.unmount());
|
|
22
|
+
container.remove();
|
|
23
|
+
vi.unstubAllGlobals();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders grid rows as muted shadcn Items and list rows as divided rows", async () => {
|
|
27
|
+
await act(async () =>
|
|
28
|
+
root.render(
|
|
29
|
+
<>
|
|
30
|
+
<AppList layout="grid">
|
|
31
|
+
<AppListRow>grid app</AppListRow>
|
|
32
|
+
</AppList>
|
|
33
|
+
<AppList>
|
|
34
|
+
<AppListRow>list app</AppListRow>
|
|
35
|
+
</AppList>
|
|
36
|
+
</>,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const rows = [...container.querySelectorAll("div")].filter((row) =>
|
|
41
|
+
/^(grid|list) app$/.test(row.textContent ?? ""),
|
|
42
|
+
);
|
|
43
|
+
const grid = rows.find(
|
|
44
|
+
(row) => row.textContent === "grid app" && row.dataset.slot,
|
|
45
|
+
);
|
|
46
|
+
const list = rows.filter((row) => row.textContent === "list app").at(-1);
|
|
47
|
+
expect(grid?.dataset.slot).toBe("item");
|
|
48
|
+
expect(grid?.dataset.variant).toBe("muted");
|
|
49
|
+
// Tabbing into a row marks it; Item only styles its own focus.
|
|
50
|
+
expect(grid?.className).toContain("focus-within:border-ring/50");
|
|
51
|
+
expect(list?.dataset.slot).toBeUndefined();
|
|
52
|
+
expect(list?.className).toContain("border-b");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -1,23 +1,61 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Item } from "@agent-native/toolkit/ui/item";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
type ComponentProps,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
2
8
|
|
|
3
9
|
import { cn } from "../lib/utils";
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
"w-full xl:grid xl:grid-cols-2 xl:gap-3 xl:overflow-visible xl:rounded-none";
|
|
7
|
-
export const APP_LIST_GRID_ROW_CLASS =
|
|
8
|
-
"xl:rounded-2xl xl:border xl:border-transparent xl:transition-[border-color] xl:focus-within:border-ring/50";
|
|
11
|
+
type AppListLayout = "list" | "grid";
|
|
9
12
|
|
|
13
|
+
const AppListLayoutContext = createContext<AppListLayout>("list");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `grid` is the two-column app list on Home, New chat, All apps and the
|
|
17
|
+
* catalogs; its rows are shadcn `Item`s. `list` is a bordered, divided list.
|
|
18
|
+
*/
|
|
10
19
|
export function AppList({
|
|
11
20
|
children,
|
|
12
21
|
className,
|
|
22
|
+
layout = "list",
|
|
13
23
|
}: {
|
|
14
24
|
children: ReactNode;
|
|
15
25
|
className?: string;
|
|
26
|
+
layout?: AppListLayout;
|
|
16
27
|
}) {
|
|
17
28
|
return (
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
<AppListLayoutContext.Provider value={layout}>
|
|
30
|
+
<div
|
|
31
|
+
className={cn(
|
|
32
|
+
layout === "grid"
|
|
33
|
+
? "grid w-full grid-cols-1 gap-2 sm:grid-cols-2"
|
|
34
|
+
: "overflow-hidden rounded-2xl",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
</AppListLayoutContext.Provider>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One app in a grid app list. */
|
|
45
|
+
export function AppGridItem({
|
|
46
|
+
className,
|
|
47
|
+
...props
|
|
48
|
+
}: ComponentProps<typeof Item>) {
|
|
49
|
+
return (
|
|
50
|
+
<Item
|
|
51
|
+
variant="muted"
|
|
52
|
+
size="sm"
|
|
53
|
+
className={cn(
|
|
54
|
+
"group min-w-0 flex-nowrap focus-within:border-ring/50",
|
|
55
|
+
className,
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
21
59
|
);
|
|
22
60
|
}
|
|
23
61
|
|
|
@@ -28,6 +66,10 @@ export function AppListRow({
|
|
|
28
66
|
children: ReactNode;
|
|
29
67
|
className?: string;
|
|
30
68
|
}) {
|
|
69
|
+
const layout = useContext(AppListLayoutContext);
|
|
70
|
+
if (layout === "grid") {
|
|
71
|
+
return <AppGridItem className={className}>{children}</AppGridItem>;
|
|
72
|
+
}
|
|
31
73
|
return (
|
|
32
74
|
<div
|
|
33
75
|
className={cn(
|
|
@@ -1,5 +1,21 @@
|
|
|
1
|
+
import type { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import type { AppOpenActionLabels } from "@agent-native/toolkit/app/chat/chat-first/app-open-actions";
|
|
3
|
+
|
|
1
4
|
export {
|
|
2
5
|
AppOpenActions,
|
|
3
6
|
type AppOpenActionLabels,
|
|
4
7
|
type AppOpenActionMenuItem,
|
|
5
8
|
} from "@agent-native/toolkit/app/chat/chat-first/app-open-actions";
|
|
9
|
+
|
|
10
|
+
/** Dispatch's localized labels for an app row's Open actions. */
|
|
11
|
+
export function appOpenActionLabels(
|
|
12
|
+
t: ReturnType<typeof useT>,
|
|
13
|
+
name: string,
|
|
14
|
+
): Partial<AppOpenActionLabels> {
|
|
15
|
+
return {
|
|
16
|
+
openApp: t("dispatch.pages.openApp", { defaultValue: "Open" }),
|
|
17
|
+
openAppAccessible: t("dispatch.pages.chatFirstOpenApp", { name }),
|
|
18
|
+
openInNewTab: t("dispatch.pages.chatFirstOpenInNewTab"),
|
|
19
|
+
moreOptions: t("extensions.optionsFor", { name }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act, useState } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
7
|
+
useT: () => (key: string) => key,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { AppPinButton } from "./app-pin-button";
|
|
11
|
+
import { TooltipProvider } from "./ui/tooltip";
|
|
12
|
+
|
|
13
|
+
function MovingRow() {
|
|
14
|
+
const [pinned, setPinned] = useState(false);
|
|
15
|
+
const button = (
|
|
16
|
+
<AppPinButton
|
|
17
|
+
appId="mail"
|
|
18
|
+
name="Mail"
|
|
19
|
+
pinned={pinned}
|
|
20
|
+
onToggle={() => setPinned((value) => !value)}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
return (
|
|
24
|
+
<TooltipProvider>
|
|
25
|
+
<section data-section="pinned">{pinned ? button : null}</section>
|
|
26
|
+
<section data-section="apps">{pinned ? null : button}</section>
|
|
27
|
+
</TooltipProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("AppPinButton", () => {
|
|
32
|
+
let container: HTMLDivElement;
|
|
33
|
+
let root: Root;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
37
|
+
container = document.createElement("div");
|
|
38
|
+
document.body.appendChild(container);
|
|
39
|
+
root = createRoot(container);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
act(() => root.unmount());
|
|
44
|
+
container.remove();
|
|
45
|
+
vi.unstubAllGlobals();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("keeps focus on the app's pin button after pinning moves its row", async () => {
|
|
49
|
+
await act(async () => root.render(<MovingRow />));
|
|
50
|
+
const before = container.querySelector<HTMLButtonElement>(
|
|
51
|
+
'[data-pin-app-id="mail"]',
|
|
52
|
+
)!;
|
|
53
|
+
before.focus();
|
|
54
|
+
|
|
55
|
+
await act(async () => before.click());
|
|
56
|
+
await act(
|
|
57
|
+
() =>
|
|
58
|
+
new Promise<void>((resolve) => requestAnimationFrame(() => resolve())),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const after = container.querySelector<HTMLButtonElement>(
|
|
62
|
+
'[data-section="pinned"] [data-pin-app-id="mail"]',
|
|
63
|
+
);
|
|
64
|
+
expect(after).not.toBeNull();
|
|
65
|
+
expect(after).not.toBe(before);
|
|
66
|
+
expect(document.activeElement).toBe(after);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { IconPin, IconPinFilled } from "@tabler/icons-react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { Button } from "./ui/button";
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
|
7
|
+
|
|
8
|
+
/** Shown on row hover or focus, always when pinned or on touch screens. */
|
|
9
|
+
export function AppPinButton({
|
|
10
|
+
appId,
|
|
11
|
+
name,
|
|
12
|
+
pinned,
|
|
13
|
+
onToggle,
|
|
14
|
+
className,
|
|
15
|
+
}: {
|
|
16
|
+
appId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
pinned: boolean;
|
|
19
|
+
onToggle: () => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}) {
|
|
22
|
+
const t = useT();
|
|
23
|
+
const label = t(
|
|
24
|
+
pinned ? "dispatch.pages.unpinApp" : "dispatch.pages.pinApp",
|
|
25
|
+
{
|
|
26
|
+
name,
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
const Icon = pinned ? IconPinFilled : IconPin;
|
|
30
|
+
const toggle = () => {
|
|
31
|
+
onToggle();
|
|
32
|
+
// Pinning moves the row to another section, which remounts it; keep
|
|
33
|
+
// keyboard focus on this app's pin button rather than dropping to <body>.
|
|
34
|
+
requestAnimationFrame(() => {
|
|
35
|
+
if (document.activeElement && document.activeElement !== document.body) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
document
|
|
39
|
+
.querySelector<HTMLElement>(`[data-pin-app-id="${CSS.escape(appId)}"]`)
|
|
40
|
+
?.focus();
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
return (
|
|
44
|
+
<Tooltip>
|
|
45
|
+
<TooltipTrigger asChild>
|
|
46
|
+
<Button
|
|
47
|
+
type="button"
|
|
48
|
+
variant="ghost"
|
|
49
|
+
size="sm"
|
|
50
|
+
aria-label={label}
|
|
51
|
+
aria-pressed={pinned}
|
|
52
|
+
data-pinned={pinned}
|
|
53
|
+
data-pin-app-id={appId}
|
|
54
|
+
onClick={toggle}
|
|
55
|
+
className={cn(
|
|
56
|
+
"size-7 shrink-0 p-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 data-[pinned=true]:text-foreground data-[pinned=true]:opacity-100 [@media(pointer:coarse)]:opacity-100",
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
<Icon size={15} aria-hidden="true" />
|
|
61
|
+
</Button>
|
|
62
|
+
</TooltipTrigger>
|
|
63
|
+
<TooltipContent>{label}</TooltipContent>
|
|
64
|
+
</Tooltip>
|
|
65
|
+
);
|
|
66
|
+
}
|