@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,173 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/** Options for the useEditableField hook. */
|
|
4
|
+
export interface UseEditableFieldOptions {
|
|
5
|
+
/** Current persisted value. */
|
|
6
|
+
value: string;
|
|
7
|
+
/** Called when the user saves a new value. */
|
|
8
|
+
onSave: (value: string) => void;
|
|
9
|
+
/** Optional validation — return an error string, or undefined if valid. */
|
|
10
|
+
validate?: (value: string) => string | undefined;
|
|
11
|
+
/** Unique identifier for this field (used for coordination). */
|
|
12
|
+
fieldId: string;
|
|
13
|
+
/** Which field is currently being edited (coordination from parent). */
|
|
14
|
+
activeFieldId?: string | null; // eslint-disable-line @rushstack/no-new-null
|
|
15
|
+
/** Callback to tell the parent which field is active. */
|
|
16
|
+
onActivate?: (fieldId: string | null) => void; // eslint-disable-line @rushstack/no-new-null
|
|
17
|
+
/** Whether Enter key triggers save (true for text inputs, false for textarea). */
|
|
18
|
+
enterToSave?: boolean;
|
|
19
|
+
/** Whether to trim whitespace before saving. Default true. */
|
|
20
|
+
trimOnSave?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Return type for the useEditableField hook. */
|
|
24
|
+
export interface UseEditableFieldReturn {
|
|
25
|
+
/** Whether this field is currently in edit mode. */
|
|
26
|
+
isEditing: boolean;
|
|
27
|
+
/** The current draft value while editing. */
|
|
28
|
+
draft: string;
|
|
29
|
+
/** Validation error message, or empty string. */
|
|
30
|
+
error: string;
|
|
31
|
+
/** Whether the draft differs from the persisted value. */
|
|
32
|
+
isDirty: boolean;
|
|
33
|
+
/** Enter edit mode with the current value as the draft. */
|
|
34
|
+
startEdit: () => void;
|
|
35
|
+
/** Exit edit mode without saving. */
|
|
36
|
+
cancelEdit: () => void;
|
|
37
|
+
/** Validate and save the current draft. */
|
|
38
|
+
save: () => void;
|
|
39
|
+
/** Update the draft value. Also clears any validation error. */
|
|
40
|
+
setDraft: (value: string) => void;
|
|
41
|
+
/** Clear the validation error. */
|
|
42
|
+
clearError: () => void;
|
|
43
|
+
/** Blur handler that auto-saves, respecting ignoreInitialBlur and data-edit-action. */
|
|
44
|
+
handleBlur: (event: React.FocusEvent) => void;
|
|
45
|
+
/** Keyboard handler for Escape (cancel) and optionally Enter (save). */
|
|
46
|
+
handleKeyDown: (event: React.KeyboardEvent) => void;
|
|
47
|
+
/**
|
|
48
|
+
* Ref that prevents the initial blur (caused by clicking the edit button)
|
|
49
|
+
* from triggering a save. Set to true when startEdit is called, reset on
|
|
50
|
+
* first blur.
|
|
51
|
+
*/
|
|
52
|
+
ignoreInitialBlurRef: React.RefObject<boolean>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Shared hook that encapsulates the click-to-edit state machine used by
|
|
57
|
+
* EditableTextField, EditableTextArea, and EditableSelect.
|
|
58
|
+
*/
|
|
59
|
+
export function useEditableField(options: UseEditableFieldOptions): UseEditableFieldReturn {
|
|
60
|
+
const {
|
|
61
|
+
value,
|
|
62
|
+
onSave,
|
|
63
|
+
validate,
|
|
64
|
+
fieldId,
|
|
65
|
+
activeFieldId,
|
|
66
|
+
onActivate,
|
|
67
|
+
enterToSave = true,
|
|
68
|
+
trimOnSave = true,
|
|
69
|
+
} = options;
|
|
70
|
+
|
|
71
|
+
const [draft, setDraftRaw] = useState("");
|
|
72
|
+
const [error, setError] = useState("");
|
|
73
|
+
const ignoreInitialBlurRef = useRef<boolean>(false);
|
|
74
|
+
|
|
75
|
+
const isEditing = activeFieldId === fieldId;
|
|
76
|
+
|
|
77
|
+
const setDraft = useCallback((v: string) => {
|
|
78
|
+
setDraftRaw(v);
|
|
79
|
+
setError("");
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const clearError = useCallback(() => {
|
|
83
|
+
setError("");
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const cancelEdit = useCallback(() => {
|
|
87
|
+
ignoreInitialBlurRef.current = false;
|
|
88
|
+
onActivate?.(null);
|
|
89
|
+
setDraftRaw("");
|
|
90
|
+
setError("");
|
|
91
|
+
}, [onActivate]);
|
|
92
|
+
|
|
93
|
+
const save = useCallback(() => {
|
|
94
|
+
const saveValue = trimOnSave ? draft.trim() : draft;
|
|
95
|
+
|
|
96
|
+
if (validate) {
|
|
97
|
+
const validationError = validate(draft);
|
|
98
|
+
if (validationError) {
|
|
99
|
+
setError(validationError);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// No-op when value hasn't changed
|
|
105
|
+
const compareValue = trimOnSave ? value.trim() : value;
|
|
106
|
+
if (saveValue === compareValue) {
|
|
107
|
+
cancelEdit();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onSave(saveValue);
|
|
112
|
+
cancelEdit();
|
|
113
|
+
}, [draft, value, trimOnSave, validate, onSave, cancelEdit]);
|
|
114
|
+
|
|
115
|
+
const startEdit = useCallback(() => {
|
|
116
|
+
ignoreInitialBlurRef.current = true;
|
|
117
|
+
onActivate?.(fieldId);
|
|
118
|
+
setDraftRaw(value);
|
|
119
|
+
setError("");
|
|
120
|
+
}, [fieldId, value, onActivate]);
|
|
121
|
+
|
|
122
|
+
const handleBlur = useCallback((event: React.FocusEvent) => {
|
|
123
|
+
if (ignoreInitialBlurRef.current) {
|
|
124
|
+
ignoreInitialBlurRef.current = false;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
event.relatedTarget instanceof HTMLElement &&
|
|
129
|
+
event.relatedTarget.dataset.editAction === fieldId
|
|
130
|
+
) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
save();
|
|
134
|
+
}, [fieldId, save]);
|
|
135
|
+
|
|
136
|
+
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
|
137
|
+
if (event.key === "Escape") {
|
|
138
|
+
cancelEdit();
|
|
139
|
+
} else if (event.key === "Enter" && enterToSave) {
|
|
140
|
+
save();
|
|
141
|
+
}
|
|
142
|
+
}, [cancelEdit, enterToSave, save]);
|
|
143
|
+
|
|
144
|
+
const isDirty = (() => {
|
|
145
|
+
if (!isEditing) return false;
|
|
146
|
+
const compareValue = trimOnSave ? value.trim() : value;
|
|
147
|
+
const draftValue = trimOnSave ? draft.trim() : draft;
|
|
148
|
+
return draftValue !== compareValue;
|
|
149
|
+
})();
|
|
150
|
+
|
|
151
|
+
// If another field becomes active, reset our local state
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!isEditing && (draft !== "" || error !== "")) {
|
|
154
|
+
setDraftRaw("");
|
|
155
|
+
setError("");
|
|
156
|
+
}
|
|
157
|
+
}, [isEditing, draft, error]);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
isEditing,
|
|
161
|
+
draft,
|
|
162
|
+
error,
|
|
163
|
+
isDirty,
|
|
164
|
+
startEdit,
|
|
165
|
+
cancelEdit,
|
|
166
|
+
save,
|
|
167
|
+
setDraft,
|
|
168
|
+
clearError,
|
|
169
|
+
handleBlur,
|
|
170
|
+
handleKeyDown,
|
|
171
|
+
ignoreInitialBlurRef,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component exports organized by category.
|
|
3
|
+
* @module components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Layout components - application shell structure
|
|
7
|
+
export { StatusBar, Sidebar, BottomStatusBar } from "./layout/index.js";
|
|
8
|
+
|
|
9
|
+
// Panel components - main content areas
|
|
10
|
+
export { FindingsPanel } from "./panels/index.js";
|
|
11
|
+
|
|
12
|
+
// List components - sidebar navigation
|
|
13
|
+
export { EnvironmentNav } from "./lists/index.js";
|
|
14
|
+
|
|
15
|
+
// Display components - content rendering
|
|
16
|
+
export { EventRenderer } from "./display/index.js";
|
|
17
|
+
|
|
18
|
+
// Notification components - toasts and callouts
|
|
19
|
+
export { Toast, ToastContainer, Callout } from "./notifications/index.js";
|
|
20
|
+
export type { CalloutVariant } from "./notifications/index.js";
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
.panel {
|
|
2
|
+
position: absolute;
|
|
3
|
+
top: 0;
|
|
4
|
+
right: 0;
|
|
5
|
+
bottom: 0;
|
|
6
|
+
width: 350px;
|
|
7
|
+
background: var(--bg-surface, #1a1a2e);
|
|
8
|
+
border-left: 1px solid var(--border-default, #333);
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
overflow-y: auto;
|
|
12
|
+
z-index: 10;
|
|
13
|
+
animation: slideIn 0.2s ease-out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@keyframes slideIn {
|
|
17
|
+
from {
|
|
18
|
+
transform: translateX(100%);
|
|
19
|
+
}
|
|
20
|
+
to {
|
|
21
|
+
transform: translateX(0);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.header {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
padding: 16px;
|
|
30
|
+
border-bottom: 1px solid var(--border-default, #333);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.title {
|
|
34
|
+
margin: 0;
|
|
35
|
+
font-size: 16px;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
text-overflow: ellipsis;
|
|
39
|
+
white-space: nowrap;
|
|
40
|
+
flex: 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.closeButton {
|
|
44
|
+
background: none;
|
|
45
|
+
border: none;
|
|
46
|
+
color: var(--text-secondary, #999);
|
|
47
|
+
font-size: 24px;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
padding: 0 4px;
|
|
50
|
+
line-height: 1;
|
|
51
|
+
|
|
52
|
+
&:hover {
|
|
53
|
+
color: var(--text-primary, #fff);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.body {
|
|
58
|
+
padding: 16px;
|
|
59
|
+
flex: 1;
|
|
60
|
+
overflow-y: auto;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.badge {
|
|
64
|
+
display: inline-block;
|
|
65
|
+
padding: 2px 8px;
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
font-weight: 500;
|
|
69
|
+
background: var(--bg-inset, #222);
|
|
70
|
+
color: var(--text-secondary, #999);
|
|
71
|
+
margin-bottom: 12px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.section {
|
|
75
|
+
margin-bottom: 16px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.sectionLabel {
|
|
79
|
+
font-size: 11px;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
text-transform: uppercase;
|
|
82
|
+
color: var(--text-disabled, #666);
|
|
83
|
+
margin-bottom: 6px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.content {
|
|
87
|
+
font-size: 14px;
|
|
88
|
+
line-height: 1.5;
|
|
89
|
+
color: var(--text-primary, #eee);
|
|
90
|
+
margin: 0;
|
|
91
|
+
white-space: pre-wrap;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.tags {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-wrap: wrap;
|
|
97
|
+
gap: 4px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.tag {
|
|
101
|
+
display: inline-block;
|
|
102
|
+
padding: 2px 8px;
|
|
103
|
+
border-radius: 12px;
|
|
104
|
+
font-size: 12px;
|
|
105
|
+
background: var(--bg-inset, #222);
|
|
106
|
+
color: var(--text-secondary, #aaa);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.viewLink {
|
|
110
|
+
background: none;
|
|
111
|
+
border: 1px solid var(--accent-blue, #4A9EFF);
|
|
112
|
+
color: var(--accent-blue, #4A9EFF);
|
|
113
|
+
padding: 6px 12px;
|
|
114
|
+
border-radius: 6px;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
font-size: 13px;
|
|
117
|
+
|
|
118
|
+
&:hover {
|
|
119
|
+
background: var(--accent-blue, #4A9EFF);
|
|
120
|
+
color: #fff;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.edgeList {
|
|
125
|
+
list-style: none;
|
|
126
|
+
padding: 0;
|
|
127
|
+
margin: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.edgeItem {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 8px;
|
|
134
|
+
padding: 4px 0;
|
|
135
|
+
border-bottom: 1px solid var(--border-default, #2a2a2a);
|
|
136
|
+
font-size: 13px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.edgeType {
|
|
140
|
+
font-family: monospace;
|
|
141
|
+
font-size: 11px;
|
|
142
|
+
color: var(--text-disabled, #666);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.edgeNodeLink {
|
|
146
|
+
background: none;
|
|
147
|
+
border: none;
|
|
148
|
+
color: var(--accent-blue, #4A9EFF);
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
font-family: monospace;
|
|
152
|
+
|
|
153
|
+
&:hover {
|
|
154
|
+
text-decoration: underline;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.timestamps {
|
|
159
|
+
margin-top: 16px;
|
|
160
|
+
font-size: 12px;
|
|
161
|
+
color: var(--text-disabled, #666);
|
|
162
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, fn, userEvent } from "@storybook/test";
|
|
3
|
+
import type { GraphNode, NodeDetail } from "../../hooks/types.js";
|
|
4
|
+
import { makeGraphNode } from "../../test-utils/storybook-helpers.js";
|
|
5
|
+
import { KnowledgeDetailPanel } from "./KnowledgeDetailPanel.js";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Mock data
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const defaultNode: GraphNode = makeGraphNode({
|
|
12
|
+
id: "node-001",
|
|
13
|
+
label: "Authentication Flow",
|
|
14
|
+
content: "OAuth2 flow with PKCE for CLI clients.",
|
|
15
|
+
tags: ["auth", "security", "autodocs"],
|
|
16
|
+
val: 3,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const connectedNodeA: GraphNode = makeGraphNode({
|
|
20
|
+
id: "node-002",
|
|
21
|
+
label: "Session Token Storage",
|
|
22
|
+
category: "decision",
|
|
23
|
+
val: 1,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const connectedNodeB: GraphNode = makeGraphNode({
|
|
27
|
+
id: "node-003",
|
|
28
|
+
label: "Token Rotation Policy",
|
|
29
|
+
category: "insight",
|
|
30
|
+
val: 2,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const unknownNodeId: string = "62d111f7-aaaa-bbbb-cccc-123456789abc";
|
|
34
|
+
|
|
35
|
+
const defaultDetail: NodeDetail = {
|
|
36
|
+
node: defaultNode,
|
|
37
|
+
edges: [
|
|
38
|
+
{ fromId: "node-001", toId: "node-002", type: "relates_to" },
|
|
39
|
+
{ fromId: "node-003", toId: "node-001", type: "derived_from" },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const allNodes: GraphNode[] = [defaultNode, connectedNodeA, connectedNodeB];
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Story meta
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const meta: Meta<typeof KnowledgeDetailPanel> = {
|
|
50
|
+
title: "Grackle/Knowledge/KnowledgeDetailPanel",
|
|
51
|
+
component: KnowledgeDetailPanel,
|
|
52
|
+
args: {
|
|
53
|
+
detail: defaultDetail,
|
|
54
|
+
nodes: allNodes,
|
|
55
|
+
onClose: fn(),
|
|
56
|
+
onSelectNode: fn(),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default meta;
|
|
61
|
+
type Story = StoryObj<typeof KnowledgeDetailPanel>;
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Stories
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/** Panel renders the node label, badge, content, tags, and timestamps. */
|
|
68
|
+
export const BasicRendering: Story = {
|
|
69
|
+
play: async ({ canvas }) => {
|
|
70
|
+
// Title
|
|
71
|
+
await expect(canvas.getByText("Authentication Flow")).toBeInTheDocument();
|
|
72
|
+
|
|
73
|
+
// Category badge
|
|
74
|
+
await expect(canvas.getByText("concept")).toBeInTheDocument();
|
|
75
|
+
|
|
76
|
+
// Content
|
|
77
|
+
await expect(canvas.getByText("OAuth2 flow with PKCE for CLI clients.")).toBeInTheDocument();
|
|
78
|
+
|
|
79
|
+
// Tags
|
|
80
|
+
await expect(canvas.getByText("auth")).toBeInTheDocument();
|
|
81
|
+
await expect(canvas.getByText("security")).toBeInTheDocument();
|
|
82
|
+
|
|
83
|
+
// Timestamps
|
|
84
|
+
await expect(canvas.getByText(/Created:/)).toBeInTheDocument();
|
|
85
|
+
await expect(canvas.getByText(/Updated:/)).toBeInTheDocument();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Edge links display connected node titles, not truncated UUIDs. */
|
|
90
|
+
export const EdgeLinksShowNodeTitles: Story = {
|
|
91
|
+
play: async ({ canvas }) => {
|
|
92
|
+
// Should show resolved titles, not truncated UUIDs
|
|
93
|
+
await expect(canvas.getByText("Session Token Storage")).toBeInTheDocument();
|
|
94
|
+
await expect(canvas.getByText("Token Rotation Policy")).toBeInTheDocument();
|
|
95
|
+
|
|
96
|
+
// Edge types should be displayed
|
|
97
|
+
const edgeTypes = canvas.getAllByTestId("edge-type");
|
|
98
|
+
await expect(edgeTypes.length).toBe(2);
|
|
99
|
+
await expect(edgeTypes[0]).toHaveTextContent("relates_to");
|
|
100
|
+
await expect(edgeTypes[1]).toHaveTextContent("derived_from");
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Edge links fall back to truncated UUID when node is not in the graph. */
|
|
105
|
+
export const EdgeLinksFallbackToTruncatedId: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
detail: {
|
|
108
|
+
node: defaultNode,
|
|
109
|
+
edges: [
|
|
110
|
+
{ fromId: "node-001", toId: unknownNodeId, type: "mentions" },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
nodes: [defaultNode],
|
|
114
|
+
},
|
|
115
|
+
play: async ({ canvas }) => {
|
|
116
|
+
// Should show truncated UUID since the connected node is not in the nodes list
|
|
117
|
+
await expect(canvas.getByText("62d111f7...")).toBeInTheDocument();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** Clicking an edge link calls onSelectNode with the connected node ID. */
|
|
122
|
+
export const EdgeLinkClickCallsOnSelectNode: Story = {
|
|
123
|
+
play: async ({ canvas, args }) => {
|
|
124
|
+
const edgeLinks = canvas.getAllByTestId("edge-node-link");
|
|
125
|
+
await userEvent.click(edgeLinks[0]);
|
|
126
|
+
await expect(args.onSelectNode).toHaveBeenCalledWith("node-002");
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Close button calls onClose. */
|
|
131
|
+
export const CloseButtonCallsOnClose: Story = {
|
|
132
|
+
play: async ({ canvas, args }) => {
|
|
133
|
+
const closeButton = canvas.getByRole("button", { name: "Close" });
|
|
134
|
+
await userEvent.click(closeButton);
|
|
135
|
+
await expect(args.onClose).toHaveBeenCalled();
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** No edges section when the node has no edges. */
|
|
140
|
+
export const NoEdges: Story = {
|
|
141
|
+
args: {
|
|
142
|
+
detail: {
|
|
143
|
+
node: defaultNode,
|
|
144
|
+
edges: [],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
play: async ({ canvas }) => {
|
|
148
|
+
// Title should still render
|
|
149
|
+
await expect(canvas.getByText("Authentication Flow")).toBeInTheDocument();
|
|
150
|
+
|
|
151
|
+
// No "Edges" section label
|
|
152
|
+
const panel = canvas.getByTestId("knowledge-detail-panel");
|
|
153
|
+
await expect(panel.textContent).not.toContain("Edges (");
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Reference nodes show the "View in Grackle" link and a reference badge. */
|
|
158
|
+
export const ReferenceNode: Story = {
|
|
159
|
+
args: {
|
|
160
|
+
detail: {
|
|
161
|
+
node: makeGraphNode({
|
|
162
|
+
id: "ref-001",
|
|
163
|
+
kind: "reference",
|
|
164
|
+
sourceType: "task",
|
|
165
|
+
sourceId: "task-123",
|
|
166
|
+
label: "Fix login bug",
|
|
167
|
+
content: undefined,
|
|
168
|
+
tags: [],
|
|
169
|
+
}),
|
|
170
|
+
edges: [],
|
|
171
|
+
},
|
|
172
|
+
nodes: [],
|
|
173
|
+
},
|
|
174
|
+
play: async ({ canvas }) => {
|
|
175
|
+
// Reference badge
|
|
176
|
+
await expect(canvas.getByText("Reference (task)")).toBeInTheDocument();
|
|
177
|
+
|
|
178
|
+
// View in Grackle link
|
|
179
|
+
const viewLink = canvas.getByRole("button", { name: /View in Grackle/ });
|
|
180
|
+
await expect(viewLink).toBeInTheDocument();
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** Node without content or tags omits those sections. */
|
|
185
|
+
export const MinimalNode: Story = {
|
|
186
|
+
args: {
|
|
187
|
+
detail: {
|
|
188
|
+
node: makeGraphNode({
|
|
189
|
+
label: "Minimal Node",
|
|
190
|
+
content: undefined,
|
|
191
|
+
tags: undefined,
|
|
192
|
+
createdAt: undefined,
|
|
193
|
+
updatedAt: undefined,
|
|
194
|
+
}),
|
|
195
|
+
edges: [],
|
|
196
|
+
},
|
|
197
|
+
nodes: [],
|
|
198
|
+
},
|
|
199
|
+
play: async ({ canvas }) => {
|
|
200
|
+
// Title renders
|
|
201
|
+
await expect(canvas.getByText("Minimal Node")).toBeInTheDocument();
|
|
202
|
+
|
|
203
|
+
// No Content or Tags sections
|
|
204
|
+
const panel = canvas.getByTestId("knowledge-detail-panel");
|
|
205
|
+
await expect(panel.textContent).not.toContain("Content");
|
|
206
|
+
await expect(panel.textContent).not.toContain("Tags");
|
|
207
|
+
},
|
|
208
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slide-in detail panel for a selected knowledge graph node.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useMemo, type JSX } from "react";
|
|
8
|
+
import type { GraphNode, NodeDetail } from "../../hooks/types.js";
|
|
9
|
+
import { taskUrl, sessionUrl, findingUrl } from "../../utils/navigation.js";
|
|
10
|
+
import { useAppNavigate } from "../../utils/navigation.js";
|
|
11
|
+
import styles from "./KnowledgeDetailPanel.module.scss";
|
|
12
|
+
|
|
13
|
+
interface KnowledgeDetailPanelProps {
|
|
14
|
+
detail: NodeDetail;
|
|
15
|
+
nodes: GraphNode[];
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
onSelectNode: (id: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Slide-in panel showing full details for a selected knowledge node. */
|
|
21
|
+
export function KnowledgeDetailPanel({
|
|
22
|
+
detail,
|
|
23
|
+
nodes,
|
|
24
|
+
onClose,
|
|
25
|
+
onSelectNode,
|
|
26
|
+
}: KnowledgeDetailPanelProps): JSX.Element {
|
|
27
|
+
const navigate = useAppNavigate();
|
|
28
|
+
const { node, edges } = detail;
|
|
29
|
+
const nodeById = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
|
|
30
|
+
|
|
31
|
+
/** Navigate to the source entity for reference nodes. */
|
|
32
|
+
function handleViewInGrackle(): void {
|
|
33
|
+
if (node.kind !== "reference" || !node.sourceId) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
switch (node.sourceType) {
|
|
37
|
+
case "task":
|
|
38
|
+
navigate(taskUrl(node.sourceId));
|
|
39
|
+
break;
|
|
40
|
+
case "session":
|
|
41
|
+
navigate(sessionUrl(node.sourceId));
|
|
42
|
+
break;
|
|
43
|
+
case "finding":
|
|
44
|
+
navigate(findingUrl(node.sourceId));
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={styles.panel} data-testid="knowledge-detail-panel">
|
|
53
|
+
<div className={styles.header}>
|
|
54
|
+
<h3 className={styles.title}>{node.label}</h3>
|
|
55
|
+
<button className={styles.closeButton} onClick={onClose} aria-label="Close">
|
|
56
|
+
×
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className={styles.body}>
|
|
61
|
+
<div className={styles.badge}>
|
|
62
|
+
{node.kind === "reference" ? `Reference (${node.sourceType})` : node.category}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{node.content && (
|
|
66
|
+
<div className={styles.section}>
|
|
67
|
+
<div className={styles.sectionLabel}>Content</div>
|
|
68
|
+
<p className={styles.content}>{node.content}</p>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{node.tags && node.tags.length > 0 && (
|
|
73
|
+
<div className={styles.section}>
|
|
74
|
+
<div className={styles.sectionLabel}>Tags</div>
|
|
75
|
+
<div className={styles.tags}>
|
|
76
|
+
{node.tags.map((tag) => (
|
|
77
|
+
<span key={tag} className={styles.tag}>{tag}</span>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{node.kind === "reference" && node.sourceId && (
|
|
84
|
+
<div className={styles.section}>
|
|
85
|
+
<button className={styles.viewLink} onClick={handleViewInGrackle}>
|
|
86
|
+
View in Grackle →
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{edges.length > 0 && (
|
|
92
|
+
<div className={styles.section}>
|
|
93
|
+
<div className={styles.sectionLabel}>Edges ({edges.length})</div>
|
|
94
|
+
<ul className={styles.edgeList}>
|
|
95
|
+
{edges.map((edge) => {
|
|
96
|
+
const otherId: string = edge.fromId === node.id ? edge.toId : edge.fromId;
|
|
97
|
+
const edgeKey: string = `${edge.fromId}:${edge.toId}:${edge.type}`;
|
|
98
|
+
return (
|
|
99
|
+
<li key={edgeKey} className={styles.edgeItem} data-testid="edge-item">
|
|
100
|
+
<span className={styles.edgeType} data-testid="edge-type">{edge.type}</span>
|
|
101
|
+
<button
|
|
102
|
+
className={styles.edgeNodeLink}
|
|
103
|
+
data-testid="edge-node-link"
|
|
104
|
+
onClick={() => { onSelectNode(otherId); }}
|
|
105
|
+
>
|
|
106
|
+
{nodeById.get(otherId)?.label ?? `${otherId.substring(0, 8)}...`}
|
|
107
|
+
</button>
|
|
108
|
+
</li>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</ul>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
<div className={styles.timestamps}>
|
|
116
|
+
{node.createdAt && <div>Created: {new Date(node.createdAt).toLocaleDateString()}</div>}
|
|
117
|
+
{node.updatedAt && <div>Updated: {new Date(node.updatedAt).toLocaleDateString()}</div>}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|