diff --git a/js_modules/dagit/src/App.tsx b/js_modules/dagit/src/App.tsx index 850bccb1c388..708381286b58 100644 --- a/js_modules/dagit/src/App.tsx +++ b/js_modules/dagit/src/App.tsx @@ -13,23 +13,17 @@ import { import {APP_PATH_PREFIX} from 'src/DomUtils'; import {FeatureFlagsRoot} from 'src/FeatureFlagsRoot'; import {InstanceDetailsRoot} from 'src/InstanceDetailsRoot'; -import {PipelineExplorerRoot} from 'src/PipelineExplorerRoot'; -import {PipelineRunsRoot} from 'src/PipelineRunsRoot'; import {PythonErrorInfo} from 'src/PythonErrorInfo'; import {TimezoneProvider} from 'src/TimeComponents'; import {AssetsRoot} from 'src/assets/AssetsRoot'; -import {PipelineExecutionRoot} from 'src/execute/PipelineExecutionRoot'; -import {PipelineExecutionSetupRoot} from 'src/execute/PipelineExecutionSetupRoot'; import {useDocumentTitle} from 'src/hooks/useDocumentTitle'; import {LeftNav} from 'src/nav/LeftNav'; -import {PipelineNav} from 'src/nav/PipelineNav'; -import {PipelinePartitionsRoot} from 'src/partitions/PipelinePartitionsRoot'; -import {PipelineOverviewRoot} from 'src/pipelines/PipelineOverviewRoot'; -import {RunRoot} from 'src/runs/RunRoot'; +import {PipelineRoot} from 'src/pipelines/PipelineRoot'; import {RunsRoot} from 'src/runs/RunsRoot'; import {ScheduleRoot} from 'src/schedules/ScheduleRoot'; import {SchedulerRoot} from 'src/schedules/SchedulerRoot'; import {SchedulesRoot} from 'src/schedules/SchedulesRoot'; +import {SnapshotRoot} from 'src/snapshots/SnapshotRoot'; import {SolidDetailsRoot} from 'src/solids/SolidDetailsRoot'; import {SolidsRoot} from 'src/solids/SolidsRoot'; @@ -45,37 +39,8 @@ const AppRoutes = () => ( - - ( -
- - - - - - - - - - {/* Capture solid subpath in a regex match */} - - -
- )} - /> + + {(context) => diff --git a/js_modules/dagit/src/PipelineExplorerRoot.tsx b/js_modules/dagit/src/PipelineExplorerRoot.tsx index 92cc1101acc8..d6d6f4cea9d6 100644 --- a/js_modules/dagit/src/PipelineExplorerRoot.tsx +++ b/js_modules/dagit/src/PipelineExplorerRoot.tsx @@ -1,12 +1,11 @@ -import {Colors, IconName, NonIdealState} from '@blueprintjs/core'; +import {IconName, NonIdealState} from '@blueprintjs/core'; import {IconNames} from '@blueprintjs/icons'; import gql from 'graphql-tag'; import * as React from 'react'; import {useQuery} from 'react-apollo'; import {Redirect, RouteComponentProps} from 'react-router-dom'; -import styled from 'styled-components/macro'; -import {usePipelineSelector, useActivePipelineForName} from 'src/DagsterRepositoryContext'; +import {usePipelineSelector} from 'src/DagsterRepositoryContext'; import {Loading} from 'src/Loading'; import {PipelineExplorer, PipelineExplorerOptions} from 'src/PipelineExplorer'; import { @@ -133,8 +132,6 @@ export const PipelineExplorerRoot: React.FunctionComponent const selectedName = explorerPath.pathSolids[explorerPath.pathSolids.length - 1]; - const pipeline = useActivePipelineForName(explorerPath.pipelineName); - return ( {(result) => { @@ -162,29 +159,22 @@ export const PipelineExplorerRoot: React.FunctionComponent return ; } - const pathID = explorerPath.snapshotId; - return ( - <> - {pathID && pipeline?.pipelineSnapshotId !== pathID && ( - You are viewing a historical pipeline snapshot. - )} - - displayedHandles - .filter((s) => s.solid.definition.name === definitionName) - .map((s) => ({handleID: s.handleID})) - } - /> - + + displayedHandles + .filter((s) => s.solid.definition.name === definitionName) + .map((s) => ({handleID: s.handleID})) + } + /> ); }} @@ -293,11 +283,3 @@ const ExplorerSnapshotResolver: React.FunctionComponent = ({ ); }; - -const SnapshotNotice = styled.div` - background: linear-gradient(to bottom, ${Colors.GOLD5}, ${Colors.GOLD4}); - border-bottom: 1px solid ${Colors.GOLD3}; - text-align: center; - padding: 4px 10px; - user-select: none; -`; diff --git a/js_modules/dagit/src/PipelineRunsRoot.tsx b/js_modules/dagit/src/PipelineRunsRoot.tsx index 6419ecee6f0a..df72f223b5c9 100644 --- a/js_modules/dagit/src/PipelineRunsRoot.tsx +++ b/js_modules/dagit/src/PipelineRunsRoot.tsx @@ -8,6 +8,7 @@ import styled from 'styled-components/macro'; import {CursorPaginationControls} from 'src/CursorPaginationControls'; import {ScrollContainer} from 'src/ListComponents'; import {Loading} from 'src/Loading'; +import {explorerPathFromString} from 'src/PipelinePathUtils'; import {useDocumentTitle} from 'src/hooks/useDocumentTitle'; import {RunTable} from 'src/runs/RunTable'; import {RunsQueryRefetchContext} from 'src/runs/RunUtils'; @@ -24,14 +25,13 @@ import { } from 'src/types/PipelineRunsRootQuery'; const PAGE_SIZE = 25; -const ENABLED_FILTERS: RunFilterTokenType[] = ['id', 'status', 'tag']; +const ENABLED_FILTERS: RunFilterTokenType[] = ['id', 'snapshotId', 'status', 'tag']; export const PipelineRunsRoot: React.FunctionComponent> = ({match}) => { - const pipelineName = match.params.pipelinePath.split(':')[0]; + const {pipelineName, snapshotId} = explorerPathFromString(match.params.pipelinePath); useDocumentTitle(`Pipeline: ${pipelineName}`); - const [filterTokens, setFilterTokens] = useRunFiltering(ENABLED_FILTERS); const {queryResult, paginationProps} = useCursorPaginatedQuery< @@ -41,7 +41,7 @@ export const PipelineRunsRoot: React.FunctionComponent { if (runs.pipelineRunsOrError.__typename !== 'PipelineRuns') { @@ -57,6 +57,11 @@ export const PipelineRunsRoot: React.FunctionComponent @@ -71,7 +76,7 @@ export const PipelineRunsRoot: React.FunctionComponent @@ -106,7 +111,6 @@ export const PipelineRunsRoot: React.FunctionComponent = ({ const StyledTagInput = styled(TagInput)<{small?: boolean}>` min-width: 400px; - max-width: 400px; + max-width: 600px; input { font-size: 12px; } diff --git a/js_modules/dagit/src/__tests__/__snapshots__/App.test.tsx.snap b/js_modules/dagit/src/__tests__/__snapshots__/App.test.tsx.snap index 2b32d1e4e00c..efc2ad1ebd97 100644 --- a/js_modules/dagit/src/__tests__/__snapshots__/App.test.tsx.snap +++ b/js_modules/dagit/src/__tests__/__snapshots__/App.test.tsx.snap @@ -529,7 +529,7 @@ exports[`renders execution 1`] = ` } >
@@ -12704,7 +12704,7 @@ exports[`renders type page 1`] = ` } >
@@ -42,7 +42,7 @@ exports[`renders with tokens [snapshot] 1`] = ` onClick={[Function]} >
diff --git a/js_modules/dagit/src/execute/PipelineExecutionRoot.tsx b/js_modules/dagit/src/execute/PipelineExecutionRoot.tsx index 62539c039f28..9873f4095f1c 100644 --- a/js_modules/dagit/src/execute/PipelineExecutionRoot.tsx +++ b/js_modules/dagit/src/execute/PipelineExecutionRoot.tsx @@ -3,7 +3,7 @@ import {IconNames} from '@blueprintjs/icons'; import gql from 'graphql-tag'; import * as React from 'react'; import {Query} from 'react-apollo'; -import {RouteComponentProps} from 'react-router-dom'; +import {Redirect, RouteComponentProps} from 'react-router-dom'; import { usePipelineSelector, @@ -16,6 +16,7 @@ import { applyCreateSession, useStorage, } from 'src/LocalStorage'; +import {explorerPathFromString} from 'src/PipelinePathUtils'; import { ExecutionSessionContainer, ExecutionSessionContainerError, @@ -35,7 +36,7 @@ import {useDocumentTitle} from 'src/hooks/useDocumentTitle'; export const PipelineExecutionRoot: React.FunctionComponent> = ({match}) => { - const pipelineName = match.params.pipelinePath.split(':')[0]; + const {pipelineName, snapshotId} = explorerPathFromString(match.params.pipelinePath); useDocumentTitle(`Pipeline: ${pipelineName}`); const {loading} = useRepositoryOptions(); @@ -45,6 +46,10 @@ export const PipelineExecutionRoot: React.FunctionComponent; + } + const onSaveSession = (session: string, changes: IExecutionSessionChanges) => { onSave(applyChangesToSession(data, session, changes)); }; diff --git a/js_modules/dagit/src/nav/PipelineNav.tsx b/js_modules/dagit/src/nav/PipelineNav.tsx index 3be356b9f991..2f24ad57d829 100644 --- a/js_modules/dagit/src/nav/PipelineNav.tsx +++ b/js_modules/dagit/src/nav/PipelineNav.tsx @@ -1,48 +1,52 @@ -import {IBreadcrumbProps, IconName} from '@blueprintjs/core'; +import {IBreadcrumbProps, Tag} from '@blueprintjs/core'; import React from 'react'; import {useRouteMatch} from 'react-router-dom'; +import styled from 'styled-components'; import {useRepository} from 'src/DagsterRepositoryContext'; import {explorerPathFromString, explorerPathToString} from 'src/PipelinePathUtils'; import {TopNav} from 'src/nav/TopNav'; -const PIPELINE_TABS: { - title: string; - pathComponent: string; - icon: IconName; -}[] = [ - {title: 'Overview', pathComponent: 'overview', icon: 'dashboard'}, - {title: 'Definition', pathComponent: '', icon: 'diagram-tree'}, - { +const pipelineTabs = { + overview: {title: 'Overview', pathComponent: 'overview', icon: 'dashboard'}, + definition: {title: 'Definition', pathComponent: '', icon: 'diagram-tree'}, + playground: { title: 'Playground', pathComponent: 'playground', icon: 'manually-entered-data', }, - { + runs: { title: 'Runs', pathComponent: 'runs', icon: 'history', }, - { + partitions: { title: 'Partitions', pathComponent: 'partitions', icon: 'multi-select', }, -]; +}; + +const snapshotOrder = ['definition', 'runs']; +const currentOrder = ['overview', 'definition', 'playground', 'runs', 'partitions']; export function tabForPipelinePathComponent(component?: string) { - return ( - PIPELINE_TABS.find((t) => t.pathComponent === component) || - PIPELINE_TABS.find((t) => t.pathComponent === '')! - ); + const tabList = Object.keys(pipelineTabs); + const match = + tabList.find((t) => pipelineTabs[t].pathComponent === component) || + tabList.find((t) => pipelineTabs[t].pathComponent === '')!; + return pipelineTabs[match]; } -export const PipelineNav: React.FunctionComponent = () => { +interface PipelineNavProps { + isHistorical: boolean; + isSnapshot: boolean; +} + +export const PipelineNav: React.FunctionComponent = (props: PipelineNavProps) => { + const {isHistorical, isSnapshot} = props; const repository = useRepository(); const match = useRouteMatch<{tab: string; selector: string}>(['/pipeline/:selector/:tab?']); - if (!match) { - return ; - } const active = tabForPipelinePathComponent(match.params.tab); const explorerPath = explorerPathFromString(match.params.selector); @@ -52,25 +56,66 @@ export const PipelineNav: React.FunctionComponent = () => { // When you click one of the top tabs, it resets the snapshot you may be looking at // in the Definition tab and also clears solids from the path - const explorerPathWithoutSnapshot = explorerPathToString({ + const explorerPathForTab = explorerPathToString({ ...explorerPath, - snapshotId: undefined, pathSolids: [], }); - const breadcrumbs: IBreadcrumbProps[] = [ - {text: 'Pipelines', icon: 'diagram-tree'}, - {text: explorerPath.pipelineName}, - ]; + const breadcrumbs: IBreadcrumbProps[] = [{text: 'Pipelines', icon: 'diagram-tree'}]; - const tabs = PIPELINE_TABS.filter( - (tab) => hasPartitionSet || tab.pathComponent !== 'partitions', - ).map((tab) => { - return { - text: tab.title, - href: `/pipeline/${explorerPathWithoutSnapshot}${tab.pathComponent}`, - }; - }); + if (isSnapshot) { + const tag = isHistorical ? ( + + Historical snapshot + + ) : ( + + Current snapshot + + ); + + breadcrumbs.push( + { + text: explorerPath.pipelineName, + href: `/pipeline/${explorerPath.pipelineName}`, + }, + { + text: ( +
+ {explorerPath.snapshotId} + {tag} +
+ ), + }, + ); + } else { + breadcrumbs.push({ + text: explorerPath.pipelineName, + }); + } + + const tabForKey = React.useCallback( + (key) => { + const tab = pipelineTabs[key]; + return { + text: tab.title, + href: `/pipeline/${explorerPathForTab}${tab.pathComponent}`, + }; + }, + [explorerPathForTab], + ); + + const tabs = React.useMemo(() => { + return isSnapshot + ? snapshotOrder.map(tabForKey) + : currentOrder.filter((key) => hasPartitionSet || key !== 'partitions').map(tabForKey); + }, [hasPartitionSet, isSnapshot, tabForKey]); return ; }; + +const Mono = styled.div` + font-family: monospace; + font-size: 14px; + margin-right: 12px; +`; diff --git a/js_modules/dagit/src/nav/TopNav.stories.tsx b/js_modules/dagit/src/nav/TopNav.stories.tsx index b3a8785b2402..f603670fe124 100644 --- a/js_modules/dagit/src/nav/TopNav.stories.tsx +++ b/js_modules/dagit/src/nav/TopNav.stories.tsx @@ -42,3 +42,24 @@ WithTag.args = { }, ], }; + +export const WithTagAndTabs = Template.bind({}); +WithTagAndTabs.args = { + breadcrumbs: [ + {text: 'Snapshots', icon: 'camera'}, + { + text: ( +
+
c513370e7e15df09c9edd297dfa8f3b4
+ + Historical snapshot + +
+ ), + }, + ], + tabs: [ + {text: 'Overview', href: '#'}, + {text: 'Definition', href: '#'}, + ], +}; diff --git a/js_modules/dagit/src/nav/TopNav.tsx b/js_modules/dagit/src/nav/TopNav.tsx index b94b5f9476b1..4ea57f951a3c 100644 --- a/js_modules/dagit/src/nav/TopNav.tsx +++ b/js_modules/dagit/src/nav/TopNav.tsx @@ -46,9 +46,9 @@ const BreadcrumbContainer = styled.div` `; const PipelineTabBarContainer = styled.div` - align-items: center; background: ${Colors.LIGHT_GRAY4}; border-bottom: 1px solid ${Colors.GRAY5}; display: flex; + flex-wrap: wrap; padding: 2px 16px 0; `; diff --git a/js_modules/dagit/src/partitions/PipelinePartitionsRoot.tsx b/js_modules/dagit/src/partitions/PipelinePartitionsRoot.tsx index e4f21ff4fdc4..6840ecb7b057 100644 --- a/js_modules/dagit/src/partitions/PipelinePartitionsRoot.tsx +++ b/js_modules/dagit/src/partitions/PipelinePartitionsRoot.tsx @@ -4,11 +4,12 @@ import * as querystring from 'query-string'; import * as React from 'react'; import {useQuery} from 'react-apollo'; import {__RouterContext as RouterContext} from 'react-router'; -import {RouteComponentProps} from 'react-router-dom'; +import {Redirect, RouteComponentProps} from 'react-router-dom'; import styled from 'styled-components'; import {useRepositorySelector} from 'src/DagsterRepositoryContext'; import {Loading} from 'src/Loading'; +import {explorerPathFromString} from 'src/PipelinePathUtils'; import {useDocumentTitle} from 'src/hooks/useDocumentTitle'; import {PartitionView} from 'src/partitions/PartitionView'; import {PartitionsBackfill} from 'src/partitions/PartitionsBackfill'; @@ -22,7 +23,7 @@ type PartitionSet = PipelinePartitionsRootQuery_partitionSetsOrError_PartitionSe export const PipelinePartitionsRoot: React.FunctionComponent> = ({location, match}) => { - const pipelineName = match.params.pipelinePath.split(':')[0]; + const {pipelineName, snapshotId} = explorerPathFromString(match.params.pipelinePath); useDocumentTitle(`Pipeline: ${pipelineName}`); const repositorySelector = useRepositorySelector(); @@ -42,6 +43,10 @@ export const PipelinePartitionsRoot: React.FunctionComponent(false); const [runTags, setRunTags] = React.useState<{[key: string]: string}>({}); + if (snapshotId) { + return ; + } + return ( {({partitionSetsOrError}) => { diff --git a/js_modules/dagit/src/pipelines/PipelineOverviewRoot.tsx b/js_modules/dagit/src/pipelines/PipelineOverviewRoot.tsx index fb5338de7cf2..ddd5463820e9 100644 --- a/js_modules/dagit/src/pipelines/PipelineOverviewRoot.tsx +++ b/js_modules/dagit/src/pipelines/PipelineOverviewRoot.tsx @@ -3,13 +3,14 @@ import {IconNames} from '@blueprintjs/icons'; import gql from 'graphql-tag'; import * as React from 'react'; import {useQuery} from 'react-apollo'; -import {Link} from 'react-router-dom'; +import {Link, Redirect} from 'react-router-dom'; import {RouteComponentProps} from 'react-router-dom'; import styled from 'styled-components/macro'; import {usePipelineSelector} from 'src/DagsterRepositoryContext'; import {RowColumn, RowContainer} from 'src/ListComponents'; import {Loading} from 'src/Loading'; +import {explorerPathFromString} from 'src/PipelinePathUtils'; import {Timestamp} from 'src/TimeComponents'; import {PipelineGraph} from 'src/graph/PipelineGraph'; import {SVGViewport} from 'src/graph/SVGViewport'; @@ -32,7 +33,7 @@ type Schedule = PipelineOverviewQuery_pipelineSnapshotOrError_PipelineSnapshot_s export const PipelineOverviewRoot: React.FunctionComponent> = ({match}) => { - const pipelineName = match.params.pipelinePath.split(':')[0]; + const {pipelineName, snapshotId} = explorerPathFromString(match.params.pipelinePath); useDocumentTitle(`Pipeline: ${pipelineName}`); const pipelineSelector = usePipelineSelector(pipelineName); @@ -44,6 +45,11 @@ export const PipelineOverviewRoot: React.FunctionComponent; + } + return ( {({pipelineSnapshotOrError}) => { diff --git a/js_modules/dagit/src/pipelines/PipelineRoot.tsx b/js_modules/dagit/src/pipelines/PipelineRoot.tsx new file mode 100644 index 000000000000..286037ebfb80 --- /dev/null +++ b/js_modules/dagit/src/pipelines/PipelineRoot.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import {Route, Switch} from 'react-router-dom'; + +import {useActivePipelineForName} from 'src/DagsterRepositoryContext'; +import {PipelineExplorerRoot} from 'src/PipelineExplorerRoot'; +import {explorerPathFromString} from 'src/PipelinePathUtils'; +import {PipelineRunsRoot} from 'src/PipelineRunsRoot'; +import {PipelineExecutionRoot} from 'src/execute/PipelineExecutionRoot'; +import {PipelineExecutionSetupRoot} from 'src/execute/PipelineExecutionSetupRoot'; +import {PipelineNav} from 'src/nav/PipelineNav'; +import {PipelinePartitionsRoot} from 'src/partitions/PipelinePartitionsRoot'; +import {PipelineOverviewRoot} from 'src/pipelines/PipelineOverviewRoot'; +import {RunRoot} from 'src/runs/RunRoot'; + +export const PipelineRoot: React.FunctionComponent = (props: any) => { + const {params} = props.match; + const path = params['0']; + const {pipelineName, snapshotId} = explorerPathFromString(path); + const currentPipelineState = useActivePipelineForName(pipelineName); + + const isSnapshot = !!snapshotId; + const isHistorical = isSnapshot && currentPipelineState?.pipelineSnapshotId !== snapshotId; + + return ( +
+ + + + + + + + + {/* Capture solid subpath in a regex match */} + + +
+ ); +}; diff --git a/js_modules/dagit/src/runs/RunTable.tsx b/js_modules/dagit/src/runs/RunTable.tsx index c9756e0eb27d..85c93603cd7f 100644 --- a/js_modules/dagit/src/runs/RunTable.tsx +++ b/js_modules/dagit/src/runs/RunTable.tsx @@ -5,6 +5,7 @@ import {Link} from 'react-router-dom'; import {useActivePipelineForName} from 'src/DagsterRepositoryContext'; import {Legend, LegendColumn, RowColumn, RowContainer} from 'src/ListComponents'; +import {explorerPathToString} from 'src/PipelinePathUtils'; import {PythonErrorInfo} from 'src/PythonErrorInfo'; import {TokenizingFieldValue} from 'src/TokenizingField'; import {RunActionsMenu, RunBulkActionsMenu} from 'src/runs/RunActionsMenu'; @@ -135,7 +136,13 @@ const RunRow: React.FunctionComponent<{ checked?: boolean; onToggleChecked?: () => void; }> = ({run, onSetFilter, checked, onToggleChecked}) => { - const pipelineLink = `/pipeline/${run.pipelineName}@${run.pipelineSnapshotId}/`; + const pipelineLink = `/pipeline/${explorerPathToString({ + pipelineName: run.pipelineName, + snapshotId: run.pipelineSnapshotId || '', + solidsQuery: '', + pathSolids: [], + })}`; + const activePipeline = useActivePipelineForName(run.pipelineName); const isHistorical = activePipeline?.pipelineSnapshotId !== run.pipelineSnapshotId; @@ -157,7 +164,7 @@ const RunRow: React.FunctionComponent<{ - {titleForRun(run)} + {titleForRun(run)} {run.pipelineName} diff --git a/js_modules/dagit/src/runs/RunsFilter.tsx b/js_modules/dagit/src/runs/RunsFilter.tsx index e8c73272b5e4..863f26baf59a 100644 --- a/js_modules/dagit/src/runs/RunsFilter.tsx +++ b/js_modules/dagit/src/runs/RunsFilter.tsx @@ -15,7 +15,7 @@ import { import {RunsSearchSpaceQuery} from 'src/runs/types/RunsSearchSpaceQuery'; import {PipelineRunStatus, PipelineRunsFilter} from 'src/types/globalTypes'; -export type RunFilterTokenType = 'id' | 'status' | 'pipeline' | 'tag'; +export type RunFilterTokenType = 'id' | 'status' | 'pipeline' | 'snapshotId' | 'tag'; export const RUN_PROVIDERS_EMPTY = [ { @@ -34,6 +34,10 @@ export const RUN_PROVIDERS_EMPTY = [ token: 'tag', values: () => [], }, + { + token: 'snapshotId', + values: () => [], + }, ]; /** @@ -76,6 +80,8 @@ export function runsFilterForSearchTokens(search: TokenizingFieldValue[]) { obj.runId = item.value; } else if (item.token === 'status') { obj.status = item.value as PipelineRunStatus; + } else if (item.token === 'snapshotId') { + obj.snapshotId = item.value; } else if (item.token === 'tag') { const [key, value] = item.value.split('='); if (obj.tags) { @@ -122,6 +128,10 @@ function searchSuggestionsForRuns( return all; }, }, + { + token: 'snapshotId', + values: () => [], + }, ]; if (enabledFilters) { @@ -174,7 +184,7 @@ export const RunsFilter: React.FunctionComponent = ({ } // Can only have one filter value for pipeline, status, or id - const limitedTokens = new Set(['id', 'pipeline', 'status']); + const limitedTokens = new Set(['id', 'pipeline', 'status', 'snapshotId']); const presentLimitedTokens = tokens.filter((token) => limitedTokens.has(token)); return suggestionProviders.filter((provider) => !presentLimitedTokens.includes(provider.token)); diff --git a/js_modules/dagit/src/snapshots/SnapshotRoot.tsx b/js_modules/dagit/src/snapshots/SnapshotRoot.tsx new file mode 100644 index 000000000000..b57fcc92dc0b --- /dev/null +++ b/js_modules/dagit/src/snapshots/SnapshotRoot.tsx @@ -0,0 +1,108 @@ +import {IBreadcrumbProps, NonIdealState, Tag} from '@blueprintjs/core'; +import * as React from 'react'; +import {useQuery} from 'react-apollo'; +import {Link} from 'react-router-dom'; +import styled from 'styled-components/macro'; + +import {Loading} from 'src/Loading'; +import {PIPELINE_EXPLORER_ROOT_QUERY} from 'src/PipelineExplorerRoot'; +import {explorerPathFromString} from 'src/PipelinePathUtils'; +import {TopNav} from 'src/nav/TopNav'; +import { + PipelineExplorerRootQuery, + PipelineExplorerRootQueryVariables, +} from 'src/types/PipelineExplorerRootQuery'; + +export const SnapshotRoot: React.FunctionComponent = (props) => { + const {params} = props.match; + const {pipelinePath} = params; + const {pipelineName, snapshotId} = explorerPathFromString(pipelinePath); + + const queryResult = useQuery( + PIPELINE_EXPLORER_ROOT_QUERY, + { + fetchPolicy: 'cache-and-network', + partialRefetch: true, + variables: { + snapshotId, + rootHandleID: '', + }, + }, + ); + + return ( +
+ + {(data) => { + const snapshotData = data?.pipelineSnapshotOrError; + + if (snapshotData.__typename === 'PipelineSnapshotNotFoundError') { + const {message} = snapshotData; + return ; + } + + const pipelineForSnapshot = + snapshotData?.__typename === 'PipelineSnapshot' ? snapshotData?.name : null; + + if ( + snapshotData.__typename === 'PipelineSnapshot' && + pipelineName !== pipelineForSnapshot + ) { + return ( + + ); + } + + const items: IBreadcrumbProps[] = [ + {icon: 'diagram-tree', text: 'Pipelines'}, + {text: pipelineName, href: `/pipeline/${pipelineName}`}, + { + text: ( +
+ {snapshotId} + + Historical snapshot + +
+ ), + }, + ]; + + const tabs = [ + { + text: 'Definition', + href: `/snapshot/${pipelinePath}/definition`, + }, + { + text: 'Runs', + href: `/snapshot/${pipelinePath}/runs`, + }, + ]; + + return ( + <> + +
+ {pipelineName ? ( +
+ Pipeline: {pipelineName} +
+ ) : null} +
+ + ); + }} +
+
+ ); +}; + +const Mono = styled.div` + font-family: monospace; + font-size: 16px; + margin-right: 12px; +`;