Skip to content

Commit

Permalink
[dagit] Sectioned left nav (#8017)
Browse files Browse the repository at this point in the history
  • Loading branch information
hellendag committed May 24, 2022
1 parent 71802bd commit aebfe43
Show file tree
Hide file tree
Showing 6 changed files with 388 additions and 47 deletions.
1 change: 1 addition & 0 deletions js_modules/dagit/packages/core/src/app/Flags.tsx
Expand Up @@ -11,6 +11,7 @@ export enum FeatureFlag {
flagAlwaysCollapseNavigation = 'flagAlwaysCollapseNavigation',
flagDisableWebsockets = 'flagDisableWebsockets',
flagNewPartitionsView = 'flagNewPartitionsView',
flagSectionedLeftNav = 'flagSectionedLeftNav',
}

export const getFeatureFlags: () => FeatureFlag[] = memoize(
Expand Down
10 changes: 10 additions & 0 deletions js_modules/dagit/packages/core/src/app/UserSettingsRoot.tsx
Expand Up @@ -144,6 +144,16 @@ const UserSettingsRoot: React.FC<SettingsRootProps> = ({tabs}) => {
/>
),
},
{
key: 'Sectioned left nav (experimental)',
value: (
<Checkbox
format="switch"
checked={flags.includes(FeatureFlag.flagSectionedLeftNav)}
onChange={() => toggleFlag(FeatureFlag.flagSectionedLeftNav)}
/>
),
},
]}
/>
</Box>
Expand Down
84 changes: 48 additions & 36 deletions js_modules/dagit/packages/core/src/nav/FlatContentList.tsx
Expand Up @@ -25,10 +25,11 @@ interface Props {
repoPath?: string;
}

