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,287 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { NotificationsPage } from '../index';
|
|
4
|
+
import DJClientContext from '../../../providers/djclient';
|
|
5
|
+
|
|
6
|
+
describe('<NotificationsPage />', () => {
|
|
7
|
+
const mockNotifications = [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
entity_type: 'node',
|
|
11
|
+
entity_name: 'default.metrics.revenue',
|
|
12
|
+
node: 'default.metrics.revenue',
|
|
13
|
+
activity_type: 'update',
|
|
14
|
+
user: 'alice',
|
|
15
|
+
created_at: new Date().toISOString(),
|
|
16
|
+
details: { version: 'v2' },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 2,
|
|
20
|
+
entity_type: 'node',
|
|
21
|
+
entity_name: 'default.dimensions.country',
|
|
22
|
+
node: 'default.dimensions.country',
|
|
23
|
+
activity_type: 'create',
|
|
24
|
+
user: 'bob',
|
|
25
|
+
created_at: new Date().toISOString(),
|
|
26
|
+
details: { version: 'v1' },
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const mockNodes = [
|
|
31
|
+
{
|
|
32
|
+
name: 'default.metrics.revenue',
|
|
33
|
+
type: 'metric',
|
|
34
|
+
current: { displayName: 'Revenue Metric' },
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'default.dimensions.country',
|
|
38
|
+
type: 'dimension',
|
|
39
|
+
current: { displayName: 'Country' },
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const createMockDjClient = (overrides = {}) => ({
|
|
44
|
+
getSubscribedHistory: jest.fn().mockResolvedValue(mockNotifications),
|
|
45
|
+
getNodesByNames: jest.fn().mockResolvedValue(mockNodes),
|
|
46
|
+
...overrides,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const renderWithContext = mockDjClient => {
|
|
50
|
+
return render(
|
|
51
|
+
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
|
|
52
|
+
<NotificationsPage />
|
|
53
|
+
</DJClientContext.Provider>,
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
jest.clearAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('renders the page title', async () => {
|
|
62
|
+
const mockDjClient = createMockDjClient();
|
|
63
|
+
renderWithContext(mockDjClient);
|
|
64
|
+
|
|
65
|
+
expect(screen.getByText('Notifications')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('shows loading state initially', () => {
|
|
69
|
+
const mockDjClient = createMockDjClient({
|
|
70
|
+
getSubscribedHistory: jest.fn().mockImplementation(
|
|
71
|
+
() => new Promise(() => {}), // Never resolves
|
|
72
|
+
),
|
|
73
|
+
});
|
|
74
|
+
renderWithContext(mockDjClient);
|
|
75
|
+
|
|
76
|
+
// LoadingIcon should be present (check for the container)
|
|
77
|
+
const loadingContainer = document.querySelector(
|
|
78
|
+
'[style*="text-align: center"]',
|
|
79
|
+
);
|
|
80
|
+
expect(loadingContainer).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('shows empty state when no notifications', async () => {
|
|
84
|
+
const mockDjClient = createMockDjClient({
|
|
85
|
+
getSubscribedHistory: jest.fn().mockResolvedValue([]),
|
|
86
|
+
});
|
|
87
|
+
renderWithContext(mockDjClient);
|
|
88
|
+
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(screen.getByText(/No notifications yet/i)).toBeInTheDocument();
|
|
94
|
+
expect(
|
|
95
|
+
screen.getByText(/Watch nodes to receive updates/i),
|
|
96
|
+
).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('displays notifications with display names', async () => {
|
|
100
|
+
const mockDjClient = createMockDjClient();
|
|
101
|
+
renderWithContext(mockDjClient);
|
|
102
|
+
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Display names should be shown
|
|
108
|
+
expect(await screen.findByText('Revenue Metric')).toBeInTheDocument();
|
|
109
|
+
expect(await screen.findByText('Country')).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('displays entity names below display names', async () => {
|
|
113
|
+
const mockDjClient = createMockDjClient();
|
|
114
|
+
renderWithContext(mockDjClient);
|
|
115
|
+
|
|
116
|
+
await waitFor(() => {
|
|
117
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Entity names should be shown
|
|
121
|
+
expect(
|
|
122
|
+
await screen.findByText('default.metrics.revenue'),
|
|
123
|
+
).toBeInTheDocument();
|
|
124
|
+
expect(
|
|
125
|
+
await screen.findByText('default.dimensions.country'),
|
|
126
|
+
).toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('falls back to entity_name when no display_name', async () => {
|
|
130
|
+
const mockDjClient = createMockDjClient({
|
|
131
|
+
getNodesByNames: jest.fn().mockResolvedValue([]), // No node info
|
|
132
|
+
});
|
|
133
|
+
renderWithContext(mockDjClient);
|
|
134
|
+
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Entity names should be shown as the title (no display names)
|
|
140
|
+
const revenueElements = await screen.findAllByText(
|
|
141
|
+
'default.metrics.revenue',
|
|
142
|
+
);
|
|
143
|
+
expect(revenueElements.length).toBeGreaterThan(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('shows version badge when version is available', async () => {
|
|
147
|
+
const mockDjClient = createMockDjClient();
|
|
148
|
+
renderWithContext(mockDjClient);
|
|
149
|
+
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(await screen.findByText('v2')).toBeInTheDocument();
|
|
155
|
+
expect(await screen.findByText('v1')).toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('links to revision page when version is available', async () => {
|
|
159
|
+
const mockDjClient = createMockDjClient();
|
|
160
|
+
renderWithContext(mockDjClient);
|
|
161
|
+
|
|
162
|
+
await waitFor(() => {
|
|
163
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await waitFor(() => {
|
|
167
|
+
const links = document.querySelectorAll('a.notification-item');
|
|
168
|
+
expect(links.length).toBe(2);
|
|
169
|
+
|
|
170
|
+
const revenueLink = Array.from(links).find(l =>
|
|
171
|
+
l.textContent.includes('Revenue Metric'),
|
|
172
|
+
);
|
|
173
|
+
expect(revenueLink).toHaveAttribute(
|
|
174
|
+
'href',
|
|
175
|
+
'/nodes/default.metrics.revenue/revisions/v2',
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('links to history page when no version', async () => {
|
|
181
|
+
const mockDjClient = createMockDjClient({
|
|
182
|
+
getSubscribedHistory: jest.fn().mockResolvedValue([
|
|
183
|
+
{
|
|
184
|
+
id: 1,
|
|
185
|
+
entity_type: 'node',
|
|
186
|
+
entity_name: 'default.source.orders',
|
|
187
|
+
node: 'default.source.orders',
|
|
188
|
+
activity_type: 'update',
|
|
189
|
+
user: 'alice',
|
|
190
|
+
created_at: new Date().toISOString(),
|
|
191
|
+
details: {}, // No version
|
|
192
|
+
},
|
|
193
|
+
]),
|
|
194
|
+
getNodesByNames: jest.fn().mockResolvedValue([]),
|
|
195
|
+
});
|
|
196
|
+
renderWithContext(mockDjClient);
|
|
197
|
+
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await waitFor(() => {
|
|
203
|
+
const link = document.querySelector('a.notification-item');
|
|
204
|
+
expect(link).toHaveAttribute(
|
|
205
|
+
'href',
|
|
206
|
+
'/nodes/default.source.orders/history',
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('shows node type badge', async () => {
|
|
212
|
+
const mockDjClient = createMockDjClient();
|
|
213
|
+
renderWithContext(mockDjClient);
|
|
214
|
+
|
|
215
|
+
await waitFor(() => {
|
|
216
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(await screen.findByText('METRIC')).toBeInTheDocument();
|
|
220
|
+
expect(await screen.findByText('DIMENSION')).toBeInTheDocument();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('shows activity type and user', async () => {
|
|
224
|
+
const mockDjClient = createMockDjClient();
|
|
225
|
+
renderWithContext(mockDjClient);
|
|
226
|
+
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(await screen.findByText('alice')).toBeInTheDocument();
|
|
232
|
+
expect(await screen.findByText('bob')).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('groups notifications by date', async () => {
|
|
236
|
+
const mockDjClient = createMockDjClient();
|
|
237
|
+
renderWithContext(mockDjClient);
|
|
238
|
+
|
|
239
|
+
await waitFor(() => {
|
|
240
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Both notifications are from today
|
|
244
|
+
expect(await screen.findByText('Today')).toBeInTheDocument();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('fetches node info via GraphQL', async () => {
|
|
248
|
+
const mockDjClient = createMockDjClient();
|
|
249
|
+
renderWithContext(mockDjClient);
|
|
250
|
+
|
|
251
|
+
await waitFor(() => {
|
|
252
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalledWith(50);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await waitFor(() => {
|
|
256
|
+
expect(mockDjClient.getNodesByNames).toHaveBeenCalledWith([
|
|
257
|
+
'default.metrics.revenue',
|
|
258
|
+
'default.dimensions.country',
|
|
259
|
+
]);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('handles errors gracefully', async () => {
|
|
264
|
+
const consoleSpy = jest
|
|
265
|
+
.spyOn(console, 'error')
|
|
266
|
+
.mockImplementation(() => {});
|
|
267
|
+
|
|
268
|
+
const mockDjClient = createMockDjClient({
|
|
269
|
+
getSubscribedHistory: jest
|
|
270
|
+
.fn()
|
|
271
|
+
.mockRejectedValue(new Error('Network error')),
|
|
272
|
+
});
|
|
273
|
+
renderWithContext(mockDjClient);
|
|
274
|
+
|
|
275
|
+
await waitFor(() => {
|
|
276
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
277
|
+
'Error fetching notifications:',
|
|
278
|
+
expect.any(Error),
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Should show empty state after error
|
|
283
|
+
expect(screen.getByText(/No notifications yet/i)).toBeInTheDocument();
|
|
284
|
+
|
|
285
|
+
consoleSpy.mockRestore();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
4
|
+
import { formatRelativeTime, groupByDate } from '../../utils/date';
|
|
5
|
+
|
|
6
|
+
// Enrich history entries with node info from GraphQL
|
|
7
|
+
const enrichWithNodeInfo = (entries, nodes) => {
|
|
8
|
+
const nodeMap = new Map(nodes.map(n => [n.name, n]));
|
|
9
|
+
return entries.map(entry => {
|
|
10
|
+
const node = nodeMap.get(entry.entity_name);
|
|
11
|
+
return {
|
|
12
|
+
...entry,
|
|
13
|
+
node_type: node?.type,
|
|
14
|
+
display_name: node?.current?.displayName,
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function NotificationsPage() {
|
|
20
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
21
|
+
const [notifications, setNotifications] = useState([]);
|
|
22
|
+
const [loading, setLoading] = useState(true);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
async function fetchNotifications() {
|
|
26
|
+
try {
|
|
27
|
+
const history = (await djClient.getSubscribedHistory(50)) || [];
|
|
28
|
+
|
|
29
|
+
// Get unique entity names and fetch their info via GraphQL
|
|
30
|
+
const nodeNames = Array.from(new Set(history.map(h => h.entity_name)));
|
|
31
|
+
const nodes = nodeNames.length
|
|
32
|
+
? await djClient.getNodesByNames(nodeNames)
|
|
33
|
+
: [];
|
|
34
|
+
|
|
35
|
+
const enriched = enrichWithNodeInfo(history, nodes);
|
|
36
|
+
setNotifications(enriched);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error fetching notifications:', error);
|
|
39
|
+
} finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
fetchNotifications();
|
|
44
|
+
}, [djClient]);
|
|
45
|
+
|
|
46
|
+
const groupedNotifications = groupByDate(notifications);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="mid">
|
|
50
|
+
<div className="card">
|
|
51
|
+
<div className="card-header">
|
|
52
|
+
<h2>Notifications</h2>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="card-body">
|
|
55
|
+
<div className="notifications-list">
|
|
56
|
+
{loading ? (
|
|
57
|
+
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
58
|
+
<LoadingIcon />
|
|
59
|
+
</div>
|
|
60
|
+
) : notifications.length === 0 ? (
|
|
61
|
+
<div
|
|
62
|
+
style={{
|
|
63
|
+
padding: '2rem 1rem',
|
|
64
|
+
color: '#666',
|
|
65
|
+
textAlign: 'center',
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
No notifications yet. Watch nodes to receive updates when they
|
|
69
|
+
change.
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
groupedNotifications.map(group => (
|
|
73
|
+
<div key={group.label} className="notification-group">
|
|
74
|
+
<div
|
|
75
|
+
style={{
|
|
76
|
+
padding: '0.5rem 0.75rem',
|
|
77
|
+
backgroundColor: '#f8f9fa',
|
|
78
|
+
borderBottom: '1px solid #eee',
|
|
79
|
+
fontSize: '12px',
|
|
80
|
+
fontWeight: 600,
|
|
81
|
+
color: '#666',
|
|
82
|
+
textTransform: 'uppercase',
|
|
83
|
+
letterSpacing: '0.5px',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{group.label}
|
|
87
|
+
</div>
|
|
88
|
+
{group.items.map(entry => {
|
|
89
|
+
const version = entry.details?.version;
|
|
90
|
+
const href = version
|
|
91
|
+
? `/nodes/${entry.entity_name}/revisions/${version}`
|
|
92
|
+
: `/nodes/${entry.entity_name}/history`;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<a
|
|
96
|
+
key={entry.id}
|
|
97
|
+
className="notification-item"
|
|
98
|
+
href={href}
|
|
99
|
+
>
|
|
100
|
+
<span className="notification-node">
|
|
101
|
+
<span className="notification-title">
|
|
102
|
+
{entry.display_name || entry.entity_name}
|
|
103
|
+
{version && (
|
|
104
|
+
<span className="badge version">{version}</span>
|
|
105
|
+
)}
|
|
106
|
+
</span>
|
|
107
|
+
{entry.display_name && (
|
|
108
|
+
<span className="notification-entity">
|
|
109
|
+
{entry.entity_name}
|
|
110
|
+
</span>
|
|
111
|
+
)}
|
|
112
|
+
</span>
|
|
113
|
+
<span className="notification-meta">
|
|
114
|
+
{entry.node_type && (
|
|
115
|
+
<span
|
|
116
|
+
className={`node_type__${entry.node_type} badge node_type`}
|
|
117
|
+
>
|
|
118
|
+
{entry.node_type.toUpperCase()}
|
|
119
|
+
</span>
|
|
120
|
+
)}
|
|
121
|
+
{entry.activity_type}d by{' '}
|
|
122
|
+
<span style={{ color: '#333' }}>{entry.user}</span> ·{' '}
|
|
123
|
+
{formatRelativeTime(entry.created_at)}
|
|
124
|
+
</span>
|
|
125
|
+
</a>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
128
|
+
</div>
|
|
129
|
+
))
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
|
|
4
|
+
import ValidIcon from '../../icons/ValidIcon';
|
|
5
|
+
import InvalidIcon from '../../icons/InvalidIcon';
|
|
6
|
+
|
|
7
|
+
const COLOR_MAPPING = {
|
|
8
|
+
valid: '#00b368',
|
|
9
|
+
invalid: '#FF91A3', // '#b34b00',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const ByStatusPanel = () => {
|
|
13
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
14
|
+
const [nodesByStatus, setNodesByStatus] = useState(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const fetchData = async () => {
|
|
18
|
+
setNodesByStatus(await djClient.system.node_counts_by_status());
|
|
19
|
+
};
|
|
20
|
+
fetchData().catch(console.error);
|
|
21
|
+
}, [djClient]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<div className="chart-box" style={{ flex: '0 0 2%' }}>
|
|
26
|
+
<div className="horiz-box">
|
|
27
|
+
<div className="chart-title">Nodes By Status</div>
|
|
28
|
+
{nodesByStatus?.map(entry => (
|
|
29
|
+
<div
|
|
30
|
+
className="jss316 badge"
|
|
31
|
+
style={{ color: '#000', margin: '0.2em' }}
|
|
32
|
+
key={entry.name}
|
|
33
|
+
>
|
|
34
|
+
<span style={{ color: COLOR_MAPPING[entry.name.toLowerCase()] }}>
|
|
35
|
+
{entry.name === 'VALID' ? (
|
|
36
|
+
<ValidIcon
|
|
37
|
+
width={'45px'}
|
|
38
|
+
height={'45px'}
|
|
39
|
+
style={{ marginTop: '0.75em' }}
|
|
40
|
+
/>
|
|
41
|
+
) : (
|
|
42
|
+
<InvalidIcon
|
|
43
|
+
width={'45px'}
|
|
44
|
+
height={'45px'}
|
|
45
|
+
style={{ marginTop: '0.75em' }}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
</span>
|
|
49
|
+
|
|
50
|
+
<div style={{ display: 'inline-grid', alignItems: 'center' }}>
|
|
51
|
+
<strong
|
|
52
|
+
className="horiz-box-value"
|
|
53
|
+
style={{
|
|
54
|
+
color: COLOR_MAPPING[entry.name.toLowerCase()],
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{entry.value}
|
|
58
|
+
</strong>
|
|
59
|
+
<span className={'horiz-box-label'}>
|
|
60
|
+
{entry.name.toLowerCase()} nodes
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
|
|
4
|
+
export const DimensionNodeUsagePanel = () => {
|
|
5
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
6
|
+
const [dimensionNodes, setDimensionNodes] = useState(null);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const fetchData = async () => {
|
|
10
|
+
setDimensionNodes(await djClient.system.dimensions());
|
|
11
|
+
};
|
|
12
|
+
fetchData().catch(console.error);
|
|
13
|
+
}, [djClient]);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<div className="chart-box">
|
|
18
|
+
<div className="chart-title">Dimension Node Usage</div>
|
|
19
|
+
<table className="card-inner-table table" style={{ marginTop: '0' }}>
|
|
20
|
+
<thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
|
|
21
|
+
<tr>
|
|
22
|
+
<th className="text-start">Dimension</th>
|
|
23
|
+
<th className="a">Links</th>
|
|
24
|
+
<th className="a">Cubes</th>
|
|
25
|
+
</tr>
|
|
26
|
+
</thead>
|
|
27
|
+
<tbody>
|
|
28
|
+
{dimensionNodes
|
|
29
|
+
?.sort(
|
|
30
|
+
(a, b) =>
|
|
31
|
+
b.cube_count + b.indegree - (a.cube_count + a.indegree),
|
|
32
|
+
)
|
|
33
|
+
.slice(0, 6)
|
|
34
|
+
.map((dim, index) => (
|
|
35
|
+
<tr key={index}>
|
|
36
|
+
<td className="a">
|
|
37
|
+
<a href={`/nodes/${dim.name}`}>{dim.name}</a>
|
|
38
|
+
</td>
|
|
39
|
+
<td className="a">{dim.indegree}</td>
|
|
40
|
+
<td className="a">{dim.cube_count}</td>
|
|
41
|
+
</tr>
|
|
42
|
+
))}
|
|
43
|
+
</tbody>
|
|
44
|
+
</table>
|
|
45
|
+
</div>
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import DJClientContext from '../../providers/djclient';
|
|
3
|
+
import '../../../styles/node-creation.scss';
|
|
4
|
+
const COLOR_MAPPING = {
|
|
5
|
+
source: '#00C49F',
|
|
6
|
+
dimension: '#FFBB28', //'#FF8042',
|
|
7
|
+
transform: '#0088FE',
|
|
8
|
+
metric: '#ff91a3', //'#FFBB28',
|
|
9
|
+
cube: '#AA46BE',
|
|
10
|
+
valid: '#00b368',
|
|
11
|
+
invalid: '#b34b00',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const GovernanceWarningsPanel = () => {
|
|
15
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
16
|
+
const [nodesWithoutDescription, setNodesWithoutDescription] = useState(null);
|
|
17
|
+
const [dimensionNodes, setDimensionNodes] = useState(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const fetchData = async () => {
|
|
21
|
+
setNodesWithoutDescription(
|
|
22
|
+
await djClient.system.nodes_without_description(),
|
|
23
|
+
);
|
|
24
|
+
setDimensionNodes(await djClient.system.dimensions());
|
|
25
|
+
};
|
|
26
|
+
fetchData().catch(console.error);
|
|
27
|
+
}, [djClient]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="chart-box" style={{ flex: '1 1 10%', maxWidth: '470px' }}>
|
|
31
|
+
<div className="chart-title">Governance Warnings</div>
|
|
32
|
+
<div
|
|
33
|
+
className="horiz-box"
|
|
34
|
+
style={{
|
|
35
|
+
padding: '5px 10px',
|
|
36
|
+
marginTop: '10px',
|
|
37
|
+
border: '1px solid #AA46BE30',
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<span style={{ color: '#FF804255', fontSize: '30px' }}>⚠</span>
|
|
41
|
+
<span style={{ padding: '10px 12px', fontSize: '18px' }}>
|
|
42
|
+
Missing Description
|
|
43
|
+
</span>
|
|
44
|
+
<div style={{ display: 'block' }}>
|
|
45
|
+
{nodesWithoutDescription?.map(entry => (
|
|
46
|
+
<div
|
|
47
|
+
className="jss316 badge"
|
|
48
|
+
style={{
|
|
49
|
+
margin: '5px 10px',
|
|
50
|
+
fontSize: '14px',
|
|
51
|
+
padding: '10px',
|
|
52
|
+
color: COLOR_MAPPING[entry.name.toLowerCase()],
|
|
53
|
+
backgroundColor: COLOR_MAPPING[entry.name.toLowerCase()] + '10',
|
|
54
|
+
}}
|
|
55
|
+
key={entry.name}
|
|
56
|
+
>
|
|
57
|
+
<strong>{Math.round(entry.value * 100)}%</strong>{' '}
|
|
58
|
+
<span>{entry.name.toLowerCase()}s</span>
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div
|
|
64
|
+
className="horiz-box"
|
|
65
|
+
style={{
|
|
66
|
+
padding: '5px 10px',
|
|
67
|
+
marginTop: '10px',
|
|
68
|
+
border: '1px solid #AA46BE30',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<div
|
|
72
|
+
style={{ width: '100%', display: 'inline-flex', marginTop: '-10px' }}
|
|
73
|
+
>
|
|
74
|
+
<span style={{ color: '#FF804255', fontSize: '40px' }}>∅</span>
|
|
75
|
+
<span
|
|
76
|
+
style={{
|
|
77
|
+
padding: '10px 12px',
|
|
78
|
+
fontSize: '18px',
|
|
79
|
+
marginTop: '10px',
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
Orphaned Dimensions
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
<div style={{ display: 'block' }}>
|
|
86
|
+
<div
|
|
87
|
+
className="jss316 badge"
|
|
88
|
+
style={{
|
|
89
|
+
margin: '5px 10px',
|
|
90
|
+
fontSize: '14px',
|
|
91
|
+
padding: '10px',
|
|
92
|
+
color: COLOR_MAPPING.dimension,
|
|
93
|
+
backgroundColor: COLOR_MAPPING.dimension + '10',
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<strong>
|
|
97
|
+
{dimensionNodes?.filter(
|
|
98
|
+
dim => dim.indegree === 0 || dim.cube_count === 0,
|
|
99
|
+
).length || '...'}
|
|
100
|
+
</strong>
|
|
101
|
+
<span> dimension nodes</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronously loads the component for the Overview page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { lazyLoad } from '../../../utils/loadable';
|
|
7
|
+
|
|
8
|
+
export const OverviewPage = props => {
|
|
9
|
+
return lazyLoad(
|
|
10
|
+
() => import('./index'),
|
|
11
|
+
module => module.OverviewPage,
|
|
12
|
+
{
|
|
13
|
+
fallback: <div></div>,
|
|
14
|
+
},
|
|
15
|
+
)(props);
|
|
16
|
+
};
|