Skip to content

Commit

Permalink
[dagit] Use dialog for multiple schedules/sensors in left nav (#8065)
Browse files Browse the repository at this point in the history
  • Loading branch information
hellendag committed May 25, 2022
1 parent ac6007c commit 71a85ba
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 144 deletions.
141 changes: 106 additions & 35 deletions js_modules/dagit/packages/core/src/nav/FlatContentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import styled from 'styled-components/macro';

import {isAssetGroup} from '../asset-graph/Utils';
import {LegacyPipelineTag} from '../pipelines/LegacyPipelineTag';
import {humanCronString} from '../schedules/humanCronString';
import {InstigationStatus} from '../types/globalTypes';
import {
DagsterRepoOption,
Expand All @@ -17,6 +18,7 @@ import {RepoAddress} from '../workspace/types';
import {workspacePathFromAddress} from '../workspace/workspacePath';

import {Item, Items} from './RepositoryContentList';
import {ScheduleAndSensorDialog} from './ScheduleAndSensorDialog';

interface Props {
selector?: string;
Expand All @@ -25,14 +27,14 @@ interface Props {
repoPath?: string;
}

export type JobItem = {
export type JobItemType = {
name: string;
isJob: boolean;
label: React.ReactNode;
path: string;
repoAddress: RepoAddress;
schedule: WorkspaceRepositorySchedule | null;
sensor: WorkspaceRepositorySensor | null;
schedules: WorkspaceRepositorySchedule[];
sensors: WorkspaceRepositorySensor[];
};

export const FlatContentList: React.FC<Props> = (props) => {
Expand All @@ -46,7 +48,7 @@ export const FlatContentList: React.FC<Props> = (props) => {
}, [repos]);

const jobs = React.useMemo(() => {
const items: JobItem[] = [];
const items: JobItemType[] = [];

for (const option of repos) {
const {repository, repositoryLocation} = option;
Expand Down Expand Up @@ -88,7 +90,7 @@ export const FlatContentList: React.FC<Props> = (props) => {
};

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

const {repository, repositoryLocation} = option;
const address = buildRepoAddress(repository.name, repositoryLocation.name);
Expand All @@ -100,16 +102,15 @@ export const getJobItemsForOption = (option: DagsterRepoOption) => {
}

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;
const schedulesForJob = schedules.filter((schedule) => schedule.pipelineName === name);
const sensorsForJob = sensors.filter((sensor) =>
sensor.targets?.map((target) => target.pipelineName).includes(name),
);
items.push({
name,
isJob,
label: (
<Label $hasIcon={!!(schedule || sensor) || !isJob}>
<Label $hasIcon={!!(schedules.length || sensors.length) || !isJob}>
<TruncatingName data-tooltip={name} data-tooltip-style={LabelTooltipStyles}>
{name}
</TruncatingName>
Expand All @@ -119,53 +120,109 @@ export const getJobItemsForOption = (option: DagsterRepoOption) => {
),
path: workspacePathFromAddress(address, `/${isJob ? 'jobs' : 'pipelines'}/${name}`),
repoAddress: address,
schedule,
sensor,
schedules: schedulesForJob,
sensors: sensorsForJob,
});
}

return items;
};

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

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

const [showDialog, setShowDialog] = React.useState(false);

const jobRepoPath = repoAddressAsString(repoAddress);

const icon = () => {
if (!schedule && !sensor) {
const scheduleCount = schedules.length;
const sensorCount = sensors.length;

if (!scheduleCount && !sensorCount) {
return null;
}

const whichIcon = schedule ? 'schedule' : 'sensors';
const status = schedule ? schedule?.scheduleState.status : sensor?.sensorState.status;
const tooltipContent = schedule ? (
<>
Schedule: <strong>{schedule.name}</strong>
</>
) : (
<>
Sensor: <strong>{sensor?.name}</strong>
</>
);
const path = schedule ? `/schedules/${schedule.name}` : `/sensors/${sensor?.name}`;
const whichIcon = scheduleCount ? 'schedule' : 'sensors';
const needsDialog = scheduleCount > 1 || sensorCount > 1 || (scheduleCount && sensorCount);

const status = () => {
return schedules.some(
(schedule) => schedule.scheduleState.status === InstigationStatus.RUNNING,
) || sensors.some((sensor) => sensor.sensorState.status === InstigationStatus.RUNNING)
? InstigationStatus.RUNNING
: InstigationStatus.STOPPED;
};

const tooltipContent = () => {
if (scheduleCount && sensorCount) {
const scheduleString = scheduleCount > 1 ? `${scheduleCount} schedules` : '1 schedule';
const sensorString = sensorCount > 1 ? `${sensorCount} sensors` : '1 sensor';
return `${scheduleString}, ${sensorString}`;
}

if (scheduleCount) {
return scheduleCount === 1 ? (
<div>
Schedule: <strong>{humanCronString(schedules[0].cronSchedule)}</strong>
</div>
) : (
`${scheduleCount} schedules`
);
}

return sensorCount === 1 ? (
<div>
Sensor: <strong>{sensors[0].name}</strong>
</div>
) : (
`${sensorCount} sensors`
);
};

const link = () => {
const icon = (
<Icon
name={whichIcon}
color={status() === InstigationStatus.RUNNING ? Colors.Green500 : Colors.Gray600}
/>
);

if (needsDialog) {
return (
<SensorScheduleDialogButton onClick={() => setShowDialog(true)}>
{icon}
</SensorScheduleDialogButton>
);
}

const path = scheduleCount
? `/schedules/${schedules[0].name}`
: `/sensors/${sensors[0].name}`;
return <Link to={workspacePathFromAddress(repoAddress, path)}>{icon}</Link>;
};

return (
<IconWithTooltip content={tooltipContent}>
<Link to={workspacePathFromAddress(repoAddress, path)}>
<Icon
name={whichIcon}
color={status === InstigationStatus.RUNNING ? Colors.Green500 : Colors.Gray600}
<>
<IconWithTooltip content={tooltipContent()}>{link()}</IconWithTooltip>
{needsDialog ? (
<ScheduleAndSensorDialog
isOpen={showDialog}
onClose={() => setShowDialog(false)}
repoAddress={repoAddress}
schedules={schedules}
sensors={sensors}
showSwitch
/>
</Link>
</IconWithTooltip>
) : null}
</>
);
};

Expand All @@ -191,6 +248,20 @@ const Label = styled.div<{$hasIcon: boolean}>`
width: ${({$hasIcon}) => ($hasIcon ? '260px' : '280px')};
`;

const SensorScheduleDialogButton = styled.button`
background: transparent;
padding: 0;
margin: 0;
border: 0;
cursor: pointer;
:focus,
:active,
:hover {
outline: none;
}
`;

const LabelTooltipStyles = JSON.stringify({
background: Colors.Gray100,
filter: `brightness(97%)`,
Expand Down
128 changes: 128 additions & 0 deletions js_modules/dagit/packages/core/src/nav/ScheduleAndSensorDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {Box, Button, Dialog, DialogFooter, Subheading, Table} from '@dagster-io/ui';
import * as React from 'react';
import {Link} from 'react-router-dom';

import {ScheduleSwitch} from '../schedules/ScheduleSwitch';
import {humanCronString} from '../schedules/humanCronString';
import {ScheduleSwitchFragment} from '../schedules/types/ScheduleSwitchFragment';
import {SensorSwitch} from '../sensors/SensorSwitch';
import {SensorSwitchFragment} from '../sensors/types/SensorSwitchFragment';
import {RepoAddress} from '../workspace/types';
import {workspacePathFromAddress} from '../workspace/workspacePath';

interface Props {
isOpen: boolean;
onClose: () => void;
repoAddress: RepoAddress;
schedules: ScheduleSwitchFragment[];
sensors: SensorSwitchFragment[];
showSwitch?: boolean;
}

export const ScheduleAndSensorDialog = ({
isOpen,
onClose,
repoAddress,
schedules,
sensors,
showSwitch,
}: Props) => {
const scheduleCount = schedules.length;
const sensorCount = sensors.length;

const dialogTitle =
scheduleCount && sensorCount
? 'Schedules and sensors'
: scheduleCount
? 'Schedules'
: 'Sensors';

return (
<Dialog
title={dialogTitle}
canOutsideClickClose
canEscapeKeyClose
isOpen={isOpen}
style={{width: '50vw', minWidth: '600px', maxWidth: '800px'}}
onClose={onClose}
>
<Box padding={{bottom: 12}}>
{scheduleCount ? (
<>
{sensorCount ? (
<Box padding={{vertical: 16, horizontal: 24}}>
<Subheading>Schedules ({scheduleCount})</Subheading>
</Box>
) : null}
<Table>
<thead>
<tr>
{showSwitch ? <th style={{width: '80px'}} /> : null}
<th>Schedule name</th>
<th>Schedule</th>
</tr>
</thead>
<tbody>
{schedules.map((schedule) => (
<tr key={schedule.name}>
{showSwitch ? (
<td>
<ScheduleSwitch repoAddress={repoAddress} schedule={schedule} />
</td>
) : null}
<td>
<Link
to={workspacePathFromAddress(repoAddress, `/schedules/${schedule.name}`)}
>
{schedule.name}
</Link>
</td>
<td>{humanCronString(schedule.cronSchedule)}</td>
</tr>
))}
</tbody>
</Table>
</>
) : null}
{sensorCount ? (
<>
{scheduleCount ? (
<Box padding={{vertical: 16, horizontal: 24}}>
<Subheading>Sensors ({sensorCount})</Subheading>
</Box>
) : null}
<Table>
<thead>
<tr>
{showSwitch ? <th style={{width: '80px'}} /> : null}
<th>Sensor name</th>
</tr>
</thead>
<tbody>
{sensors.map((sensor) => (
<tr key={sensor.name}>
{showSwitch ? (
<td>
<SensorSwitch repoAddress={repoAddress} sensor={sensor} />
</td>
) : null}
<td>
<Link to={workspacePathFromAddress(repoAddress, `/sensors/${sensor.name}`)}>
{sensor.name}
</Link>
</td>
</tr>
))}
</tbody>
</Table>
</>
) : null}
</Box>
<DialogFooter>
<Button intent="primary" onClick={onClose}>
OK
</Button>
</DialogFooter>
</Dialog>
);
};

0 comments on commit 71a85ba

Please sign in to comment.