datajunction-ui 0.0.1-rc.9 → 0.0.2-0.dev1
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/.env +2 -0
- package/.prettierignore +3 -1
- package/Makefile +9 -0
- package/cleanup-deps.sh +70 -0
- package/dj-logo.svg +10 -0
- package/package.json +53 -14
- package/public/favicon.ico +0 -0
- package/public/index.html +1 -1
- package/runit.sh +30 -0
- package/runit2.sh +30 -0
- package/src/__tests__/reportWebVitals.test.jsx +44 -0
- package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -109
- package/src/app/components/AddNodeDropdown.jsx +44 -0
- package/src/app/components/ListGroupItem.jsx +9 -1
- package/src/app/components/NamespaceHeader.jsx +4 -13
- package/src/app/components/NodeListActions.jsx +69 -0
- package/src/app/components/NodeMaterializationDelete.jsx +90 -0
- package/src/app/components/NotificationBell.tsx +223 -0
- package/src/app/components/QueryInfo.jsx +172 -0
- package/src/app/components/Search.jsx +94 -0
- package/src/app/components/Tab.jsx +8 -1
- package/src/app/components/ToggleSwitch.jsx +20 -0
- package/src/app/components/UserMenu.tsx +100 -0
- package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
- package/src/app/components/__tests__/NodeMaterializationDelete.test.jsx +263 -0
- package/src/app/components/__tests__/NotificationBell.test.tsx +302 -0
- package/src/app/components/__tests__/QueryInfo.test.jsx +183 -0
- package/src/app/components/__tests__/Search.test.jsx +307 -0
- package/src/app/components/__tests__/Tab.test.jsx +27 -0
- package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
- package/src/app/components/__tests__/UserMenu.test.tsx +241 -0
- package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +8 -3
- package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
- package/src/app/components/djgraph/Collapse.jsx +47 -0
- package/src/app/components/djgraph/DJNode.jsx +61 -83
- package/src/app/components/djgraph/DJNodeColumns.jsx +75 -0
- package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
- package/src/app/components/djgraph/LayoutFlow.jsx +106 -0
- package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
- package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
- package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
- package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
- package/src/app/components/forms/Action.jsx +8 -0
- package/src/app/components/forms/NodeNameField.jsx +64 -0
- package/src/app/components/search.css +17 -0
- package/src/app/constants.js +2 -0
- package/src/app/icons/AddItemIcon.jsx +16 -0
- package/src/app/icons/AlertIcon.jsx +33 -0
- package/src/app/icons/CollapsedIcon.jsx +15 -0
- package/src/app/icons/CommitIcon.jsx +45 -0
- package/src/app/icons/DJLogo.jsx +36 -0
- package/src/app/icons/DeleteIcon.jsx +21 -0
- package/src/app/icons/DiffIcon.jsx +63 -0
- package/src/app/icons/EditIcon.jsx +18 -0
- package/src/app/icons/ExpandedIcon.jsx +15 -0
- package/src/app/icons/EyeIcon.jsx +20 -0
- package/src/app/icons/FilterIcon.jsx +7 -0
- package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
- package/src/app/icons/InvalidIcon.jsx +16 -0
- package/src/app/icons/JupyterExportIcon.jsx +25 -0
- package/src/app/icons/LoadingIcon.jsx +14 -0
- package/src/app/icons/NodeIcon.jsx +49 -0
- package/src/app/icons/NotificationIcon.jsx +27 -0
- package/src/app/icons/PythonIcon.jsx +14 -0
- package/src/app/icons/SettingsIcon.jsx +28 -0
- package/src/app/icons/TableIcon.jsx +14 -0
- package/src/app/icons/ValidIcon.jsx +16 -0
- package/src/app/icons/WrenchIcon.jsx +36 -0
- package/src/app/index.tsx +130 -37
- package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
- package/src/app/pages/AddEditNodePage/ColumnMetadata.jsx +61 -0
- package/src/app/pages/AddEditNodePage/ColumnsMetadataInput.jsx +72 -0
- package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
- package/src/app/pages/AddEditNodePage/CustomMetadataField.jsx +144 -0
- package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
- package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
- package/src/app/pages/AddEditNodePage/ExperimentationExtension.jsx +338 -0
- package/src/app/pages/AddEditNodePage/FormikSelect.jsx +64 -0
- package/src/app/pages/AddEditNodePage/FullNameField.jsx +38 -0
- package/src/app/pages/AddEditNodePage/Loadable.jsx +20 -0
- package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
- package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
- package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
- package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
- package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +94 -0
- package/src/app/pages/AddEditNodePage/OwnersField.jsx +54 -0
- package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
- package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
- package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +110 -0
- package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +291 -0
- package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
- package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
- package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +54 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +3 -0
- package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +224 -0
- package/src/app/pages/AddEditNodePage/index.jsx +545 -0
- package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
- package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
- package/src/app/pages/AddEditTagPage/index.jsx +132 -0
- package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +152 -0
- package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
- package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +75 -0
- package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +373 -0
- package/src/app/pages/CubeBuilderPage/index.jsx +291 -0
- package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
- package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
- package/src/app/pages/LoginPage/__tests__/index.test.jsx +97 -0
- package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
- package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
- package/src/app/pages/LoginPage/index.jsx +17 -0
- package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
- package/src/app/pages/NamespacePage/Explorer.jsx +232 -0
- package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
- package/src/app/pages/NamespacePage/NodeModeSelect.jsx +27 -0
- package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
- package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
- package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
- package/src/app/pages/NamespacePage/__tests__/AddNamespacePopover.test.jsx +283 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +331 -0
- package/src/app/pages/NamespacePage/index.jsx +356 -42
- package/src/app/pages/NodePage/AddBackfillPopover.jsx +165 -0
- package/src/app/pages/NodePage/AddComplexDimensionLinkPopover.jsx +367 -0
- package/src/app/pages/NodePage/AddMaterializationPopover.jsx +222 -0
- package/src/app/pages/NodePage/AvailabilityStateBlock.jsx +67 -0
- package/src/app/pages/NodePage/ClientCodePopover.jsx +94 -0
- package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
- package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
- package/src/app/pages/NodePage/EditColumnPopover.jsx +116 -0
- package/src/app/pages/NodePage/LinkDimensionPopover.jsx +164 -0
- package/src/app/pages/NodePage/ManageDimensionLinksDialog.jsx +526 -0
- package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
- package/src/app/pages/NodePage/NodeColumnTab.jsx +421 -30
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
- package/src/app/pages/NodePage/NodeGraphTab.jsx +119 -148
- package/src/app/pages/NodePage/NodeHistory.jsx +236 -0
- package/src/app/pages/NodePage/NodeInfoTab.jsx +346 -49
- package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +585 -0
- package/src/app/pages/NodePage/NodeRevisionMaterializationTab.jsx +58 -0
- package/src/app/pages/NodePage/NodeStatus.jsx +100 -31
- package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
- package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
- package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
- package/src/app/pages/NodePage/PartitionColumnPopover.jsx +151 -0
- package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
- package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
- package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
- package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +56 -0
- package/src/app/pages/NodePage/__tests__/AddComplexDimensionLinkPopover.test.jsx +459 -0
- package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
- package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
- package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
- package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +144 -0
- package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +132 -0
- package/src/app/pages/NodePage/__tests__/ManageDimensionLinksDialog.test.jsx +390 -0
- package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
- package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +595 -0
- package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +58 -0
- package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +190 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +882 -0
- package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
- package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +19 -0
- package/src/app/pages/NodePage/index.jsx +190 -44
- package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
- package/src/app/pages/NotificationsPage/Loadable.jsx +6 -0
- package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +287 -0
- package/src/app/pages/NotificationsPage/index.jsx +136 -0
- package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
- package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
- package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
- package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
- package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
- package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
- package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
- package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
- package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
- package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
- package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
- package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
- package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
- package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
- package/src/app/pages/OverviewPage/index.jsx +22 -0
- package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
- package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +112 -0
- package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +38 -0
- package/src/app/pages/RegisterTablePage/index.jsx +142 -0
- package/src/app/pages/Root/__tests__/index.test.jsx +44 -0
- package/src/app/pages/Root/index.tsx +92 -10
- package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
- package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
- package/src/app/pages/SettingsPage/CreateServiceAccountModal.jsx +152 -0
- package/src/app/pages/SettingsPage/Loadable.jsx +16 -0
- package/src/app/pages/SettingsPage/NotificationSubscriptionsSection.jsx +189 -0
- package/src/app/pages/SettingsPage/ProfileSection.jsx +41 -0
- package/src/app/pages/SettingsPage/ServiceAccountsSection.jsx +95 -0
- package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +318 -0
- package/src/app/pages/SettingsPage/__tests__/NotificationSubscriptionsSection.test.jsx +233 -0
- package/src/app/pages/SettingsPage/__tests__/ProfileSection.test.jsx +65 -0
- package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +150 -0
- package/src/app/pages/SettingsPage/__tests__/index.test.jsx +184 -0
- package/src/app/pages/SettingsPage/index.jsx +148 -0
- package/src/app/pages/TagPage/Loadable.jsx +16 -0
- package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
- package/src/app/pages/TagPage/index.jsx +79 -0
- package/src/app/services/DJService.js +1444 -21
- package/src/app/services/__tests__/DJService.test.jsx +2118 -0
- package/src/app/utils/__tests__/date.test.js +198 -0
- package/src/app/utils/date.js +65 -0
- package/src/index.tsx +1 -0
- package/src/mocks/mockNodes.jsx +1477 -0
- package/src/setupTests.ts +31 -1
- package/src/styles/dag.css +117 -5
- package/src/styles/index.css +1028 -31
- package/src/styles/loading.css +34 -0
- package/src/styles/login.css +81 -0
- package/src/styles/nav-bar.css +274 -0
- package/src/styles/node-creation.scss +276 -0
- package/src/styles/node-list.css +4 -0
- package/src/styles/overview.css +72 -0
- package/src/styles/settings.css +787 -0
- package/src/styles/sorted-table.css +15 -0
- package/src/styles/styles.scss +44 -0
- package/src/styles/styles.scss.d.ts +9 -0
- package/src/utils/form.jsx +23 -0
- package/webpack.config.js +17 -6
- package/.babelrc +0 -4
- package/.env.local +0 -4
- package/.env.production +0 -1
- package/.github/pull_request_template.md +0 -11
- package/.github/workflows/ci.yml +0 -33
- package/.vscode/extensions.json +0 -7
- package/.vscode/launch.json +0 -15
- package/.vscode/settings.json +0 -25
- package/Dockerfile +0 -7
- package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
- package/src/app/pages/ListNamespacesPage/index.jsx +0 -62
- package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
- package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import { ServiceAccountsSection } from '../ServiceAccountsSection';
|
|
4
|
+
|
|
5
|
+
describe('ServiceAccountsSection', () => {
|
|
6
|
+
const mockOnCreate = jest.fn();
|
|
7
|
+
const mockOnDelete = jest.fn();
|
|
8
|
+
|
|
9
|
+
const mockAccounts = [
|
|
10
|
+
{
|
|
11
|
+
id: 1,
|
|
12
|
+
name: 'my-pipeline',
|
|
13
|
+
client_id: 'abc-123-xyz',
|
|
14
|
+
created_at: '2024-12-01T00:00:00Z',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 2,
|
|
18
|
+
name: 'etl-job',
|
|
19
|
+
client_id: 'def-456-uvw',
|
|
20
|
+
created_at: '2024-12-02T00:00:00Z',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders empty state when no accounts', () => {
|
|
29
|
+
render(
|
|
30
|
+
<ServiceAccountsSection
|
|
31
|
+
accounts={[]}
|
|
32
|
+
onCreate={mockOnCreate}
|
|
33
|
+
onDelete={mockOnDelete}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(screen.getByText(/No service accounts yet/i)).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders accounts list', () => {
|
|
41
|
+
render(
|
|
42
|
+
<ServiceAccountsSection
|
|
43
|
+
accounts={mockAccounts}
|
|
44
|
+
onCreate={mockOnCreate}
|
|
45
|
+
onDelete={mockOnDelete}
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(screen.getByText('my-pipeline')).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByText('etl-job')).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByText('abc-123-xyz')).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByText('def-456-uvw')).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('renders section title and create button', () => {
|
|
56
|
+
render(
|
|
57
|
+
<ServiceAccountsSection
|
|
58
|
+
accounts={[]}
|
|
59
|
+
onCreate={mockOnCreate}
|
|
60
|
+
onDelete={mockOnDelete}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(screen.getByText('Service Accounts')).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByText('+ Create')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('opens create modal when create button is clicked', () => {
|
|
69
|
+
render(
|
|
70
|
+
<ServiceAccountsSection
|
|
71
|
+
accounts={[]}
|
|
72
|
+
onCreate={mockOnCreate}
|
|
73
|
+
onDelete={mockOnDelete}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
fireEvent.click(screen.getByText('+ Create'));
|
|
78
|
+
|
|
79
|
+
expect(screen.getByText('Create Service Account')).toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('calls onDelete when delete is confirmed', async () => {
|
|
83
|
+
window.confirm = jest.fn().mockReturnValue(true);
|
|
84
|
+
mockOnDelete.mockResolvedValue();
|
|
85
|
+
|
|
86
|
+
render(
|
|
87
|
+
<ServiceAccountsSection
|
|
88
|
+
accounts={mockAccounts}
|
|
89
|
+
onCreate={mockOnCreate}
|
|
90
|
+
onDelete={mockOnDelete}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const deleteButtons = screen.getAllByTitle('Delete service account');
|
|
95
|
+
fireEvent.click(deleteButtons[0]);
|
|
96
|
+
|
|
97
|
+
expect(window.confirm).toHaveBeenCalledWith(
|
|
98
|
+
'Delete service account "my-pipeline"?\n\nThis will revoke all access for this account and cannot be undone.',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await waitFor(() => {
|
|
102
|
+
expect(mockOnDelete).toHaveBeenCalledWith('abc-123-xyz');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('does not call onDelete when delete is cancelled', () => {
|
|
107
|
+
window.confirm = jest.fn().mockReturnValue(false);
|
|
108
|
+
|
|
109
|
+
render(
|
|
110
|
+
<ServiceAccountsSection
|
|
111
|
+
accounts={mockAccounts}
|
|
112
|
+
onCreate={mockOnCreate}
|
|
113
|
+
onDelete={mockOnDelete}
|
|
114
|
+
/>,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const deleteButtons = screen.getAllByTitle('Delete service account');
|
|
118
|
+
fireEvent.click(deleteButtons[0]);
|
|
119
|
+
|
|
120
|
+
expect(mockOnDelete).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('renders description text', () => {
|
|
124
|
+
render(
|
|
125
|
+
<ServiceAccountsSection
|
|
126
|
+
accounts={[]}
|
|
127
|
+
onCreate={mockOnCreate}
|
|
128
|
+
onDelete={mockOnDelete}
|
|
129
|
+
/>,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(
|
|
133
|
+
screen.getByText(/Service accounts allow programmatic access/i),
|
|
134
|
+
).toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('shows table headers when accounts exist', () => {
|
|
138
|
+
render(
|
|
139
|
+
<ServiceAccountsSection
|
|
140
|
+
accounts={mockAccounts}
|
|
141
|
+
onCreate={mockOnCreate}
|
|
142
|
+
onDelete={mockOnDelete}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
147
|
+
expect(screen.getByText('Client ID')).toBeInTheDocument();
|
|
148
|
+
expect(screen.getByText('Created')).toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { SettingsPage } from '../index';
|
|
4
|
+
import DJClientContext from '../../../providers/djclient';
|
|
5
|
+
|
|
6
|
+
describe('SettingsPage', () => {
|
|
7
|
+
const mockDjClient = {
|
|
8
|
+
whoami: jest.fn(),
|
|
9
|
+
getNotificationPreferences: jest.fn(),
|
|
10
|
+
getNodesByNames: jest.fn(),
|
|
11
|
+
listServiceAccounts: jest.fn(),
|
|
12
|
+
subscribeToNotifications: jest.fn(),
|
|
13
|
+
unsubscribeFromNotifications: jest.fn(),
|
|
14
|
+
createServiceAccount: jest.fn(),
|
|
15
|
+
deleteServiceAccount: jest.fn(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const renderWithContext = () => {
|
|
19
|
+
return render(
|
|
20
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
21
|
+
<SettingsPage />
|
|
22
|
+
</DJClientContext.Provider>,
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
|
|
29
|
+
// Default mock implementations
|
|
30
|
+
mockDjClient.whoami.mockResolvedValue({
|
|
31
|
+
username: 'testuser',
|
|
32
|
+
email: 'test@example.com',
|
|
33
|
+
name: 'Test User',
|
|
34
|
+
});
|
|
35
|
+
mockDjClient.getNotificationPreferences.mockResolvedValue([]);
|
|
36
|
+
mockDjClient.getNodesByNames.mockResolvedValue([]);
|
|
37
|
+
mockDjClient.listServiceAccounts.mockResolvedValue([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('shows loading state initially', () => {
|
|
41
|
+
// Make whoami hang
|
|
42
|
+
mockDjClient.whoami.mockImplementation(() => new Promise(() => {}));
|
|
43
|
+
|
|
44
|
+
renderWithContext();
|
|
45
|
+
|
|
46
|
+
// Should show loading icon (or some loading indicator)
|
|
47
|
+
expect(document.querySelector('.settings-page')).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('renders all sections after loading', async () => {
|
|
51
|
+
renderWithContext();
|
|
52
|
+
|
|
53
|
+
await waitFor(() => {
|
|
54
|
+
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(screen.getByText('Profile')).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByText('Notification Subscriptions')).toBeInTheDocument();
|
|
59
|
+
expect(screen.getByText('Service Accounts')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('fetches and displays user profile', async () => {
|
|
63
|
+
mockDjClient.whoami.mockResolvedValue({
|
|
64
|
+
username: 'alice',
|
|
65
|
+
email: 'alice@example.com',
|
|
66
|
+
name: 'Alice Smith',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
renderWithContext();
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(screen.getByText('alice')).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
|
|
76
|
+
expect(screen.getByText('AS')).toBeInTheDocument(); // initials
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('fetches and displays subscriptions', async () => {
|
|
80
|
+
mockDjClient.getNotificationPreferences.mockResolvedValue([
|
|
81
|
+
{
|
|
82
|
+
entity_name: 'default.my_metric',
|
|
83
|
+
entity_type: 'node',
|
|
84
|
+
activity_types: ['update'],
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
mockDjClient.getNodesByNames.mockResolvedValue([
|
|
89
|
+
{
|
|
90
|
+
name: 'default.my_metric',
|
|
91
|
+
type: 'METRIC',
|
|
92
|
+
current: {
|
|
93
|
+
displayName: 'My Metric',
|
|
94
|
+
status: 'VALID',
|
|
95
|
+
mode: 'PUBLISHED',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
renderWithContext();
|
|
101
|
+
|
|
102
|
+
await waitFor(() => {
|
|
103
|
+
expect(screen.getByText('default.my_metric')).toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('fetches and displays service accounts', async () => {
|
|
108
|
+
mockDjClient.listServiceAccounts.mockResolvedValue([
|
|
109
|
+
{
|
|
110
|
+
id: 1,
|
|
111
|
+
name: 'my-pipeline',
|
|
112
|
+
client_id: 'abc-123',
|
|
113
|
+
created_at: '2024-12-01T00:00:00Z',
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
renderWithContext();
|
|
118
|
+
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(screen.getByText('my-pipeline')).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText('abc-123')).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('handles service accounts API error gracefully', async () => {
|
|
127
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
128
|
+
mockDjClient.listServiceAccounts.mockRejectedValue(
|
|
129
|
+
new Error('Not available'),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
renderWithContext();
|
|
133
|
+
|
|
134
|
+
await waitFor(() => {
|
|
135
|
+
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Page should still render without service accounts
|
|
139
|
+
expect(screen.getByText(/No service accounts yet/i)).toBeInTheDocument();
|
|
140
|
+
|
|
141
|
+
consoleSpy.mockRestore();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('handles fetch error gracefully', async () => {
|
|
145
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
146
|
+
mockDjClient.whoami.mockRejectedValue(new Error('Network error'));
|
|
147
|
+
|
|
148
|
+
renderWithContext();
|
|
149
|
+
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
// Page should render even after error
|
|
152
|
+
expect(document.querySelector('.settings-page')).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
consoleSpy.mockRestore();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('enriches subscriptions with node info from GraphQL', async () => {
|
|
159
|
+
mockDjClient.getNotificationPreferences.mockResolvedValue([
|
|
160
|
+
{
|
|
161
|
+
entity_name: 'default.orders',
|
|
162
|
+
entity_type: 'node',
|
|
163
|
+
activity_types: ['update'],
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
mockDjClient.getNodesByNames.mockResolvedValue([
|
|
168
|
+
{
|
|
169
|
+
name: 'default.orders',
|
|
170
|
+
type: 'SOURCE',
|
|
171
|
+
current: {
|
|
172
|
+
displayName: 'Orders Table',
|
|
173
|
+
status: 'VALID',
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
renderWithContext();
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
expect(screen.getByText('SOURCE')).toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
4
|
+
import ProfileSection from './ProfileSection';
|
|
5
|
+
import NotificationSubscriptionsSection from './NotificationSubscriptionsSection';
|
|
6
|
+
import ServiceAccountsSection from './ServiceAccountsSection';
|
|
7
|
+
import '../../../styles/settings.css';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Main Settings page that orchestrates all settings sections.
|
|
11
|
+
*/
|
|
12
|
+
export function SettingsPage() {
|
|
13
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
14
|
+
const [currentUser, setCurrentUser] = useState(null);
|
|
15
|
+
const [subscriptions, setSubscriptions] = useState([]);
|
|
16
|
+
const [serviceAccounts, setServiceAccounts] = useState([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
async function fetchData() {
|
|
21
|
+
try {
|
|
22
|
+
// Fetch user profile
|
|
23
|
+
const user = await djClient.whoami();
|
|
24
|
+
setCurrentUser(user);
|
|
25
|
+
|
|
26
|
+
// Fetch notification subscriptions
|
|
27
|
+
const prefs = await djClient.getNotificationPreferences();
|
|
28
|
+
|
|
29
|
+
// Fetch node details for subscriptions via GraphQL
|
|
30
|
+
const nodeNames = (prefs || [])
|
|
31
|
+
.filter(p => p.entity_type === 'node')
|
|
32
|
+
.map(p => p.entity_name);
|
|
33
|
+
|
|
34
|
+
let nodeInfoMap = {};
|
|
35
|
+
if (nodeNames.length > 0) {
|
|
36
|
+
const nodes = await djClient.getNodesByNames(nodeNames);
|
|
37
|
+
nodeInfoMap = Object.fromEntries(
|
|
38
|
+
nodes.map(n => [
|
|
39
|
+
n.name,
|
|
40
|
+
{
|
|
41
|
+
node_type: n.type?.toLowerCase(),
|
|
42
|
+
display_name: n.current?.displayName,
|
|
43
|
+
status: n.current?.status?.toLowerCase(),
|
|
44
|
+
mode: n.current?.mode?.toLowerCase(),
|
|
45
|
+
},
|
|
46
|
+
]),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Merge node info into subscriptions
|
|
51
|
+
const enrichedPrefs = (prefs || []).map(pref => ({
|
|
52
|
+
...pref,
|
|
53
|
+
...(nodeInfoMap[pref.entity_name] || {}),
|
|
54
|
+
}));
|
|
55
|
+
setSubscriptions(enrichedPrefs);
|
|
56
|
+
|
|
57
|
+
// Fetch service accounts
|
|
58
|
+
try {
|
|
59
|
+
const accounts = await djClient.listServiceAccounts();
|
|
60
|
+
setServiceAccounts(accounts || []);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// Service accounts may not be available, ignore error
|
|
63
|
+
console.log('Service accounts not available:', err);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Error fetching settings data:', error);
|
|
67
|
+
} finally {
|
|
68
|
+
setLoading(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
fetchData();
|
|
72
|
+
}, [djClient]);
|
|
73
|
+
|
|
74
|
+
// Subscription handlers
|
|
75
|
+
const handleUpdateSubscription = async (sub, activityTypes) => {
|
|
76
|
+
await djClient.subscribeToNotifications({
|
|
77
|
+
entity_type: sub.entity_type,
|
|
78
|
+
entity_name: sub.entity_name,
|
|
79
|
+
activity_types: activityTypes,
|
|
80
|
+
alert_types: sub.alert_types || ['web'],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Update local state
|
|
84
|
+
setSubscriptions(
|
|
85
|
+
subscriptions.map(s =>
|
|
86
|
+
s.entity_name === sub.entity_name
|
|
87
|
+
? { ...s, activity_types: activityTypes }
|
|
88
|
+
: s,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleUnsubscribe = async sub => {
|
|
94
|
+
await djClient.unsubscribeFromNotifications({
|
|
95
|
+
entity_type: sub.entity_type,
|
|
96
|
+
entity_name: sub.entity_name,
|
|
97
|
+
});
|
|
98
|
+
setSubscriptions(
|
|
99
|
+
subscriptions.filter(s => s.entity_name !== sub.entity_name),
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Service account handlers
|
|
104
|
+
const handleCreateServiceAccount = async name => {
|
|
105
|
+
const result = await djClient.createServiceAccount(name);
|
|
106
|
+
if (result.client_id) {
|
|
107
|
+
setServiceAccounts([...serviceAccounts, result]);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleDeleteServiceAccount = async clientId => {
|
|
113
|
+
await djClient.deleteServiceAccount(clientId);
|
|
114
|
+
setServiceAccounts(serviceAccounts.filter(a => a.client_id !== clientId));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (loading) {
|
|
118
|
+
return (
|
|
119
|
+
<div className="settings-page">
|
|
120
|
+
<div className="settings-container">
|
|
121
|
+
<LoadingIcon />
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="settings-page">
|
|
129
|
+
<div className="settings-container">
|
|
130
|
+
<h1 className="settings-title">Settings</h1>
|
|
131
|
+
|
|
132
|
+
<ProfileSection user={currentUser} />
|
|
133
|
+
|
|
134
|
+
<NotificationSubscriptionsSection
|
|
135
|
+
subscriptions={subscriptions}
|
|
136
|
+
onUpdate={handleUpdateSubscription}
|
|
137
|
+
onUnsubscribe={handleUnsubscribe}
|
|
138
|
+
/>
|
|
139
|
+
|
|
140
|
+
<ServiceAccountsSection
|
|
141
|
+
accounts={serviceAccounts}
|
|
142
|
+
onCreate={handleCreateServiceAccount}
|
|
143
|
+
onDelete={handleDeleteServiceAccount}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronously loads the component for the Node page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { lazyLoad } from '../../../utils/loadable';
|
|
7
|
+
|
|
8
|
+
export const TagPage = () => {
|
|
9
|
+
return lazyLoad(
|
|
10
|
+
() => import('./index'),
|
|
11
|
+
module => module.TagPage,
|
|
12
|
+
{
|
|
13
|
+
fallback: <div></div>,
|
|
14
|
+
},
|
|
15
|
+
)();
|
|
16
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import fetchMock from 'jest-fetch-mock';
|
|
4
|
+
import { render } from '../../../../setupTests';
|
|
5
|
+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
6
|
+
import DJClientContext from '../../../providers/djclient';
|
|
7
|
+
import { TagPage } from '../index';
|
|
8
|
+
|
|
9
|
+
describe('<TagPage />', () => {
|
|
10
|
+
const initializeMockDJClient = () => {
|
|
11
|
+
return {
|
|
12
|
+
DataJunctionAPI: {
|
|
13
|
+
getTag: jest.fn(),
|
|
14
|
+
listNodesForTag: jest.fn(),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const mockDjClient = initializeMockDJClient();
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
fetchMock.resetMocks();
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
window.scrollTo = jest.fn();
|
|
25
|
+
|
|
26
|
+
mockDjClient.DataJunctionAPI.getTag.mockReturnValue({
|
|
27
|
+
name: 'domains.com',
|
|
28
|
+
tag_type: 'domains',
|
|
29
|
+
description: 'Top-level domain .com',
|
|
30
|
+
});
|
|
31
|
+
mockDjClient.DataJunctionAPI.listNodesForTag.mockReturnValue([
|
|
32
|
+
{
|
|
33
|
+
name: 'random.node_a',
|
|
34
|
+
type: 'metric',
|
|
35
|
+
display_name: 'Node A',
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const renderTagsPage = element => {
|
|
41
|
+
return render(
|
|
42
|
+
<MemoryRouter initialEntries={['/tags/:name']}>
|
|
43
|
+
<Routes>
|
|
44
|
+
<Route path="tags/:name" element={element} />
|
|
45
|
+
</Routes>
|
|
46
|
+
</MemoryRouter>,
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const testElement = djClient => {
|
|
51
|
+
return (
|
|
52
|
+
<DJClientContext.Provider value={djClient}>
|
|
53
|
+
<TagPage />
|
|
54
|
+
</DJClientContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
it('renders the tag page correctly', async () => {
|
|
59
|
+
const element = testElement(mockDjClient);
|
|
60
|
+
renderTagsPage(element);
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(mockDjClient.DataJunctionAPI.getTag).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(
|
|
64
|
+
mockDjClient.DataJunctionAPI.listNodesForTag,
|
|
65
|
+
).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
expect(screen.getByText('Nodes')).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByText('Node A')).toBeInTheDocument();
|
|
69
|
+
}, 60000);
|
|
70
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For a given tag, displays nodes tagged with it
|
|
3
|
+
*/
|
|
4
|
+
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
5
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
6
|
+
import 'styles/node-creation.scss';
|
|
7
|
+
import DJClientContext from '../../providers/djclient';
|
|
8
|
+
import { useParams } from 'react-router-dom';
|
|
9
|
+
|
|
10
|
+
export function TagPage() {
|
|
11
|
+
const [nodes, setNodes] = useState([]);
|
|
12
|
+
const [tag, setTag] = useState([]);
|
|
13
|
+
|
|
14
|
+
const { name } = useParams();
|
|
15
|
+
|
|
16
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const fetchData = async () => {
|
|
19
|
+
const data = await djClient.listNodesForTag(name);
|
|
20
|
+
const tagData = await djClient.getTag(name);
|
|
21
|
+
setNodes(data);
|
|
22
|
+
setTag(tagData);
|
|
23
|
+
};
|
|
24
|
+
fetchData().catch(console.error);
|
|
25
|
+
}, [djClient, name]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="mid">
|
|
29
|
+
<NamespaceHeader namespace="" />
|
|
30
|
+
<div className="card">
|
|
31
|
+
<div className="card-header">
|
|
32
|
+
<h3
|
|
33
|
+
className="card-title align-items-start flex-column"
|
|
34
|
+
style={{ display: 'inline-block' }}
|
|
35
|
+
>
|
|
36
|
+
Tag
|
|
37
|
+
</h3>
|
|
38
|
+
<div>
|
|
39
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
40
|
+
<h6 className="mb-0 w-100">Display Name</h6>
|
|
41
|
+
<p className="mb-0 opacity-75">{tag.display_name}</p>
|
|
42
|
+
</div>
|
|
43
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
44
|
+
<h6 className="mb-0 w-100">Name</h6>
|
|
45
|
+
<p className="mb-0 opacity-75">{name}</p>
|
|
46
|
+
</div>
|
|
47
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
48
|
+
<h6 className="mb-0 w-100">Tag Type</h6>
|
|
49
|
+
<p className="mb-0 opacity-75">{tag.tag_type}</p>
|
|
50
|
+
</div>
|
|
51
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
52
|
+
<h6 className="mb-0 w-100">Description</h6>
|
|
53
|
+
<p className="mb-0 opacity-75">{tag.description}</p>
|
|
54
|
+
</div>
|
|
55
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
56
|
+
<h6 className="mb-0 w-100">Nodes</h6>
|
|
57
|
+
<div className={`list-group-item`}>
|
|
58
|
+
{nodes?.map(node => (
|
|
59
|
+
<div
|
|
60
|
+
className="button-3 cube-element"
|
|
61
|
+
key={node.name}
|
|
62
|
+
role="cell"
|
|
63
|
+
aria-label="CubeElement"
|
|
64
|
+
aria-hidden="false"
|
|
65
|
+
>
|
|
66
|
+
<a href={`/nodes/${node.name}`}>{node.display_name}</a>
|
|
67
|
+
<span className={`badge node_type__${node.type}`}>
|
|
68
|
+
{node.type}
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|