type JobItem = {
export type JobItem = {
name: string;
isJob: boolean;
label: React.ReactNode;
path: string;
repoAddress: RepoAddress;
schedule: WorkspaceRepositorySchedule | null;
sensor: WorkspaceRepositorySensor | null;
Expand All @@ -47,41 +48,13 @@ export const FlatContentList: React.FC<Props> = (props) => {
const jobs = React.useMemo(() => {
const items: JobItem[] = [];

for (const {repository, repositoryLocation} of repos) {
for (const option of repos) {
const {repository, repositoryLocation} = option;
const address = buildRepoAddress(repository.name, repositoryLocation.name);
if (!activeRepoAddresses.has(address)) {
continue;
}

const {schedules, sensors} = repository;
for (const pipeline of repository.pipelines) {
if (isAssetGroup(pipeline.name)) {
continue;
}

const {isJob, name} = pipeline;
const schedule = schedules.find((schedule) => schedule.pipelineName === name) || null;
const sensor =
sensors.find((sensor) =>
sensor.targets?.map((target) => target.pipelineName).includes(name),
) || null;
items.push({
name,
isJob,
label: (
<Label $hasIcon={!!(schedule || sensor) || !isJob}>
<TruncatingName data-tooltip={name} data-tooltip-style={LabelTooltipStyles}>
{name}
</TruncatingName>
<div style={{flex: 1}} />
{isJob ? null : <LegacyPipelineTag />}
</Label>
),
repoAddress: address,
schedule,
sensor,
});
}
items.push(...getJobItemsForOption(option));
}

return items.sort((a, b) =>
Expand Down Expand Up @@ -114,15 +87,55 @@ export const FlatContentList: React.FC<Props> = (props) => {
);
};

export const getJobItemsForOption = (option: DagsterRepoOption) => {
const items: JobItem[] = [];

const {repository, repositoryLocation} = option;
const address = buildRepoAddress(repository.name, repositoryLocation.name);

const {schedules, sensors} = repository;
for (const pipeline of repository.pipelines) {
if (isAssetGroup(pipeline.name)) {
continue;
}

const {isJob, name} = pipeline;
const schedule = schedules.find((schedule) => schedule.pipelineName === name) || null;
const sensor =
sensors.find((sensor) =>
sensor.targets?.map((target) => target.pipelineName).includes(name),
) || null;
items.push({
name,
isJob,
label: (
<Label $hasIcon={!!(schedule || sensor) || !isJob}>
<TruncatingName data-tooltip={name} data-tooltip-style={LabelTooltipStyles}>
{name}
</TruncatingName>
<div style={{flex: 1}} />
{isJob ? null : <LegacyPipelineTag />}
</Label>
),
path: workspacePathFromAddress(address, `/${isJob ? 'jobs' : 'pipelines'}/${name}`),
repoAddress: address,
schedule,
sensor,
});
}

return items;
};

interface JobItemProps {
job: JobItem;
repoPath?: string;
selector?: string;
}

const JobItem: React.FC<JobItemProps> = (props) => {
export const JobItem: React.FC<JobItemProps> = (props) => {
const {job: jobItem, repoPath, selector} = props;
const {name, isJob, label, repoAddress, schedule, sensor} = jobItem;
const {name, label, path, repoAddress, schedule, sensor} = jobItem;

const jobRepoPath = repoAddressAsString(repoAddress);

Expand Down Expand Up @@ -159,9 +172,8 @@ const JobItem: React.FC<JobItemProps> = (props) => {
return (
<ItemContainer>
<Item
key={name}
className={`${name === selector && repoPath === jobRepoPath ? 'selected' : ''}`}
to={workspacePathFromAddress(repoAddress, `/${isJob ? 'jobs' : 'pipelines'}/${name}`)}
to={path}
>
<div>{label}</div>
</Item>
Expand Down
38 changes: 27 additions & 11 deletions js_modules/dagit/packages/core/src/nav/LeftNavRepositorySection.tsx
Expand Up @@ -3,7 +3,9 @@ import * as React from 'react';
import {useLocation} from 'react-router-dom';
import styled from 'styled-components/macro';

import {useFeatureFlags} from '../app/Flags';
import {explorerPathFromString} from '../pipelines/PipelinePathUtils';
import {SectionedLeftNav} from '../ui/SectionedLeftNav';
import {DagsterRepoOption, WorkspaceContext} from '../workspace/WorkspaceContext';
import {RepoAddress} from '../workspace/types';

Expand All @@ -17,6 +19,8 @@ const LoadedRepositorySection: React.FC<{
toggleVisible: (repoAddresses: RepoAddress[]) => void;
}> = ({allRepos, visibleRepos, toggleVisible}) => {
const location = useLocation();
const {flagSectionedLeftNav} = useFeatureFlags();

const workspacePath = location.pathname.split('/workspace/').pop();
const [, repoPath, type, item, tab] =
workspacePath?.match(
Expand All @@ -30,19 +34,31 @@ const LoadedRepositorySection: React.FC<{
? explorerPathFromString(item).pipelineName
: item;

const listContent = () => {
if (visibleRepos.length) {
if (flagSectionedLeftNav) {
return <SectionedLeftNav />;
}

return (
<FlatContentList repoPath={repoPath} selector={selector} repos={visibleRepos} tab={tab} />
);
}

if (allRepos.length > 0) {
return <EmptyState>Select a repository to see a list of jobs.</EmptyState>;
}

return (
<EmptyState>
There are no repositories in this workspace. Add a repository to see a list of jobs.
</EmptyState>
);
};

return (
<Container>
<ListContainer>
{visibleRepos.length ? (
<FlatContentList repoPath={repoPath} selector={selector} repos={visibleRepos} tab={tab} />
) : allRepos.length > 0 ? (
<EmptyState>Select a repository to see a list of jobs.</EmptyState>
) : (
<EmptyState>
There are no repositories in this workspace. Add a repository to see a list of jobs.
</EmptyState>
)}
</ListContainer>
<ListContainer>{listContent()}</ListContainer>
<RepositoryLocationStateObserver />
<RepoNavItem allRepos={allRepos} selected={visibleRepos} onToggle={toggleVisible} />
</Container>
Expand Down
35 changes: 35 additions & 0 deletions js_modules/dagit/packages/core/src/ui/SectionedLeftNav.stories.tsx
@@ -0,0 +1,35 @@
import {Meta} from '@storybook/react/types-6-0';
import * as React from 'react';

import {LEFT_NAV_WIDTH} from '../nav/LeftNav';
import {StorybookProvider} from '../testing/StorybookProvider';
import {defaultMocks} from '../testing/defaultMocks';

import {SectionedLeftNav} from './SectionedLeftNav';

// eslint-disable-next-line import/no-default-export
export default {
title: 'SectionedLeftNav',
component: SectionedLeftNav,
} as Meta;

const mocks = {
Repository: () => ({
...defaultMocks.Repository(),
pipelines: () => [...new Array(15)],
}),
Workspace: () => ({
...defaultMocks.Workspace(),
locationEntries: () => [...new Array(2)],
}),
};

export const Default = () => {
return (
<StorybookProvider apolloProps={{mocks}}>
<div style={{position: 'absolute', left: 0, top: 0, height: '100%', width: LEFT_NAV_WIDTH}}>
<SectionedLeftNav />
</div>
</StorybookProvider>
);
};

0 comments on commit aebfe43

Please sign in to comment.