datajunction-ui 0.0.1-rc.8 → 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 +123 -150
- 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,585 @@
|
|
|
1
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
2
|
+
import TableIcon from '../../icons/TableIcon';
|
|
3
|
+
import AddMaterializationPopover from './AddMaterializationPopover';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import AddBackfillPopover from './AddBackfillPopover';
|
|
6
|
+
import { labelize } from '../../../utils/form';
|
|
7
|
+
import NodeMaterializationDelete from '../../components/NodeMaterializationDelete';
|
|
8
|
+
import Tab from '../../components/Tab';
|
|
9
|
+
import NodeRevisionMaterializationTab from './NodeRevisionMaterializationTab';
|
|
10
|
+
import AvailabilityStateBlock from './AvailabilityStateBlock';
|
|
11
|
+
|
|
12
|
+
const cronstrue = require('cronstrue');
|
|
13
|
+
|
|
14
|
+
export default function NodeMaterializationTab({ node, djClient }) {
|
|
15
|
+
const [rawMaterializations, setRawMaterializations] = useState([]);
|
|
16
|
+
const [selectedRevisionTab, setSelectedRevisionTab] = useState(null);
|
|
17
|
+
const [showInactive, setShowInactive] = useState(false);
|
|
18
|
+
const [availabilityStates, setAvailabilityStates] = useState([]);
|
|
19
|
+
const [availabilityStatesByRevision, setAvailabilityStatesByRevision] =
|
|
20
|
+
useState({});
|
|
21
|
+
const [isRebuilding, setIsRebuilding] = useState(() => {
|
|
22
|
+
// Check if we're in the middle of a rebuild operation
|
|
23
|
+
return localStorage.getItem(`rebuilding-${node?.name}`) === 'true';
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const filteredMaterializations = useMemo(() => {
|
|
27
|
+
return showInactive
|
|
28
|
+
? rawMaterializations
|
|
29
|
+
: rawMaterializations.filter(mat => !mat.deactivated_at);
|
|
30
|
+
}, [rawMaterializations, showInactive]);
|
|
31
|
+
|
|
32
|
+
const materializationsByRevision = useMemo(() => {
|
|
33
|
+
return filteredMaterializations.reduce((acc, mat) => {
|
|
34
|
+
// Extract version from materialization config
|
|
35
|
+
const matVersion = mat.config?.cube?.version || node?.version;
|
|
36
|
+
|
|
37
|
+
if (!acc[matVersion]) {
|
|
38
|
+
acc[matVersion] = [];
|
|
39
|
+
}
|
|
40
|
+
acc[matVersion].push(mat);
|
|
41
|
+
return acc;
|
|
42
|
+
}, {});
|
|
43
|
+
}, [filteredMaterializations, node?.version]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const fetchData = async () => {
|
|
47
|
+
if (node) {
|
|
48
|
+
const data = await djClient.materializations(node.name);
|
|
49
|
+
|
|
50
|
+
// Store raw data
|
|
51
|
+
setRawMaterializations(data);
|
|
52
|
+
|
|
53
|
+
// Fetch availability states
|
|
54
|
+
const availabilityData = await djClient.availabilityStates(node.name);
|
|
55
|
+
setAvailabilityStates(availabilityData);
|
|
56
|
+
|
|
57
|
+
// Group availability states by version
|
|
58
|
+
const availabilityGrouped = availabilityData.reduce((acc, avail) => {
|
|
59
|
+
const version = avail.node_version || node.version;
|
|
60
|
+
if (!acc[version]) {
|
|
61
|
+
acc[version] = [];
|
|
62
|
+
}
|
|
63
|
+
acc[version].push(avail);
|
|
64
|
+
return acc;
|
|
65
|
+
}, {});
|
|
66
|
+
|
|
67
|
+
setAvailabilityStatesByRevision(availabilityGrouped);
|
|
68
|
+
|
|
69
|
+
// Clear rebuilding state once data is loaded after a page reload
|
|
70
|
+
if (localStorage.getItem(`rebuilding-${node.name}`) === 'true') {
|
|
71
|
+
localStorage.removeItem(`rebuilding-${node.name}`);
|
|
72
|
+
setIsRebuilding(false);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
fetchData().catch(console.error);
|
|
77
|
+
}, [djClient, node]);
|
|
78
|
+
|
|
79
|
+
// Separate useEffect to set default selected tab
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (
|
|
82
|
+
!selectedRevisionTab &&
|
|
83
|
+
Object.keys(materializationsByRevision).length > 0
|
|
84
|
+
) {
|
|
85
|
+
// First try to find current node version
|
|
86
|
+
if (materializationsByRevision[node?.version]) {
|
|
87
|
+
setSelectedRevisionTab(node.version);
|
|
88
|
+
} else {
|
|
89
|
+
// Otherwise, select the most recent version (sort by version string)
|
|
90
|
+
const sortedVersions = Object.keys(materializationsByRevision).sort(
|
|
91
|
+
(a, b) => b.localeCompare(a),
|
|
92
|
+
);
|
|
93
|
+
setSelectedRevisionTab(sortedVersions[0]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, [materializationsByRevision, selectedRevisionTab, node?.version]);
|
|
97
|
+
|
|
98
|
+
const partitionColumnsMap = node
|
|
99
|
+
? Object.fromEntries(
|
|
100
|
+
node?.columns
|
|
101
|
+
.filter(col => col.partition !== null)
|
|
102
|
+
.map(col => [col.name, col.display_name]),
|
|
103
|
+
)
|
|
104
|
+
: {};
|
|
105
|
+
const cron = materialization => {
|
|
106
|
+
var parsedCron = '';
|
|
107
|
+
try {
|
|
108
|
+
parsedCron = cronstrue.toString(materialization.schedule);
|
|
109
|
+
} catch (e) {}
|
|
110
|
+
return parsedCron;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const onClickRevisionTab = revisionId => () => {
|
|
114
|
+
setSelectedRevisionTab(revisionId);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const buildRevisionTabs = () => {
|
|
118
|
+
const versions = Object.keys(materializationsByRevision);
|
|
119
|
+
|
|
120
|
+
// Check if there are any materializations at all (including inactive ones)
|
|
121
|
+
const hasAnyMaterializations = rawMaterializations.length > 0;
|
|
122
|
+
|
|
123
|
+
// Determine which versions have only inactive materializations
|
|
124
|
+
const versionHasOnlyInactive = {};
|
|
125
|
+
rawMaterializations.forEach(mat => {
|
|
126
|
+
const matVersion = mat.config?.cube?.version || node.version;
|
|
127
|
+
if (!versionHasOnlyInactive[matVersion]) {
|
|
128
|
+
versionHasOnlyInactive[matVersion] = {
|
|
129
|
+
hasActive: false,
|
|
130
|
+
hasInactive: false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (mat.deactivated_at) {
|
|
134
|
+
versionHasOnlyInactive[matVersion].hasInactive = true;
|
|
135
|
+
} else {
|
|
136
|
+
versionHasOnlyInactive[matVersion].hasActive = true;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// If no active versions but there are inactive materializations, show checkbox and button
|
|
141
|
+
if (versions.length === 0) {
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
style={{
|
|
145
|
+
display: 'flex',
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
justifyContent: 'flex-end',
|
|
148
|
+
marginBottom: '20px',
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
|
|
152
|
+
{hasAnyMaterializations && (
|
|
153
|
+
<label
|
|
154
|
+
style={{
|
|
155
|
+
display: 'flex',
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
gap: '5px',
|
|
158
|
+
fontSize: '14px',
|
|
159
|
+
color: '#333',
|
|
160
|
+
padding: '4px 8px',
|
|
161
|
+
borderRadius: '12px',
|
|
162
|
+
backgroundColor: '#f5f5f5',
|
|
163
|
+
border: '1px solid #ddd',
|
|
164
|
+
}}
|
|
165
|
+
title="Shows inactive materializations for the latest cube."
|
|
166
|
+
>
|
|
167
|
+
<input
|
|
168
|
+
type="checkbox"
|
|
169
|
+
checked={showInactive}
|
|
170
|
+
onChange={e => setShowInactive(e.target.checked)}
|
|
171
|
+
/>
|
|
172
|
+
Show Inactive
|
|
173
|
+
</label>
|
|
174
|
+
)}
|
|
175
|
+
{node && <AddMaterializationPopover node={node} />}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Sort versions: current version first, then by version string (most recent first)
|
|
182
|
+
const sortedVersions = versions.sort((a, b) => {
|
|
183
|
+
// Current node version always comes first
|
|
184
|
+
if (a === node?.version) return -1;
|
|
185
|
+
if (b === node?.version) return 1;
|
|
186
|
+
|
|
187
|
+
// Then sort by version string (descending)
|
|
188
|
+
return b.localeCompare(a);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Check if latest version has active materializations
|
|
192
|
+
const hasLatestVersionMaterialization =
|
|
193
|
+
materializationsByRevision[node?.version] &&
|
|
194
|
+
materializationsByRevision[node?.version].length > 0;
|
|
195
|
+
|
|
196
|
+
// Refresh latest materialization function
|
|
197
|
+
const refreshLatestMaterialization = async () => {
|
|
198
|
+
if (
|
|
199
|
+
!window.confirm(
|
|
200
|
+
'This will create a new version of the cube and build new materialization workflows. The previous version of the cube and its materialization will be accessible using a specific version label. Would you like to continue?',
|
|
201
|
+
)
|
|
202
|
+
) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Set loading state in both React state and localStorage
|
|
207
|
+
setIsRebuilding(true);
|
|
208
|
+
localStorage.setItem(`rebuilding-${node.name}`, 'true');
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const { status, json } = await djClient.refreshLatestMaterialization(
|
|
212
|
+
node.name,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (status === 200 || status === 201) {
|
|
216
|
+
// Keep the loading state during page reload
|
|
217
|
+
window.location.reload(); // Reload to show the updated materialization
|
|
218
|
+
} else {
|
|
219
|
+
alert(`Failed to rebuild materialization: ${json.message}`);
|
|
220
|
+
// Clear loading state on error
|
|
221
|
+
localStorage.removeItem(`rebuilding-${node.name}`);
|
|
222
|
+
setIsRebuilding(false);
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
alert(`Error rebuilding materialization: ${error.message}`);
|
|
226
|
+
// Clear loading state on error
|
|
227
|
+
localStorage.removeItem(`rebuilding-${node.name}`);
|
|
228
|
+
setIsRebuilding(false);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div
|
|
234
|
+
style={{
|
|
235
|
+
display: 'flex',
|
|
236
|
+
alignItems: 'center',
|
|
237
|
+
justifyContent: 'space-between',
|
|
238
|
+
marginBottom: '20px',
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
<div className="align-items-center row">
|
|
242
|
+
{sortedVersions.map(version => (
|
|
243
|
+
<NodeRevisionMaterializationTab
|
|
244
|
+
key={version}
|
|
245
|
+
version={version}
|
|
246
|
+
node={node}
|
|
247
|
+
selectedRevisionTab={selectedRevisionTab}
|
|
248
|
+
onClickRevisionTab={onClickRevisionTab}
|
|
249
|
+
showInactive={showInactive}
|
|
250
|
+
versionHasOnlyInactive={versionHasOnlyInactive}
|
|
251
|
+
/>
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
|
|
255
|
+
<label
|
|
256
|
+
style={{
|
|
257
|
+
display: 'flex',
|
|
258
|
+
alignItems: 'center',
|
|
259
|
+
gap: '5px',
|
|
260
|
+
fontSize: '14px',
|
|
261
|
+
color: '#333',
|
|
262
|
+
padding: '4px 8px',
|
|
263
|
+
borderRadius: '12px',
|
|
264
|
+
backgroundColor: '#f5f5f5',
|
|
265
|
+
border: '1px solid #ddd',
|
|
266
|
+
}}
|
|
267
|
+
title="Shows inactive materializations for the latest cube."
|
|
268
|
+
>
|
|
269
|
+
<input
|
|
270
|
+
type="checkbox"
|
|
271
|
+
checked={showInactive}
|
|
272
|
+
onChange={e => setShowInactive(e.target.checked)}
|
|
273
|
+
/>
|
|
274
|
+
Show Inactive
|
|
275
|
+
</label>
|
|
276
|
+
{node &&
|
|
277
|
+
(hasLatestVersionMaterialization ? (
|
|
278
|
+
<button
|
|
279
|
+
className="edit_button"
|
|
280
|
+
aria-label="RefreshLatestMaterialization"
|
|
281
|
+
tabIndex="0"
|
|
282
|
+
onClick={refreshLatestMaterialization}
|
|
283
|
+
disabled={isRebuilding}
|
|
284
|
+
title="Create a new version of the cube and re-create its materialization workflows."
|
|
285
|
+
style={{
|
|
286
|
+
opacity: isRebuilding ? 0.7 : 1,
|
|
287
|
+
cursor: isRebuilding ? 'not-allowed' : 'pointer',
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
<span className="add_node">
|
|
291
|
+
Rebuild (latest) Materialization
|
|
292
|
+
</span>
|
|
293
|
+
</button>
|
|
294
|
+
) : (
|
|
295
|
+
<AddMaterializationPopover node={node} />
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const materializationRows = materializations => {
|
|
303
|
+
return materializations.map((materialization, index) => (
|
|
304
|
+
<div key={`${materialization.name}-${index}`}>
|
|
305
|
+
<div className="tr">
|
|
306
|
+
<div key={materialization.name} style={{ fontSize: 'large' }}>
|
|
307
|
+
<div
|
|
308
|
+
className="text-start node_name td"
|
|
309
|
+
style={{ fontWeight: '600' }}
|
|
310
|
+
>
|
|
311
|
+
{materialization.job
|
|
312
|
+
?.replace('MaterializationJob', '')
|
|
313
|
+
.match(/[A-Z][a-z]+/g)
|
|
314
|
+
.join(' ')}
|
|
315
|
+
</div>
|
|
316
|
+
<div className="td">
|
|
317
|
+
<NodeMaterializationDelete
|
|
318
|
+
nodeName={node.name}
|
|
319
|
+
materializationName={materialization.name}
|
|
320
|
+
nodeVersion={selectedRevisionTab}
|
|
321
|
+
/>
|
|
322
|
+
</div>
|
|
323
|
+
<div className="td">
|
|
324
|
+
<span className={`badge cron`}>{materialization.schedule}</span>
|
|
325
|
+
<div className={`cron-description`}>{cron(materialization)} </div>
|
|
326
|
+
</div>
|
|
327
|
+
<div className="td">
|
|
328
|
+
<span className={`badge strategy`}>
|
|
329
|
+
{labelize(materialization.strategy)}
|
|
330
|
+
</span>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
<div style={{ display: 'table-row' }}>
|
|
335
|
+
<div style={{ display: 'inline-flex' }}>
|
|
336
|
+
<ul className="backfills">
|
|
337
|
+
<li className="backfill">
|
|
338
|
+
<div className="backfills_header">Output Tables</div>{' '}
|
|
339
|
+
{materialization.output_tables.map(table => (
|
|
340
|
+
<div className={`table__full`} key={table}>
|
|
341
|
+
<div className="table__header">
|
|
342
|
+
<TableIcon />{' '}
|
|
343
|
+
<span className={`entity-info`}>
|
|
344
|
+
{table.split('.')[0] + '.' + table.split('.')[1]}
|
|
345
|
+
</span>
|
|
346
|
+
</div>
|
|
347
|
+
<div className={`table__body upstream_tables`}>
|
|
348
|
+
{table.split('.')[2]}
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
))}
|
|
352
|
+
</li>
|
|
353
|
+
</ul>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<div style={{ display: 'inline-flex' }}>
|
|
357
|
+
<ul className="backfills">
|
|
358
|
+
<li>
|
|
359
|
+
<div className="backfills_header">Workflows</div>{' '}
|
|
360
|
+
<ul>
|
|
361
|
+
{materialization.urls.map((url, idx) => (
|
|
362
|
+
<li style={{ listStyle: 'none' }} key={idx}>
|
|
363
|
+
<div
|
|
364
|
+
className="partitionLink"
|
|
365
|
+
style={{ fontSize: 'revert' }}
|
|
366
|
+
>
|
|
367
|
+
<a
|
|
368
|
+
href={url}
|
|
369
|
+
key={`url-${idx}`}
|
|
370
|
+
className=""
|
|
371
|
+
target="blank"
|
|
372
|
+
>
|
|
373
|
+
{idx === 0 ? 'main' : 'backfill'}
|
|
374
|
+
</a>
|
|
375
|
+
</div>
|
|
376
|
+
</li>
|
|
377
|
+
))}
|
|
378
|
+
</ul>
|
|
379
|
+
</li>
|
|
380
|
+
</ul>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<div style={{ display: 'inline-flex' }}>
|
|
384
|
+
<ul className="backfills">
|
|
385
|
+
<li className="backfill">
|
|
386
|
+
<details open>
|
|
387
|
+
<summary>
|
|
388
|
+
<span className="backfills_header">Backfills</span>{' '}
|
|
389
|
+
</summary>
|
|
390
|
+
{materialization.strategy === 'incremental_time' ? (
|
|
391
|
+
<ul>
|
|
392
|
+
<li>
|
|
393
|
+
<AddBackfillPopover
|
|
394
|
+
node={node}
|
|
395
|
+
materialization={materialization}
|
|
396
|
+
/>
|
|
397
|
+
</li>
|
|
398
|
+
{materialization.backfills.map(backfill => (
|
|
399
|
+
<li className="backfill">
|
|
400
|
+
<div className="partitionLink">
|
|
401
|
+
<a href={backfill.urls[0]}>
|
|
402
|
+
{backfill.spec.map(partition => {
|
|
403
|
+
const partitionBody =
|
|
404
|
+
'range' in partition &&
|
|
405
|
+
partition['range'] !== null ? (
|
|
406
|
+
<>
|
|
407
|
+
<span className="badge partition_value">
|
|
408
|
+
{partition.range[0]}
|
|
409
|
+
</span>
|
|
410
|
+
to
|
|
411
|
+
<span className="badge partition_value">
|
|
412
|
+
{partition.range[1]}
|
|
413
|
+
</span>
|
|
414
|
+
</>
|
|
415
|
+
) : (
|
|
416
|
+
<span className="badge partition_value">
|
|
417
|
+
{partition.values.join(', ')}
|
|
418
|
+
</span>
|
|
419
|
+
);
|
|
420
|
+
return (
|
|
421
|
+
<>
|
|
422
|
+
<div>
|
|
423
|
+
{
|
|
424
|
+
partitionColumnsMap[
|
|
425
|
+
partition.column_name.replaceAll(
|
|
426
|
+
'_DOT_',
|
|
427
|
+
'.',
|
|
428
|
+
)
|
|
429
|
+
]
|
|
430
|
+
}{' '}
|
|
431
|
+
{partitionBody}
|
|
432
|
+
</div>
|
|
433
|
+
</>
|
|
434
|
+
);
|
|
435
|
+
})}
|
|
436
|
+
</a>
|
|
437
|
+
</div>
|
|
438
|
+
</li>
|
|
439
|
+
))}
|
|
440
|
+
</ul>
|
|
441
|
+
) : (
|
|
442
|
+
<ul>
|
|
443
|
+
<li>N/A</li>
|
|
444
|
+
</ul>
|
|
445
|
+
)}
|
|
446
|
+
</details>
|
|
447
|
+
</li>
|
|
448
|
+
</ul>
|
|
449
|
+
</div>
|
|
450
|
+
<div className="td">
|
|
451
|
+
<ul className="backfills">
|
|
452
|
+
<li className="backfill">
|
|
453
|
+
<div className="backfills_header">Partitions</div>{' '}
|
|
454
|
+
<ul>
|
|
455
|
+
{node.columns
|
|
456
|
+
.filter(col => col.partition !== null)
|
|
457
|
+
.map(column => {
|
|
458
|
+
return (
|
|
459
|
+
<li key={column.name}>
|
|
460
|
+
<div className="partitionLink">
|
|
461
|
+
{column.display_name}
|
|
462
|
+
<span className="badge partition_value">
|
|
463
|
+
{column.partition.type_}
|
|
464
|
+
</span>
|
|
465
|
+
</div>
|
|
466
|
+
</li>
|
|
467
|
+
);
|
|
468
|
+
})}
|
|
469
|
+
</ul>
|
|
470
|
+
</li>
|
|
471
|
+
</ul>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
));
|
|
476
|
+
};
|
|
477
|
+
const currentRevisionMaterializations = selectedRevisionTab
|
|
478
|
+
? materializationsByRevision[selectedRevisionTab] || []
|
|
479
|
+
: filteredMaterializations;
|
|
480
|
+
|
|
481
|
+
const currentRevisionAvailability = selectedRevisionTab
|
|
482
|
+
? availabilityStatesByRevision[selectedRevisionTab] || []
|
|
483
|
+
: availabilityStates;
|
|
484
|
+
|
|
485
|
+
const renderMaterializedDatasets = availabilityStates => {
|
|
486
|
+
if (!availabilityStates || availabilityStates.length === 0) {
|
|
487
|
+
return (
|
|
488
|
+
<div className="message alert" style={{ marginTop: '10px' }}>
|
|
489
|
+
No materialized datasets available for this revision.
|
|
490
|
+
</div>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return availabilityStates.map((availability, index) => (
|
|
495
|
+
<AvailabilityStateBlock
|
|
496
|
+
key={`availability-${index}`}
|
|
497
|
+
availability={availability}
|
|
498
|
+
/>
|
|
499
|
+
));
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
<>
|
|
504
|
+
<div
|
|
505
|
+
className="table-vertical"
|
|
506
|
+
role="table"
|
|
507
|
+
aria-label="Materializations"
|
|
508
|
+
style={{ position: 'relative' }}
|
|
509
|
+
>
|
|
510
|
+
{/* Loading overlay */}
|
|
511
|
+
{isRebuilding && (
|
|
512
|
+
<div
|
|
513
|
+
style={{
|
|
514
|
+
position: 'absolute',
|
|
515
|
+
top: 0,
|
|
516
|
+
left: 0,
|
|
517
|
+
right: 0,
|
|
518
|
+
bottom: 0,
|
|
519
|
+
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
|
520
|
+
display: 'flex',
|
|
521
|
+
flexDirection: 'column',
|
|
522
|
+
justifyContent: 'center',
|
|
523
|
+
alignItems: 'center',
|
|
524
|
+
zIndex: 1000,
|
|
525
|
+
minHeight: '200px',
|
|
526
|
+
}}
|
|
527
|
+
>
|
|
528
|
+
<div
|
|
529
|
+
style={{
|
|
530
|
+
width: '40px',
|
|
531
|
+
height: '40px',
|
|
532
|
+
border: '4px solid #f3f3f3',
|
|
533
|
+
borderTop: '4px solid #3498db',
|
|
534
|
+
borderRadius: '50%',
|
|
535
|
+
animation: 'spin 1s linear infinite',
|
|
536
|
+
marginBottom: '16px',
|
|
537
|
+
}}
|
|
538
|
+
/>
|
|
539
|
+
<div
|
|
540
|
+
style={{ fontSize: '16px', color: '#666', textAlign: 'center' }}
|
|
541
|
+
>
|
|
542
|
+
Rebuilding materialization...
|
|
543
|
+
<br />
|
|
544
|
+
<small style={{ fontSize: '14px' }}>
|
|
545
|
+
This may take a few moments
|
|
546
|
+
</small>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
)}
|
|
550
|
+
|
|
551
|
+
<div>
|
|
552
|
+
{buildRevisionTabs()}
|
|
553
|
+
{currentRevisionMaterializations.length > 0 ? (
|
|
554
|
+
<div
|
|
555
|
+
className="card-inner-table table"
|
|
556
|
+
aria-label="Materializations"
|
|
557
|
+
aria-hidden="false"
|
|
558
|
+
>
|
|
559
|
+
<div style={{ display: 'table' }}>
|
|
560
|
+
{materializationRows(
|
|
561
|
+
currentRevisionMaterializations.filter(
|
|
562
|
+
materialization =>
|
|
563
|
+
!(
|
|
564
|
+
materialization.name === 'default' &&
|
|
565
|
+
node.type === 'cube'
|
|
566
|
+
),
|
|
567
|
+
),
|
|
568
|
+
)}
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
) : (
|
|
572
|
+
<div className="message alert" style={{ marginTop: '10px' }}>
|
|
573
|
+
No materialization workflows configured for this revision.
|
|
574
|
+
</div>
|
|
575
|
+
)}
|
|
576
|
+
{Object.keys(materializationsByRevision).length > 0 && (
|
|
577
|
+
<div style={{ marginTop: '30px' }}>
|
|
578
|
+
{renderMaterializedDatasets(currentRevisionAvailability)}
|
|
579
|
+
</div>
|
|
580
|
+
)}
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
</>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Tab from '../../components/Tab';
|
|
2
|
+
|
|
3
|
+
export default function NodeRevisionMaterializationTab({
|
|
4
|
+
version,
|
|
5
|
+
node,
|
|
6
|
+
selectedRevisionTab,
|
|
7
|
+
onClickRevisionTab,
|
|
8
|
+
showInactive,
|
|
9
|
+
versionHasOnlyInactive,
|
|
10
|
+
}) {
|
|
11
|
+
const isCurrentVersion = version === node?.version;
|
|
12
|
+
const tabName = isCurrentVersion ? `${version} (latest)` : version;
|
|
13
|
+
const versionInfo = versionHasOnlyInactive[version];
|
|
14
|
+
const isOnlyInactive =
|
|
15
|
+
versionInfo && !versionInfo.hasActive && versionInfo.hasInactive;
|
|
16
|
+
|
|
17
|
+
// For inactive-only versions, render with oval styling
|
|
18
|
+
if (isOnlyInactive && showInactive) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
key={version}
|
|
22
|
+
className={selectedRevisionTab === version ? 'col active' : 'col'}
|
|
23
|
+
>
|
|
24
|
+
<div className="header-tabs nav-overflow nav nav-tabs">
|
|
25
|
+
<div className="nav-item">
|
|
26
|
+
<button
|
|
27
|
+
id={version}
|
|
28
|
+
className="nav-link"
|
|
29
|
+
tabIndex="0"
|
|
30
|
+
onClick={onClickRevisionTab(version)}
|
|
31
|
+
aria-label={tabName}
|
|
32
|
+
aria-hidden="false"
|
|
33
|
+
style={{
|
|
34
|
+
padding: '4px 8px',
|
|
35
|
+
borderRadius: '12px',
|
|
36
|
+
backgroundColor: '#f5f5f5',
|
|
37
|
+
border: '1px solid #ddd',
|
|
38
|
+
margin: '0 2px',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{tabName}
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Tab
|
|
51
|
+
key={version}
|
|
52
|
+
id={version}
|
|
53
|
+
name={tabName}
|
|
54
|
+
onClick={onClickRevisionTab(version)}
|
|
55
|
+
selectedTab={selectedRevisionTab}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|