@grackle-ai/web-components 0.107.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
- package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
- package/.rush/temp/operation/_phase_build/all.log +18 -0
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
- package/.rush/temp/operation/_phase_build/state.json +3 -0
- package/.rush/temp/operation/_phase_test/all.log +121 -0
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
- package/.rush/temp/operation/_phase_test/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +938 -0
- package/.storybook/main.ts +22 -0
- package/.storybook/preview.tsx +30 -0
- package/config/rig.json +4 -0
- package/config/rush-project.json +12 -0
- package/dist/index.css +1 -0
- package/dist/index.js +39221 -0
- package/eslint.config.cjs +5 -0
- package/package.json +83 -0
- package/rush-logs/web-components._phase_build.cache.log +4 -0
- package/rush-logs/web-components._phase_test.cache.log +4 -0
- package/src/components/chat/ChatInput.module.scss +81 -0
- package/src/components/chat/ChatInput.stories.tsx +91 -0
- package/src/components/chat/ChatInput.tsx +168 -0
- package/src/components/chat/index.ts +6 -0
- package/src/components/dag/DagView.module.scss +149 -0
- package/src/components/dag/DagView.stories.tsx +125 -0
- package/src/components/dag/DagView.tsx +109 -0
- package/src/components/dag/TaskNode.stories.tsx +133 -0
- package/src/components/dag/TaskNode.tsx +40 -0
- package/src/components/dag/useDagLayout.ts +139 -0
- package/src/components/display/Breadcrumbs.module.scss +71 -0
- package/src/components/display/Breadcrumbs.stories.tsx +80 -0
- package/src/components/display/Breadcrumbs.tsx +46 -0
- package/src/components/display/Button.module.scss +110 -0
- package/src/components/display/Button.stories.tsx +88 -0
- package/src/components/display/Button.tsx +40 -0
- package/src/components/display/ConfirmDialog.module.scss +67 -0
- package/src/components/display/ConfirmDialog.stories.tsx +81 -0
- package/src/components/display/ConfirmDialog.tsx +88 -0
- package/src/components/display/CopyButton.module.scss +41 -0
- package/src/components/display/CopyButton.stories.tsx +78 -0
- package/src/components/display/CopyButton.tsx +64 -0
- package/src/components/display/DemoBanner.module.scss +37 -0
- package/src/components/display/DemoBanner.stories.tsx +40 -0
- package/src/components/display/DemoBanner.tsx +23 -0
- package/src/components/display/EventHoverRow.module.scss +102 -0
- package/src/components/display/EventHoverRow.stories.tsx +99 -0
- package/src/components/display/EventHoverRow.tsx +154 -0
- package/src/components/display/EventRenderer.module.scss +272 -0
- package/src/components/display/EventRenderer.stories.tsx +186 -0
- package/src/components/display/EventRenderer.tsx +271 -0
- package/src/components/display/EventStream.module.scss +93 -0
- package/src/components/display/EventStream.stories.tsx +249 -0
- package/src/components/display/EventStream.tsx +369 -0
- package/src/components/display/FloatingActionBar.module.scss +107 -0
- package/src/components/display/FloatingActionBar.stories.tsx +122 -0
- package/src/components/display/FloatingActionBar.tsx +119 -0
- package/src/components/display/SessionAttemptSelector.module.scss +50 -0
- package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
- package/src/components/display/SessionAttemptSelector.tsx +49 -0
- package/src/components/display/SessionPicker.module.scss +200 -0
- package/src/components/display/SessionPicker.stories.tsx +169 -0
- package/src/components/display/SessionPicker.tsx +214 -0
- package/src/components/display/Skeleton.module.scss +58 -0
- package/src/components/display/Skeleton.stories.tsx +94 -0
- package/src/components/display/Skeleton.tsx +127 -0
- package/src/components/display/Spinner.module.scss +41 -0
- package/src/components/display/Spinner.stories.tsx +66 -0
- package/src/components/display/Spinner.tsx +32 -0
- package/src/components/display/SplashScreen.module.scss +20 -0
- package/src/components/display/SplashScreen.stories.tsx +26 -0
- package/src/components/display/SplashScreen.tsx +16 -0
- package/src/components/display/SplitButton.module.scss +166 -0
- package/src/components/display/SplitButton.stories.tsx +95 -0
- package/src/components/display/SplitButton.tsx +128 -0
- package/src/components/display/Tooltip.module.scss +84 -0
- package/src/components/display/Tooltip.stories.tsx +240 -0
- package/src/components/display/Tooltip.tsx +184 -0
- package/src/components/display/extractText.test.tsx +48 -0
- package/src/components/display/index.ts +20 -0
- package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
- package/src/components/editable/EditableCheckbox.tsx +39 -0
- package/src/components/editable/EditableField.module.scss +135 -0
- package/src/components/editable/EditableSelect.tsx +164 -0
- package/src/components/editable/EditableTextArea.stories.tsx +50 -0
- package/src/components/editable/EditableTextArea.tsx +148 -0
- package/src/components/editable/EditableTextField.stories.tsx +62 -0
- package/src/components/editable/EditableTextField.tsx +153 -0
- package/src/components/editable/EnvironmentSelect.module.scss +17 -0
- package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
- package/src/components/editable/EnvironmentSelect.tsx +87 -0
- package/src/components/editable/index.ts +13 -0
- package/src/components/editable/useEditableField.test.tsx +233 -0
- package/src/components/editable/useEditableField.ts +173 -0
- package/src/components/index.ts +20 -0
- package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
- package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
- package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
- package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
- package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
- package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
- package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
- package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
- package/src/components/knowledge/KnowledgeNav.tsx +138 -0
- package/src/components/knowledge/index.ts +3 -0
- package/src/components/layout/AppNav.module.scss +82 -0
- package/src/components/layout/AppNav.stories.tsx +115 -0
- package/src/components/layout/AppNav.tsx +133 -0
- package/src/components/layout/BottomStatusBar.module.scss +58 -0
- package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
- package/src/components/layout/BottomStatusBar.tsx +206 -0
- package/src/components/layout/Sidebar.module.scss +60 -0
- package/src/components/layout/Sidebar.stories.tsx +46 -0
- package/src/components/layout/Sidebar.tsx +84 -0
- package/src/components/layout/StatusBar.module.scss +108 -0
- package/src/components/layout/StatusBar.stories.tsx +119 -0
- package/src/components/layout/StatusBar.tsx +70 -0
- package/src/components/layout/index.ts +9 -0
- package/src/components/lists/EnvironmentNav.module.scss +118 -0
- package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
- package/src/components/lists/EnvironmentNav.tsx +133 -0
- package/src/components/lists/FindingsNav.module.scss +126 -0
- package/src/components/lists/FindingsNav.tsx +146 -0
- package/src/components/lists/TaskList.module.scss +206 -0
- package/src/components/lists/TaskList.stories.tsx +401 -0
- package/src/components/lists/TaskList.tsx +509 -0
- package/src/components/lists/index.ts +6 -0
- package/src/components/lists/listHelpers.tsx +130 -0
- package/src/components/notifications/Callout.module.scss +83 -0
- package/src/components/notifications/Callout.stories.tsx +81 -0
- package/src/components/notifications/Callout.tsx +64 -0
- package/src/components/notifications/Toast.module.scss +86 -0
- package/src/components/notifications/Toast.stories.tsx +71 -0
- package/src/components/notifications/Toast.tsx +51 -0
- package/src/components/notifications/ToastContainer.module.scss +23 -0
- package/src/components/notifications/ToastContainer.stories.tsx +66 -0
- package/src/components/notifications/ToastContainer.tsx +29 -0
- package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
- package/src/components/notifications/UpdateBanner.test.tsx +64 -0
- package/src/components/notifications/UpdateBanner.tsx +44 -0
- package/src/components/notifications/index.ts +8 -0
- package/src/components/panels/AboutPanel.stories.tsx +70 -0
- package/src/components/panels/AboutPanel.tsx +66 -0
- package/src/components/panels/AppearancePanel.stories.tsx +45 -0
- package/src/components/panels/AppearancePanel.tsx +97 -0
- package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
- package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
- package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
- package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
- package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
- package/src/components/panels/FindingsPanel.module.scss +94 -0
- package/src/components/panels/FindingsPanel.stories.tsx +109 -0
- package/src/components/panels/FindingsPanel.tsx +76 -0
- package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
- package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
- package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
- package/src/components/panels/PluginsPanel.tsx +77 -0
- package/src/components/panels/SettingsPanel.module.scss +336 -0
- package/src/components/panels/TaskActionButtons.module.scss +22 -0
- package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
- package/src/components/panels/TaskActionButtons.tsx +87 -0
- package/src/components/panels/TaskEditPanel.module.scss +202 -0
- package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
- package/src/components/panels/TaskEditPanel.tsx +328 -0
- package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
- package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
- package/src/components/panels/TaskOverviewPanel.tsx +270 -0
- package/src/components/panels/TokensPanel.stories.tsx +131 -0
- package/src/components/panels/TokensPanel.tsx +143 -0
- package/src/components/panels/WorkpadPanel.module.scss +39 -0
- package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
- package/src/components/panels/WorkpadPanel.tsx +63 -0
- package/src/components/panels/index.ts +13 -0
- package/src/components/personas/McpToolSelector.module.scss +109 -0
- package/src/components/personas/McpToolSelector.stories.tsx +129 -0
- package/src/components/personas/McpToolSelector.tsx +180 -0
- package/src/components/personas/PersonaManager.module.scss +233 -0
- package/src/components/personas/PersonaManager.stories.tsx +139 -0
- package/src/components/personas/PersonaManager.tsx +122 -0
- package/src/components/schedules/ScheduleManager.module.scss +98 -0
- package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
- package/src/components/schedules/ScheduleManager.tsx +160 -0
- package/src/components/settings/SettingsNav.module.scss +82 -0
- package/src/components/settings/SettingsNav.stories.tsx +83 -0
- package/src/components/settings/SettingsNav.tsx +104 -0
- package/src/components/streams/StreamDetailPanel.module.scss +206 -0
- package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
- package/src/components/streams/StreamDetailPanel.tsx +119 -0
- package/src/components/streams/StreamList.module.scss +92 -0
- package/src/components/streams/StreamList.stories.tsx +99 -0
- package/src/components/streams/StreamList.tsx +114 -0
- package/src/components/streams/index.ts +10 -0
- package/src/components/tools/AgentToolCard.module.scss +118 -0
- package/src/components/tools/AgentToolCard.stories.tsx +304 -0
- package/src/components/tools/AgentToolCard.tsx +247 -0
- package/src/components/tools/FileEditCard.stories.tsx +138 -0
- package/src/components/tools/FileEditCard.tsx +160 -0
- package/src/components/tools/FileReadCard.stories.tsx +120 -0
- package/src/components/tools/FileReadCard.tsx +106 -0
- package/src/components/tools/FindingCard.stories.tsx +124 -0
- package/src/components/tools/FindingCard.tsx +178 -0
- package/src/components/tools/GenericToolCard.stories.tsx +80 -0
- package/src/components/tools/GenericToolCard.tsx +111 -0
- package/src/components/tools/IpcCard.stories.tsx +129 -0
- package/src/components/tools/IpcCard.tsx +178 -0
- package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
- package/src/components/tools/KnowledgeCard.tsx +165 -0
- package/src/components/tools/MetadataCard.stories.tsx +32 -0
- package/src/components/tools/MetadataCard.tsx +39 -0
- package/src/components/tools/SearchCard.stories.tsx +74 -0
- package/src/components/tools/SearchCard.tsx +86 -0
- package/src/components/tools/ShellCard.stories.tsx +112 -0
- package/src/components/tools/ShellCard.tsx +106 -0
- package/src/components/tools/TaskCard.stories.tsx +123 -0
- package/src/components/tools/TaskCard.tsx +203 -0
- package/src/components/tools/TodoCard.module.scss +131 -0
- package/src/components/tools/TodoCard.stories.tsx +202 -0
- package/src/components/tools/TodoCard.tsx +200 -0
- package/src/components/tools/ToolCard.stories.tsx +177 -0
- package/src/components/tools/ToolCard.tsx +60 -0
- package/src/components/tools/ToolCardProps.ts +20 -0
- package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
- package/src/components/tools/ToolSearchCard.tsx +86 -0
- package/src/components/tools/WorkpadCard.stories.tsx +106 -0
- package/src/components/tools/WorkpadCard.tsx +125 -0
- package/src/components/tools/classifyTool.test.ts +44 -0
- package/src/components/tools/classifyTool.ts +134 -0
- package/src/components/tools/parseDiff.ts +95 -0
- package/src/components/tools/parseShellOutput.ts +28 -0
- package/src/components/tools/toolCardHelpers.test.ts +53 -0
- package/src/components/tools/toolCards.module.scss +234 -0
- package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
- package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
- package/src/components/workspace/WorkspaceBoard.tsx +232 -0
- package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
- package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
- package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
- package/src/context/GrackleContext.ts +28 -0
- package/src/context/GrackleContextTypes.ts +64 -0
- package/src/context/SidebarContext.tsx +53 -0
- package/src/context/ThemeContext.tsx +21 -0
- package/src/context/ToastContext.tsx +56 -0
- package/src/hooks/types.ts +864 -0
- package/src/hooks/useEventSelection.test.ts +204 -0
- package/src/hooks/useEventSelection.ts +158 -0
- package/src/hooks/useSmartScroll.ts +151 -0
- package/src/hooks/useTheme.ts +228 -0
- package/src/index.ts +210 -0
- package/src/mocks/MockGrackleProvider.tsx +1397 -0
- package/src/mocks/mockData.ts +1966 -0
- package/src/mocks/mockKnowledgeData.ts +294 -0
- package/src/scss.d.ts +12 -0
- package/src/styles/global.scss +244 -0
- package/src/styles/mixins.scss +278 -0
- package/src/styles/prism-theme.scss +148 -0
- package/src/styles/theme.scss +1102 -0
- package/src/test-utils/storybook-decorators.tsx +50 -0
- package/src/test-utils/storybook-helpers.ts +262 -0
- package/src/themes.ts +142 -0
- package/src/utils/boardColumns.ts +141 -0
- package/src/utils/breadcrumbs.test.ts +285 -0
- package/src/utils/breadcrumbs.ts +222 -0
- package/src/utils/dashboard.test.ts +156 -0
- package/src/utils/dashboard.ts +195 -0
- package/src/utils/eventContent.test.ts +353 -0
- package/src/utils/eventContent.ts +209 -0
- package/src/utils/findingCategory.ts +33 -0
- package/src/utils/format.ts +27 -0
- package/src/utils/iconSize.ts +18 -0
- package/src/utils/navigation.ts +205 -0
- package/src/utils/route-config.test.ts +128 -0
- package/src/utils/scrollUtils.test.ts +65 -0
- package/src/utils/scrollUtils.ts +49 -0
- package/src/utils/sessionEvents.test.ts +302 -0
- package/src/utils/sessionEvents.ts +233 -0
- package/src/utils/taskStatus.tsx +137 -0
- package/src/utils/time.ts +92 -0
- package/tsconfig.json +8 -0
- package/vite.config.ts +20 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { render, within } from "@testing-library/react";
|
|
4
|
+
import { UpdateBanner } from "./UpdateBanner.js";
|
|
5
|
+
|
|
6
|
+
describe("UpdateBanner", () => {
|
|
7
|
+
it("renders nothing when updateAvailable is false", () => {
|
|
8
|
+
const { container } = render(
|
|
9
|
+
<UpdateBanner
|
|
10
|
+
currentVersion="0.76.0"
|
|
11
|
+
latestVersion="0.76.0"
|
|
12
|
+
updateAvailable={false}
|
|
13
|
+
isDocker={false}
|
|
14
|
+
/>,
|
|
15
|
+
);
|
|
16
|
+
expect(container.innerHTML).toBe("");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("shows banner with npm instructions when isDocker=false", () => {
|
|
20
|
+
const { container } = render(
|
|
21
|
+
<UpdateBanner
|
|
22
|
+
currentVersion="0.76.0"
|
|
23
|
+
latestVersion="0.77.0"
|
|
24
|
+
updateAvailable={true}
|
|
25
|
+
isDocker={false}
|
|
26
|
+
/>,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const banner = within(container).getByTestId("update-banner");
|
|
30
|
+
expect(banner).toBeTruthy();
|
|
31
|
+
expect(banner.textContent).toContain("0.77.0");
|
|
32
|
+
expect(banner.textContent).toContain("npm install");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("shows banner with Docker instructions when isDocker=true", () => {
|
|
36
|
+
const { container } = render(
|
|
37
|
+
<UpdateBanner
|
|
38
|
+
currentVersion="0.76.0"
|
|
39
|
+
latestVersion="0.77.0"
|
|
40
|
+
updateAvailable={true}
|
|
41
|
+
isDocker={true}
|
|
42
|
+
/>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const banner = within(container).getByTestId("update-banner");
|
|
46
|
+
expect(banner).toBeTruthy();
|
|
47
|
+
expect(banner.textContent).toContain("docker pull");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("displays current and latest versions", () => {
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<UpdateBanner
|
|
53
|
+
currentVersion="0.76.0"
|
|
54
|
+
latestVersion="0.77.0"
|
|
55
|
+
updateAvailable={true}
|
|
56
|
+
isDocker={false}
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const banner = within(container).getByTestId("update-banner");
|
|
61
|
+
expect(banner.textContent).toContain("0.76.0");
|
|
62
|
+
expect(banner.textContent).toContain("0.77.0");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import { Callout } from "./Callout.js";
|
|
3
|
+
|
|
4
|
+
/** Props for the UpdateBanner component. */
|
|
5
|
+
interface UpdateBannerProps {
|
|
6
|
+
/** The currently running Grackle version. */
|
|
7
|
+
currentVersion: string;
|
|
8
|
+
/** The latest version available on npm. */
|
|
9
|
+
latestVersion: string;
|
|
10
|
+
/** Whether an update is available. */
|
|
11
|
+
updateAvailable: boolean;
|
|
12
|
+
/** Whether the server is running in a Docker container. */
|
|
13
|
+
isDocker: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Dismissible info banner shown when a newer Grackle version is available.
|
|
18
|
+
*
|
|
19
|
+
* Renders nothing when `updateAvailable` is false. Shows context-appropriate
|
|
20
|
+
* upgrade instructions for Docker vs npm users.
|
|
21
|
+
*/
|
|
22
|
+
export function UpdateBanner({
|
|
23
|
+
currentVersion,
|
|
24
|
+
latestVersion,
|
|
25
|
+
updateAvailable,
|
|
26
|
+
isDocker,
|
|
27
|
+
}: UpdateBannerProps): JSX.Element {
|
|
28
|
+
if (!updateAvailable) {
|
|
29
|
+
return <></>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const command: string = isDocker
|
|
33
|
+
? `docker pull ghcr.io/nick-pape/grackle:latest && docker restart grackle`
|
|
34
|
+
: `npm install -g @grackle-ai/cli@${latestVersion}`;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div data-testid="update-banner">
|
|
38
|
+
<Callout variant="info" dismissible>
|
|
39
|
+
<strong>Grackle v{latestVersion}</strong> is available (you have v{currentVersion}).
|
|
40
|
+
{" "}Run: <code>{command}</code>
|
|
41
|
+
</Callout>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification components: transient toasts and persistent inline callouts.
|
|
3
|
+
* @module notifications
|
|
4
|
+
*/
|
|
5
|
+
export { Toast } from "./Toast.js";
|
|
6
|
+
export { ToastContainer } from "./ToastContainer.js";
|
|
7
|
+
export { Callout } from "./Callout.js";
|
|
8
|
+
export type { CalloutVariant } from "./Callout.js";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect } from "@storybook/test";
|
|
3
|
+
import { AboutPanel } from "./AboutPanel.js";
|
|
4
|
+
import { buildEnvironment, buildSession } from "../../test-utils/storybook-helpers.js";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof AboutPanel> = {
|
|
7
|
+
title: "App/Panels/AboutPanel",
|
|
8
|
+
component: AboutPanel,
|
|
9
|
+
args: {
|
|
10
|
+
connectionStatus: "connected",
|
|
11
|
+
environments: [],
|
|
12
|
+
sessions: [],
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof AboutPanel>;
|
|
18
|
+
|
|
19
|
+
/** Connected state with no environments or sessions. */
|
|
20
|
+
export const ConnectedEmpty: Story = {
|
|
21
|
+
play: async ({ canvas }) => {
|
|
22
|
+
await expect(canvas.getByTestId("about-panel")).toBeInTheDocument();
|
|
23
|
+
await expect(canvas.getByText("Connected")).toBeInTheDocument();
|
|
24
|
+
await expect(canvas.getByText("0/0 connected")).toBeInTheDocument();
|
|
25
|
+
await expect(canvas.getByText("About")).toBeInTheDocument();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Disconnected state. */
|
|
30
|
+
export const Disconnected: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
connectionStatus: "disconnected",
|
|
33
|
+
},
|
|
34
|
+
play: async ({ canvas }) => {
|
|
35
|
+
await expect(canvas.getByText("Disconnected")).toBeInTheDocument();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Connecting state shown during reconnection attempts. */
|
|
40
|
+
export const Connecting: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
connectionStatus: "connecting",
|
|
43
|
+
},
|
|
44
|
+
play: async ({ canvas }) => {
|
|
45
|
+
await expect(canvas.getByText("Connecting...")).toBeInTheDocument();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Shows environment and session counts when data is present. */
|
|
50
|
+
export const WithEnvironmentsAndSessions: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
connectionStatus: "connected",
|
|
53
|
+
environments: [
|
|
54
|
+
buildEnvironment({ id: "env-1", status: "connected" }),
|
|
55
|
+
buildEnvironment({ id: "env-2", status: "disconnected" }),
|
|
56
|
+
buildEnvironment({ id: "env-3", status: "connected" }),
|
|
57
|
+
],
|
|
58
|
+
sessions: [
|
|
59
|
+
buildSession({ id: "s-1", status: "running" }),
|
|
60
|
+
buildSession({ id: "s-2", status: "idle" }),
|
|
61
|
+
buildSession({ id: "s-3", status: "stopped" }),
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
play: async ({ canvas }) => {
|
|
65
|
+
// 2 of 3 environments connected
|
|
66
|
+
await expect(canvas.getByText("2/3 connected")).toBeInTheDocument();
|
|
67
|
+
// 2 active sessions (running + idle)
|
|
68
|
+
await expect(canvas.getByText("2")).toBeInTheDocument();
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { ConnectionStatus, Environment, Session } from "../../hooks/types.js";
|
|
3
|
+
import styles from "./SettingsPanel.module.scss";
|
|
4
|
+
|
|
5
|
+
declare const __APP_VERSION__: string;
|
|
6
|
+
|
|
7
|
+
/** Human-readable label for each connection state. */
|
|
8
|
+
const CONNECTION_LABEL: Record<ConnectionStatus, string> = {
|
|
9
|
+
connected: "Connected",
|
|
10
|
+
connecting: "Connecting...",
|
|
11
|
+
disconnected: "Disconnected",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** CSS class for the connection dot in each state. */
|
|
15
|
+
const CONNECTION_DOT_CLASS: Record<ConnectionStatus, string> = {
|
|
16
|
+
connected: styles.aboutDotConnected,
|
|
17
|
+
connecting: styles.aboutDotConnecting,
|
|
18
|
+
disconnected: styles.aboutDotDisconnected,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Props for the AboutPanel component. */
|
|
22
|
+
interface AboutPanelProps {
|
|
23
|
+
/** Current connection state of the event stream. */
|
|
24
|
+
connectionStatus: ConnectionStatus;
|
|
25
|
+
/** List of all environments. */
|
|
26
|
+
environments: Environment[];
|
|
27
|
+
/** List of all sessions. */
|
|
28
|
+
sessions: Session[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** About panel showing connection status, environment summary, session count, and version. */
|
|
32
|
+
export function AboutPanel({ connectionStatus, environments, sessions }: AboutPanelProps): JSX.Element {
|
|
33
|
+
const connectedEnvs = environments.filter((e) => e.status === "connected").length;
|
|
34
|
+
const totalEnvs = environments.length;
|
|
35
|
+
const activeSessionCount = sessions.filter((s) => ["running", "idle"].includes(s.status)).length;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<section className={styles.section} data-testid="about-panel">
|
|
39
|
+
<h3 className={styles.sectionTitle}>About</h3>
|
|
40
|
+
<p className={styles.sectionDescription}>
|
|
41
|
+
Connection status and application information.
|
|
42
|
+
</p>
|
|
43
|
+
<div className={styles.aboutGrid}>
|
|
44
|
+
<div className={styles.aboutItem}>
|
|
45
|
+
<span className={styles.aboutLabel}>Connection</span>
|
|
46
|
+
<span className={styles.aboutValue}>
|
|
47
|
+
<span className={`${styles.aboutDot} ${CONNECTION_DOT_CLASS[connectionStatus]}`} />
|
|
48
|
+
{CONNECTION_LABEL[connectionStatus]}
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div className={styles.aboutItem}>
|
|
52
|
+
<span className={styles.aboutLabel}>Environments</span>
|
|
53
|
+
<span className={styles.aboutValue}>{connectedEnvs}/{totalEnvs} connected</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div className={styles.aboutItem}>
|
|
56
|
+
<span className={styles.aboutLabel}>Active Sessions</span>
|
|
57
|
+
<span className={styles.aboutValue}>{activeSessionCount}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div className={styles.aboutItem}>
|
|
60
|
+
<span className={styles.aboutLabel}>Version</span>
|
|
61
|
+
<span className={styles.aboutValue}>{typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : "unknown"}</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, fn } from "@storybook/test";
|
|
3
|
+
import { AppearancePanel } from "./AppearancePanel.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof AppearancePanel> = {
|
|
6
|
+
component: AppearancePanel,
|
|
7
|
+
title: "App/Panels/AppearancePanel",
|
|
8
|
+
args: {
|
|
9
|
+
themeId: "grackle",
|
|
10
|
+
resolvedThemeId: "grackle-dark",
|
|
11
|
+
onSetTheme: fn(),
|
|
12
|
+
preferSystem: false,
|
|
13
|
+
onSetPreferSystem: fn(),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
|
|
19
|
+
/** Default appearance panel with theme options and system toggle. */
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
play: async ({ canvas }) => {
|
|
22
|
+
await expect(canvas.getByText("Appearance")).toBeInTheDocument();
|
|
23
|
+
await expect(canvas.getByText("Choose how Grackle looks across the app.")).toBeInTheDocument();
|
|
24
|
+
// The active theme should be reported at the bottom
|
|
25
|
+
await expect(canvas.getByText("grackle-dark")).toBeInTheDocument();
|
|
26
|
+
// System preference checkbox should be unchecked
|
|
27
|
+
const checkbox = canvas.getByRole("checkbox");
|
|
28
|
+
await expect(checkbox).not.toBeChecked();
|
|
29
|
+
// Theme buttons should be present
|
|
30
|
+
await expect(canvas.getByText("Grackle")).toBeInTheDocument();
|
|
31
|
+
await expect(canvas.getByText("Matrix")).toBeInTheDocument();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Panel with system preference enabled. */
|
|
36
|
+
export const SystemPreferenceEnabled: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
preferSystem: true,
|
|
39
|
+
},
|
|
40
|
+
play: async ({ canvas }) => {
|
|
41
|
+
const checkbox = canvas.getByRole("checkbox");
|
|
42
|
+
await expect(checkbox).toBeChecked();
|
|
43
|
+
await expect(canvas.getByText("Match system light/dark preference")).toBeInTheDocument();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import { THEMES } from "../../themes.js";
|
|
3
|
+
import styles from "./SettingsPanel.module.scss";
|
|
4
|
+
|
|
5
|
+
/** Props for the AppearancePanel component. */
|
|
6
|
+
export interface AppearancePanelProps {
|
|
7
|
+
/** The user's chosen theme ID. */
|
|
8
|
+
themeId: string;
|
|
9
|
+
/** The resolved data-theme value after system preference. */
|
|
10
|
+
resolvedThemeId: string;
|
|
11
|
+
/** Set a new theme by ID. */
|
|
12
|
+
onSetTheme: (nextId: string) => void;
|
|
13
|
+
/** Whether the theme follows the OS light/dark preference. */
|
|
14
|
+
preferSystem: boolean;
|
|
15
|
+
/** Toggle the OS preference behavior. */
|
|
16
|
+
onSetPreferSystem: (prefer: boolean) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Appearance settings panel with theme picker and system preference toggle. */
|
|
20
|
+
export function AppearancePanel({ themeId, resolvedThemeId, onSetTheme, preferSystem, onSetPreferSystem }: AppearancePanelProps): JSX.Element {
|
|
21
|
+
return (
|
|
22
|
+
<section className={styles.section}>
|
|
23
|
+
<h3 className={styles.sectionTitle}>Appearance</h3>
|
|
24
|
+
<p className={styles.sectionDescription}>
|
|
25
|
+
Choose how Grackle looks across the app.
|
|
26
|
+
</p>
|
|
27
|
+
<div className={styles.themeOptions}>
|
|
28
|
+
{THEMES.filter((t) => !t.hidden).map((t) => {
|
|
29
|
+
const hasVariants = !!(t.variantLightId && t.variantDarkId);
|
|
30
|
+
const isSelected = hasVariants
|
|
31
|
+
? (themeId === t.id || themeId === t.variantLightId || themeId === t.variantDarkId)
|
|
32
|
+
: themeId === t.id;
|
|
33
|
+
const isLight = hasVariants && resolvedThemeId === t.variantLightId;
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
key={t.id}
|
|
37
|
+
type="button"
|
|
38
|
+
className={`${styles.themeOption} ${isSelected ? styles.themeOptionSelected : ""}`}
|
|
39
|
+
aria-pressed={isSelected}
|
|
40
|
+
onClick={() => onSetTheme(t.id)}
|
|
41
|
+
>
|
|
42
|
+
<span className={styles.themeOptionHeader}>
|
|
43
|
+
<span>
|
|
44
|
+
<span className={styles.themeOptionLabel}>{t.label}</span>
|
|
45
|
+
<span className={styles.themeOptionDesc}>{t.description}</span>
|
|
46
|
+
</span>
|
|
47
|
+
{hasVariants && (
|
|
48
|
+
<span className={styles.variantToggle}>
|
|
49
|
+
<span
|
|
50
|
+
role="button"
|
|
51
|
+
tabIndex={0}
|
|
52
|
+
className={`${styles.variantButton} ${isSelected && isLight ? styles.variantActive : ""}`}
|
|
53
|
+
onClick={(e) => { e.stopPropagation(); onSetPreferSystem(false); onSetTheme(t.variantLightId!); }}
|
|
54
|
+
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); onSetPreferSystem(false); onSetTheme(t.variantLightId!); } }}
|
|
55
|
+
aria-label="Light variant"
|
|
56
|
+
aria-pressed={isSelected && isLight}
|
|
57
|
+
>☼</span>
|
|
58
|
+
<span
|
|
59
|
+
role="button"
|
|
60
|
+
tabIndex={0}
|
|
61
|
+
className={`${styles.variantButton} ${isSelected && !isLight ? styles.variantActive : ""}`}
|
|
62
|
+
onClick={(e) => { e.stopPropagation(); onSetPreferSystem(false); onSetTheme(t.variantDarkId!); }}
|
|
63
|
+
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); onSetPreferSystem(false); onSetTheme(t.variantDarkId!); } }}
|
|
64
|
+
aria-label="Dark variant"
|
|
65
|
+
aria-pressed={isSelected && !isLight}
|
|
66
|
+
>☾</span>
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</span>
|
|
70
|
+
{t.swatches && (
|
|
71
|
+
<span className={styles.themeSwatches}>
|
|
72
|
+
{t.swatches.map((color, i) => (
|
|
73
|
+
<span key={i} className={styles.themeSwatch} style={{ background: color }} />
|
|
74
|
+
))}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</button>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
<label className={styles.systemToggle}>
|
|
82
|
+
<input
|
|
83
|
+
type="checkbox"
|
|
84
|
+
checked={preferSystem}
|
|
85
|
+
onChange={(e) => onSetPreferSystem(e.target.checked)}
|
|
86
|
+
/>
|
|
87
|
+
<span>Match system light/dark preference</span>
|
|
88
|
+
</label>
|
|
89
|
+
<p className={styles.systemToggleHint}>
|
|
90
|
+
Automatically switches between light and dark variants when available.
|
|
91
|
+
</p>
|
|
92
|
+
<p className={styles.themeActive}>
|
|
93
|
+
Active theme: <strong>{resolvedThemeId}</strong>
|
|
94
|
+
</p>
|
|
95
|
+
</section>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, fn, userEvent } from "@storybook/test";
|
|
3
|
+
import { CredentialProvidersPanel } from "./CredentialProvidersPanel.js";
|
|
4
|
+
import { buildCredentialProviderConfig } from "../../test-utils/storybook-helpers.js";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof CredentialProvidersPanel> = {
|
|
7
|
+
title: "App/Panels/CredentialProvidersPanel",
|
|
8
|
+
component: CredentialProvidersPanel,
|
|
9
|
+
args: {
|
|
10
|
+
credentialProviders: buildCredentialProviderConfig(),
|
|
11
|
+
onUpdateCredentialProviders: fn(),
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof CredentialProvidersPanel>;
|
|
17
|
+
|
|
18
|
+
/** Default state with all providers set to "off". */
|
|
19
|
+
export const AllProvidersOff: Story = {
|
|
20
|
+
play: async ({ canvas }) => {
|
|
21
|
+
// All five provider labels should be visible
|
|
22
|
+
await expect(canvas.getByText("Claude")).toBeInTheDocument();
|
|
23
|
+
await expect(canvas.getByText("GitHub")).toBeInTheDocument();
|
|
24
|
+
await expect(canvas.getByText("Copilot")).toBeInTheDocument();
|
|
25
|
+
await expect(canvas.getByText("Codex")).toBeInTheDocument();
|
|
26
|
+
await expect(canvas.getByText("Goose")).toBeInTheDocument();
|
|
27
|
+
|
|
28
|
+
// Section title should be present
|
|
29
|
+
await expect(canvas.getByText("Credential Providers")).toBeInTheDocument();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Some providers enabled. */
|
|
34
|
+
export const SomeProvidersEnabled: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
credentialProviders: buildCredentialProviderConfig({
|
|
37
|
+
claude: "subscription",
|
|
38
|
+
github: "on",
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
play: async ({ canvas }) => {
|
|
42
|
+
await expect(canvas.getByText("Claude")).toBeInTheDocument();
|
|
43
|
+
await expect(canvas.getByText("GitHub")).toBeInTheDocument();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Changing a provider selection fires the onUpdateCredentialProviders callback. */
|
|
48
|
+
export const ChangingProviderFiresCallback: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
onUpdateCredentialProviders: fn(),
|
|
51
|
+
},
|
|
52
|
+
play: async ({ canvas, args }) => {
|
|
53
|
+
// Find the GitHub provider row by its label, then get its select element
|
|
54
|
+
const githubLabel: HTMLElement = canvas.getByText("GitHub");
|
|
55
|
+
const githubRow: HTMLElement = githubLabel.closest("div")!;
|
|
56
|
+
const githubSelect: HTMLSelectElement = githubRow.querySelector("select")!;
|
|
57
|
+
|
|
58
|
+
await userEvent.selectOptions(githubSelect, "on");
|
|
59
|
+
|
|
60
|
+
await expect(args.onUpdateCredentialProviders).toHaveBeenCalled();
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import type { CredentialProviderConfig } from "../../hooks/types.js";
|
|
3
|
+
import styles from "./SettingsPanel.module.scss";
|
|
4
|
+
|
|
5
|
+
/** Provider descriptor for rendering toggle rows. */
|
|
6
|
+
interface ProviderDef {
|
|
7
|
+
key: keyof CredentialProviderConfig;
|
|
8
|
+
label: string;
|
|
9
|
+
description: string;
|
|
10
|
+
options: Array<{ value: string; label: string }>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Definitions for each credential provider. */
|
|
14
|
+
const PROVIDERS: ProviderDef[] = [
|
|
15
|
+
{
|
|
16
|
+
key: "claude",
|
|
17
|
+
label: "Claude",
|
|
18
|
+
description: "Forward Claude credentials for AI agent access.",
|
|
19
|
+
options: [
|
|
20
|
+
{ value: "off", label: "Off" },
|
|
21
|
+
{ value: "subscription", label: "Subscription" },
|
|
22
|
+
{ value: "api_key", label: "API Key" },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: "github",
|
|
27
|
+
label: "GitHub",
|
|
28
|
+
description: "Forward GITHUB_TOKEN and GH_TOKEN for git operations.",
|
|
29
|
+
options: [
|
|
30
|
+
{ value: "off", label: "Off" },
|
|
31
|
+
{ value: "on", label: "On" },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "copilot",
|
|
36
|
+
label: "Copilot",
|
|
37
|
+
description: "Forward Copilot tokens (COPILOT_GITHUB_TOKEN, CLI config).",
|
|
38
|
+
options: [
|
|
39
|
+
{ value: "off", label: "Off" },
|
|
40
|
+
{ value: "on", label: "On" },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "codex",
|
|
45
|
+
label: "Codex",
|
|
46
|
+
description: "Forward OPENAI_API_KEY for Codex/OpenAI access.",
|
|
47
|
+
options: [
|
|
48
|
+
{ value: "off", label: "Off" },
|
|
49
|
+
{ value: "on", label: "On" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "goose",
|
|
54
|
+
label: "Goose",
|
|
55
|
+
description: "Forward Goose config and API keys for Goose agent access.",
|
|
56
|
+
options: [
|
|
57
|
+
{ value: "off", label: "Off" },
|
|
58
|
+
{ value: "on", label: "On" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** Props for the CredentialProvidersPanel component. */
|
|
64
|
+
interface CredentialProvidersPanelProps {
|
|
65
|
+
/** Current credential provider configuration. */
|
|
66
|
+
credentialProviders: CredentialProviderConfig;
|
|
67
|
+
/** Callback to update the credential provider configuration. */
|
|
68
|
+
onUpdateCredentialProviders: (config: CredentialProviderConfig) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Panel for configuring which credential providers are auto-forwarded to environments. */
|
|
72
|
+
export function CredentialProvidersPanel({ credentialProviders, onUpdateCredentialProviders }: CredentialProvidersPanelProps): JSX.Element {
|
|
73
|
+
|
|
74
|
+
const handleChange = (key: keyof CredentialProviderConfig, value: string): void => {
|
|
75
|
+
const updated: CredentialProviderConfig = { ...credentialProviders };
|
|
76
|
+
if (key === "claude") {
|
|
77
|
+
updated.claude = value as CredentialProviderConfig["claude"];
|
|
78
|
+
} else {
|
|
79
|
+
updated[key] = value as "off" | "on";
|
|
80
|
+
}
|
|
81
|
+
onUpdateCredentialProviders(updated);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<section className={styles.section}>
|
|
86
|
+
<h3 className={styles.sectionTitle}>Credential Providers</h3>
|
|
87
|
+
<p className={styles.sectionDescription}>
|
|
88
|
+
Enable providers to automatically forward credentials to environments at task start.
|
|
89
|
+
Credentials are read fresh from the host each time.
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
<div className={styles.tokenList}>
|
|
93
|
+
{PROVIDERS.map((provider) => (
|
|
94
|
+
<div key={provider.key} className={styles.tokenRow}>
|
|
95
|
+
<span className={styles.tokenName}>{provider.label}</span>
|
|
96
|
+
<span className={styles.tokenTarget}>{provider.description}</span>
|
|
97
|
+
<select
|
|
98
|
+
className={styles.select}
|
|
99
|
+
value={credentialProviders[provider.key]}
|
|
100
|
+
onChange={(e) => handleChange(provider.key, e.target.value)}
|
|
101
|
+
>
|
|
102
|
+
{provider.options.map((opt) => (
|
|
103
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
104
|
+
))}
|
|
105
|
+
</select>
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
</section>
|
|
110
|
+
);
|
|
111
|
+
}
|