From f7dd62ecd9e2c8b9360801142ada59b516ab08f9 Mon Sep 17 00:00:00 2001 From: Isaac Hellendag Date: Tue, 29 Sep 2020 16:23:41 -0500 Subject: [PATCH] [dagit] Separate snapshot and current pipeline permalinks Summary: Separate a specific snapshot view from the main view of a pipeline. The snapshot permalink will have two tabs: - Definition, for the state of the snapshot - Runs, for runs specific to that snapshot ID It will also display "Historical snapshot" or "Current snapshot" tags, depending on whether the viewed snapshot ID is the current one for the pipeline. Breadcrumbing allows navigation back to the main pipeline page, where one can use the Playground and view all runs for the pipeline. Stacked on top of the change that modifies how the Runs table rows are rendered. Screenshots below. Test Plan: View all existing `pipeline@snapshot` paths, verify that they display the correct breadcrumbs, tag, and tabs. View `pipeline`-only paths, verify same. Reviewers: bengotow, alangenfeld, sashank, catherinewu, prha Reviewed By: bengotow Subscribers: alangenfeld Differential Revision: https://dagster.phacility.com/D4709 --- js_modules/dagit/src/App.tsx | 43 +------ js_modules/dagit/src/PipelineExplorerRoot.tsx | 52 +++----- js_modules/dagit/src/PipelineRunsRoot.tsx | 16 ++- js_modules/dagit/src/TokenizingField.tsx | 2 +- .../__tests__/__snapshots__/App.test.tsx.snap | 24 ++-- .../TokenizingField.test.tsx.snap | 4 +- .../src/execute/PipelineExecutionRoot.tsx | 9 +- js_modules/dagit/src/nav/PipelineNav.tsx | 113 ++++++++++++------ js_modules/dagit/src/nav/TopNav.stories.tsx | 21 ++++ js_modules/dagit/src/nav/TopNav.tsx | 2 +- .../src/partitions/PipelinePartitionsRoot.tsx | 9 +- .../src/pipelines/PipelineOverviewRoot.tsx | 10 +- .../dagit/src/pipelines/PipelineRoot.tsx | 50 ++++++++ js_modules/dagit/src/runs/RunTable.tsx | 11 +- js_modules/dagit/src/runs/RunsFilter.tsx | 14 ++- .../dagit/src/snapshots/SnapshotRoot.tsx | 108 +++++++++++++++++ 16 files changed, 348 insertions(+), 140 deletions(-) create mode 100644 js_modules/dagit/src/pipelines/PipelineRoot.tsx create mode 100644 js_modules/dagit/src/snapshots/SnapshotRoot.tsx 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; +`;