Skip to content

Commit

Permalink
[dagit] Disable unloadables if no permissions (#8021)
Browse files Browse the repository at this point in the history
### Summary & Motivation

Disallow sensor/schedule toggling on unloadable sensors/schedules for viewers who don't have permissions.

### How I Tested These Changes

`yarn jest Unloadable`
  • Loading branch information
hellendag committed May 24, 2022
1 parent bf78a28 commit 8c6a8fe
Show file tree
Hide file tree
Showing 4 changed files with 381 additions and 16 deletions.
252 changes: 252 additions & 0 deletions js_modules/dagit/packages/core/src/instigation/Unloadable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import {gql, useQuery} from '@apollo/client';
import {act, render, screen} from '@testing-library/react';
import * as React from 'react';

import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorInfo';
import {TestProvider} from '../testing/TestProvider';
import {InstigationStatus} from '../types/globalTypes';

import {INSTIGATION_STATE_FRAGMENT} from './InstigationUtils';
import {UnloadableSchedules, UnloadableSensors} from './Unloadable';
import {UnloadableInstigationStatesQuery} from './types/UnloadableInstigationStatesQuery';

describe('Unloadables', () => {
const UNLOADABLE_INSTIGATION_STATES_QUERY = gql`
query UnloadableInstigationStatesQuery {
unloadableInstigationStatesOrError {
... on InstigationStates {
results {
id
...InstigationStateFragment
}
}
...PythonErrorFragment
}
}
${PYTHON_ERROR_FRAGMENT}
${INSTIGATION_STATE_FRAGMENT}
`;

const defaultMocks = {
InstigationStates: () => ({
results: () => [...new Array(1)],
}),
};

describe('Sensors', () => {
const Test = () => {
const {data, loading} = useQuery<UnloadableInstigationStatesQuery>(
UNLOADABLE_INSTIGATION_STATES_QUERY,
);
if (loading) {
return <div>Loading…</div>;
}
const states = data?.unloadableInstigationStatesOrError;
if (states && states.__typename === 'InstigationStates') {
return <UnloadableSensors sensorStates={states.results} />;
}
return <div>Error!</div>;
};

it('shows enabled ON switch if running and viewer has permission to edit sensors', async () => {
const mocks = {
InstigationState: () => ({
status: () => InstigationStatus.RUNNING,
}),
};

await act(async () => {
render(
<TestProvider
apolloProps={{mocks: [defaultMocks, mocks]}}
permissionOverrides={{edit_sensor: true}}
>
<Test />
</TestProvider>,
);
});

const switchElem: HTMLInputElement = screen.getByRole('checkbox');
expect(switchElem.checked).toBe(true);
expect(switchElem.disabled).toBe(false);
});

it('shows disabled OFF switch if stopped, even if viewer has permission to edit sensors', async () => {
const mocks = {
InstigationState: () => ({
status: () => InstigationStatus.STOPPED,
}),
};

await act(async () => {
render(
<TestProvider
apolloProps={{mocks: [defaultMocks, mocks]}}
permissionOverrides={{edit_sensor: true}}
>
<Test />
</TestProvider>,
);
});

const switchElem: HTMLInputElement = screen.getByRole('checkbox');
expect(switchElem.checked).toBe(false);
expect(switchElem.disabled).toBe(true);
});

it('shows disabled ON switch if running and no permission to edit sensors', async () => {
const mocks = {
InstigationState: () => ({
status: () => InstigationStatus.RUNNING,
}),
};

await act(async () => {
render(
<TestProvider
apolloProps={{mocks: [defaultMocks, mocks]}}
permissionOverrides={{edit_sensor: false}}
>
<Test />
</TestProvider>,
);
});

const switchElem: HTMLInputElement = screen.getByRole('checkbox');
expect(switchElem.checked).toBe(true);
expect(switchElem.disabled).toBe(true);
});

it('shows disabled OFF switch if stopped and no permission to edit sensors', async () => {
const mocks = {
InstigationState: () => ({
status: () => InstigationStatus.STOPPED,
}),
};

await act(async () => {
render(
<TestProvider
apolloProps={{mocks: [defaultMocks, mocks]}}
permissionOverrides={{edit_sensor: false}}
>
<Test />
</TestProvider>,
);
});

const switchElem: HTMLInputElement = screen.getByRole('checkbox');
expect(switchElem.checked).toBe(false);
expect(switchElem.disabled).toBe(true);
});
});

describe('Schedules', () => {
const Test = () => {
const {data, loading} = useQuery<UnloadableInstigationStatesQuery>(
UNLOADABLE_INSTIGATION_STATES_QUERY,
);
if (loading) {
return <div>Loading…</div>;
}
const states = data?.unloadableInstigationStatesOrError;
if (states && states.__typename === 'InstigationStates') {
return <UnloadableSchedules scheduleStates={states.results} />;
}
return <div>Error!</div>;
};

it('shows enabled ON switch if running and viewer has permission to stop schedules', async () => {
const mocks = {
InstigationState: () => ({
status: () => InstigationStatus.RUNNING,
}),
};

await act(async () => {
render(
<TestProvider
apolloProps={{mocks: [defaultMocks, mocks]}}
permissionOverrides={{stop_running_schedule: true}}
>
<Test />
</TestProvider>,
);
});

const switchElem: HTMLInputElement = screen.getByRole('checkbox');
expect(switchElem.checked).toBe(true);
expect(switchElem.disabled).toBe(false);
});

it('shows disabled OFF switch if stopped, even if viewer has permission to start schedule', async () => {
const mocks = {
InstigationState: () => ({
status: () => InstigationStatus.STOPPED,
}),
};

await act(async () => {
render(
<TestProvider
apolloProps={{mocks: [defaultMocks, mocks]}}
permissionOverrides={{start_schedule: true}}
>
<Test />
</TestProvider>,
);
});

const switchElem: HTMLInputElement = screen.getByRole('checkbox');
expect(switchElem.checked).toBe(false);
expect(switchElem.disabled).toBe(true);
});

it('shows disabled ON switch if running and no permission to stop schedules', async () => {
const mocks = {
InstigationState: () => ({
status: () => InstigationStatus.RUNNING,
}),
};

await act(async () => {
render(
<TestProvider
apolloProps={{mocks: [defaultMocks, mocks]}}
permissionOverrides={{stop_running_schedule: false}}
>
<Test />
</TestProvider>,
);
});

const switchElem: HTMLInputElement = screen.getByRole('checkbox');
expect(switchElem.checked).toBe(true);
expect(switchElem.disabled).toBe(true);
});

it('shows disabled OFF switch if stopped and no permission to start schedule', async () => {
const mocks = {
InstigationState: () => ({
status: () => InstigationStatus.STOPPED,
}),
};

await act(async () => {
render(
<TestProvider
apolloProps={{mocks: [defaultMocks, mocks]}}
permissionOverrides={{start_schedule: false}}
>
<Test />
</TestProvider>,
);
});

const switchElem: HTMLInputElement = screen.getByRole('checkbox');
expect(switchElem.checked).toBe(false);
expect(switchElem.disabled).toBe(true);
});
});
});
48 changes: 32 additions & 16 deletions js_modules/dagit/packages/core/src/instigation/Unloadable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Alert, Box, Checkbox, Colors, Group, Table, Subheading, Tooltip} from '@
import * as React from 'react';

