Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ui] New Evaluations list table #25908

Merged
merged 1 commit into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Table} from '@dagster-io/ui-components';

import {EvaluationListRow} from './EvaluationListRow';
import {AssetViewDefinitionNodeFragment} from '../types/AssetView.types';
import {AssetConditionEvaluationRecordFragment} from './types/GetEvaluationsQuery.types';

interface Props {
definition: AssetViewDefinitionNodeFragment;
evaluations: AssetConditionEvaluationRecordFragment[];
}

export const EvaluationList = ({definition, evaluations}: Props) => {
return (
<Table>
<thead>
<tr>
<th>Timestamp</th>
<th style={{width: '240px'}}>Evaluation result</th>
<th style={{width: '240px'}}>Run(s)</th>
</tr>
</thead>
<tbody>
{evaluations.map((evaluation) => {
return (
<EvaluationListRow
key={evaluation.id}
evaluation={evaluation}
definition={definition}
/>
);
})}
</tbody>
</Table>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {
Box,
Button,
ButtonLink,
Colors,
Dialog,
DialogFooter,
DialogHeader,
Mono,
} from '@dagster-io/ui-components';
import {useState} from 'react';

import {EvaluationStatusTag} from './EvaluationStatusTag';
import {PolicyEvaluationTable} from './PolicyEvaluationTable';
import {AssetConditionEvaluationRecordFragment} from './types/GetEvaluationsQuery.types';
import {DEFAULT_TIME_FORMAT} from '../../app/time/TimestampFormat';
import {RunsFeedTableWithFilters} from '../../runs/RunsFeedTable';
import {TimestampDisplay} from '../../schedules/TimestampDisplay';
import {AssetViewDefinitionNodeFragment} from '../types/AssetView.types';

interface Props {
definition: AssetViewDefinitionNodeFragment;
evaluation: AssetConditionEvaluationRecordFragment;
}

export const EvaluationListRow = ({evaluation, definition}: Props) => {
const [isOpen, setIsOpen] = useState(false);
// const [selectedPartition, setSelectedPartition] = useState<string | null>(null);

return (
<>
<tr>
<td style={{verticalAlign: 'middle'}}>
<ButtonLink onClick={() => setIsOpen(true)}>
<TimestampDisplay
timestamp={evaluation.timestamp}
timeFormat={{...DEFAULT_TIME_FORMAT, showSeconds: true}}
/>
</ButtonLink>
</td>
<td style={{verticalAlign: 'middle'}}>
<EvaluationStatusTag
definition={definition}
selectedEvaluation={evaluation}
selectPartition={() => {}}
/>
</td>
<td style={{verticalAlign: 'middle'}}>
<EvaluationRunInfo evaluation={evaluation} />
</td>
</tr>
<Dialog
isOpen={isOpen}
onClose={() => setIsOpen(false)}
style={{
width: '80vw',
maxWidth: '1400px',
minWidth: '800px',
height: '80vh',
minHeight: '400px',
maxHeight: '1400px',
}}
>
<Box flex={{direction: 'column'}} style={{height: '100%'}}>
<DialogHeader
icon="automation"
label={
<div>
Evaluation details:{' '}
<TimestampDisplay
timestamp={evaluation.timestamp}
timeFormat={{...DEFAULT_TIME_FORMAT, showSeconds: true}}
/>
</div>
}
/>
<div style={{flex: 1, overflowY: 'auto'}}>
<PolicyEvaluationTable
assetKeyPath={definition?.assetKey.path ?? null}
evaluationId={evaluation.evaluationId}
evaluationNodes={
!evaluation.isLegacy
? evaluation.evaluationNodes
: // : selectedPartition && specificPartitionData?.assetConditionEvaluationForPartition
// ? specificPartitionData.assetConditionEvaluationForPartition.evaluationNodes
evaluation.evaluation.evaluationNodes
}
isLegacyEvaluation={evaluation.isLegacy}
rootUniqueId={evaluation.evaluation.rootUniqueId}
// todo dish
selectPartition={() => {}}
/>
</div>
<div style={{flexGrow: 0}}>
<DialogFooter topBorder>
<Button onClick={() => setIsOpen(false)}>Done</Button>
</DialogFooter>
</div>
</Box>
</Dialog>
</>
);
};

const EvaluationRunInfo = ({evaluation}: {evaluation: AssetConditionEvaluationRecordFragment}) => {
const {runIds} = evaluation;
const [isOpen, setIsOpen] = useState(false);

if (runIds.length === 0) {
return <span style={{color: Colors.textDisabled()}}>None</span>;
}

if (runIds.length === 1) {
return (
<Box flex={{direction: 'row', gap: 4}}>
<Mono>{runIds[0]}</Mono>
</Box>
);
}

return (
<>
<ButtonLink onClick={() => setIsOpen(true)}>{runIds.length} runs</ButtonLink>
<Dialog
isOpen={isOpen}
onClose={() => setIsOpen(false)}
style={{
width: '80vw',
maxWidth: '1400px',
minWidth: '800px',
height: '80vh',
minHeight: '400px',
maxHeight: '1400px',
}}
>
<Box flex={{direction: 'column'}} style={{height: '100%'}}>
<DialogHeader
label={
<>
Runs at{' '}
<TimestampDisplay
timestamp={evaluation.timestamp}
timeFormat={{...DEFAULT_TIME_FORMAT, showSeconds: true}}
/>
</>
}
/>
<div style={{flex: 1, overflowY: 'auto'}}>
<RunsFeedTableWithFilters filter={{runIds}} />
</div>
<DialogFooter topBorder>
<Button onClick={() => setIsOpen(false)}>Done</Button>
</DialogFooter>
</Box>
</Dialog>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {Box, Colors, Icon, Popover, Tag} from '@dagster-io/ui-components';
import {useMemo} from 'react';

import {PartitionSubsetList} from './PartitionSubsetList';
import {AssetConditionEvaluationRecordFragment} from './types/GetEvaluationsQuery.types';
import {AssetViewDefinitionNodeFragment} from '../types/AssetView.types';

interface Props {
definition: AssetViewDefinitionNodeFragment;
selectedEvaluation: AssetConditionEvaluationRecordFragment;
selectPartition: (partitionKey: string | null) => void;
}

export const EvaluationStatusTag = ({definition, selectedEvaluation, selectPartition}: Props) => {
const evaluation = selectedEvaluation?.evaluation;
const rootEvaluationNode = useMemo(
() => evaluation?.evaluationNodes.find((node) => node.uniqueId === evaluation.rootUniqueId),
[evaluation],
);
const rootUniqueId = evaluation?.rootUniqueId;

const partitionDefinition = definition?.partitionDefinition;
const assetKeyPath = definition?.assetKey.path || [];
const numRequested = selectedEvaluation?.numRequested;

const numTrue =
rootEvaluationNode?.__typename === 'PartitionedAssetConditionEvaluationNode'
? rootEvaluationNode.numTrue
: null;

if (numRequested) {
if (partitionDefinition && rootUniqueId && numTrue) {
return (
<Box flex={{direction: 'row', gap: 4, alignItems: 'center'}}>
<Popover
interactionKind="hover"
placement="bottom"
hoverOpenDelay={50}
hoverCloseDelay={50}
content={
<PartitionSubsetList
description="Requested assets"
assetKeyPath={assetKeyPath}
evaluationId={selectedEvaluation.evaluationId}
nodeUniqueId={rootUniqueId}
selectPartition={selectPartition}
/>
}
>
<Tag intent="success">
<Box flex={{direction: 'row', gap: 4, alignItems: 'center'}}>
<Icon name="check_filled" color={Colors.accentGreen()} />
{numRequested} requested
</Box>
</Tag>
</Popover>
</Box>
);
}

return (
<Tag intent="success">
<Box flex={{direction: 'row', gap: 4, alignItems: 'center'}}>
<Icon name="check_filled" color={Colors.accentGreen()} />
Requested
</Box>
</Tag>
);
}

return (
<Tag>
<Box flex={{direction: 'row', gap: 4, alignItems: 'center'}}>
<Icon name="check_missing" color={Colors.accentGray()} />
Not requested
</Box>
</Tag>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import faker from 'faker';

import {
buildAssetConditionEvaluation,
buildAssetConditionEvaluationRecord,
buildAutomationConditionEvaluationNode,
} from '../../../graphql/types';

const ONE_MINUTE = 60 * 1000;

export const buildEvaluationRecordsForList = (length: number) => {
const now = Date.now();
const evaluationId = 100;
return new Array(length).fill(null).map((_, ii) => {
const evaluationNodes = new Array(30).fill(null).map((_, jj) => {
const id = faker.lorem.word();
return buildAutomationConditionEvaluationNode({
startTimestamp: 0 + jj,
endTimestamp: 10 + jj,
uniqueId: id,
userLabel: faker.lorem.word(),
isPartitioned: false,
numTrue: 0,
});
});

return buildAssetConditionEvaluationRecord({
id: `evaluation-${ii}`,
evaluationId: `${evaluationId + ii}`,
evaluation: buildAssetConditionEvaluation({
rootUniqueId: 'my-root',
}),
timestamp: (now - ONE_MINUTE * ii) / 1000,
numRequested: Math.random() > 0.5 ? 1 : 0,
runIds: Array.from({length: Math.floor(Math.random() * 5)}).map(() =>
faker.datatype.uuid().slice(0, 8),
),
isLegacy: false,
rootUniqueId: 'my-root',
evaluationNodes: [
buildAutomationConditionEvaluationNode({
startTimestamp: 0,
endTimestamp: 1000,
uniqueId: 'my-root',
userLabel: faker.lorem.word(),
isPartitioned: false,
numTrue: 0,
childUniqueIds: evaluationNodes.map((node) => node.uniqueId),
}),
...evaluationNodes,
],
});
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {MockedProvider} from '@apollo/client/testing';

import {buildAssetNode} from '../../../graphql/types';
import {EvaluationList} from '../EvaluationList';
import {buildEvaluationRecordsForList} from '../__fixtures__/EvaluationList.fixtures';

// eslint-disable-next-line import/no-default-export
export default {
title: 'Asset Details/Automaterialize/EvaluationList',
component: EvaluationList,
};

export const Default = () => {
const definition = buildAssetNode({
id: '1',
groupName: '1',
isMaterializable: true,
partitionDefinition: null,
partitionKeysByDimension: [],
});

const evaluations = buildEvaluationRecordsForList(25);

return (
<MockedProvider>
<EvaluationList definition={definition} evaluations={evaluations} />
</MockedProvider>
);
};