@@ -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;
+`;