import {useConfirmation} from '../app/CustomConfirmationProvider';
import {DISABLED_MESSAGE, usePermissions} from '../app/Permissions';
import {
displayScheduleMutationErrors,
STOP_SCHEDULE_MUTATION,
Expand Down Expand Up @@ -124,6 +125,7 @@ const UnloadableScheduleInfo = () => (

const SensorStateRow = ({sensorState}: {sensorState: InstigationStateFragment}) => {
const {id, selectorId, name, status, ticks} = sensorState;
const {canStopSensor} = usePermissions();

const [stopSensor, {loading: toggleOffInFlight}] = useMutation<StopSensor, StopSensorVariables>(
STOP_SENSOR_MUTATION,
Expand All @@ -146,18 +148,24 @@ const SensorStateRow = ({sensorState}: {sensorState: InstigationStateFragment})
}
};

const lacksPermission = status === InstigationStatus.RUNNING && !canStopSensor;
const latestTick = ticks.length ? ticks[0] : null;

const checkbox = () => {
const element = (
<Checkbox
format="switch"
disabled={toggleOffInFlight || status === InstigationStatus.STOPPED || lacksPermission}
checked={status === InstigationStatus.RUNNING}
onChange={onChangeSwitch}
/>
);
return lacksPermission ? <Tooltip content={DISABLED_MESSAGE}>{element}</Tooltip> : element;
};

return (
<tr key={name}>
<td style={{width: 60}}>
<Checkbox
format="switch"
disabled={toggleOffInFlight || status === InstigationStatus.STOPPED}
checked={status === InstigationStatus.RUNNING}
onChange={onChangeSwitch}
/>
</td>
<td style={{width: 60}}>{checkbox()}</td>
<td>
<Group direction="row" spacing={8} alignItems="center">
{name}
Expand All @@ -183,6 +191,7 @@ const SensorStateRow = ({sensorState}: {sensorState: InstigationStateFragment})
const ScheduleStateRow: React.FC<{
scheduleState: InstigationStateFragment;
}> = ({scheduleState}) => {
const {canStopRunningSchedule} = usePermissions();
const [stopSchedule, {loading: toggleOffInFlight}] = useMutation<
StopSchedule,
StopScheduleVariables
Expand All @@ -209,16 +218,23 @@ const ScheduleStateRow: React.FC<{
}
};

const lacksPermission = status === InstigationStatus.RUNNING && !canStopRunningSchedule;
const checkbox = () => {
const element = (
<Checkbox
format="switch"
checked={status === InstigationStatus.RUNNING}
disabled={status !== InstigationStatus.RUNNING || toggleOffInFlight || lacksPermission}
onChange={onChangeSwitch}
/>
);

return lacksPermission ? <Tooltip content={DISABLED_MESSAGE}>{element}</Tooltip> : element;
};

return (
<tr key={name}>
<td style={{width: 60}}>
<Checkbox
format="switch"
checked={status === InstigationStatus.RUNNING}
disabled={status !== InstigationStatus.RUNNING || toggleOffInFlight}
onChange={onChangeSwitch}
/>
</td>
<td style={{width: 60}}>{checkbox()}</td>
<td>
<Group direction="row" spacing={8} alignItems="center">
<div>{name}</div>
Expand Down

0 comments on commit 8c6a8fe

Please sign in to comment.