diff --git a/js_modules/dagster-ui/packages/ui-core/client.json b/js_modules/dagster-ui/packages/ui-core/client.json index e7c1569dacf9e..48690a059c17b 100644 --- a/js_modules/dagster-ui/packages/ui-core/client.json +++ b/js_modules/dagster-ui/packages/ui-core/client.json @@ -52,11 +52,11 @@ "AssetWipeMutation": "accefb0c47b3d4a980d16965e8af565afed787a8a987a03570df876bd734dc8f", "RunGroupPanelQuery": "c454b4e4c3d881b2a78361c5868212f734c458291a3cb28be8ba4a63030eb004", "InstanceBackfillsQuery": "e9baee9c4eabc561ffe1ffcb06430969883c1d1cfb469438f98d821b90d3d06a", - "InstanceConcurrencyLimitsQuery": "eff036379500d5b400ba5a0d3f4f22fad1bd42efefbeeafa16b43ca8b160c312", + "InstanceConcurrencyLimitsQuery": "bd8c406b16c8dce571454c6504becc886bee182e3a6aacdc68034f54eeb05b79", "SetConcurrencyLimit": "758e6bfdb936dff3e4e38f8e1fb447548710a2b2c66fbcad9d4f264a10a61044", "DeleteConcurrencyLimit": "03397142bc71bc17649f43dd17aabf4ea771436ebc4ee1cb40eff2c2848d7b4d", "FreeConcurrencySlots": "7363c435dba06ed2a4be96e1d9bf1f1f8d9c90533b80ff42896fe9d50879d60e", - "ConcurrencyKeyDetailsQuery": "1ef7d2f0933f52851ef7259c8d607601f4f4f7aac57d8af66da717362f8cb28f", + "ConcurrencyKeyDetailsQuery": "4badee73c642b27c5a224c978eb7f46a9d7661b5529a94e0ed5d22f9266d0b22", "RunsForConcurrencyKeyQuery": "35ebd16622a13c6aaa35577c7694bf8ffdeb16921b46c6040a407bb3095eaf75", "InstanceConfigQuery": "bcc75f020d292abb1e2d27cf924ec84a3c1a48f7f24a216e5ec0ed2864becc7a", "InstanceHealthQuery": "287f4e065bba5aba76b6c1e1a58f0f929c0b37e0065b75d5e5fd8a5bc69617b9", diff --git a/js_modules/dagster-ui/packages/ui-core/src/instance/ConcurrencyGroupTag.tsx b/js_modules/dagster-ui/packages/ui-core/src/instance/ConcurrencyGroupTag.tsx index 79d6f58bac1b8..19b0a6e63e9a8 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/instance/ConcurrencyGroupTag.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/instance/ConcurrencyGroupTag.tsx @@ -1,12 +1,12 @@ import {Box, Icon, Tag, Tooltip} from '@dagster-io/ui-components'; import {Link} from 'react-router-dom'; -import {CONCURRENCY_KEY_DETAILS_QUERY} from './InstanceConcurrency'; +import {CONCURRENCY_KEY_DETAILS_QUERY} from './InstanceConcurrencyKeyInfo'; import {useQuery} from '../apollo-client'; import { ConcurrencyKeyDetailsQuery, ConcurrencyKeyDetailsQueryVariables, -} from '../instance/types/InstanceConcurrency.types'; +} from '../instance/types/InstanceConcurrencyKeyInfo.types'; export const ConcurrencyGroupTag = ({groupName}: {groupName: string}) => { const groupPath = `/deployment/concurrency/${groupName}`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/instance/InstanceConcurrency.tsx b/js_modules/dagster-ui/packages/ui-core/src/instance/InstanceConcurrency.tsx index e8e0e20643899..ad5ffcf8bd6bb 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/instance/InstanceConcurrency.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/instance/InstanceConcurrency.tsx @@ -2,78 +2,66 @@ import { Box, Button, ButtonLink, - Colors, Dialog, DialogBody, DialogFooter, Heading, Icon, - Menu, - MenuItem, MetadataTableWIP, Mono, NonIdealState, Page, PageHeader, - Popover, Spinner, Subheading, - Table, - Tag, TextInput, - Tooltip, } from '@dagster-io/ui-components'; import {StyledRawCodeMirror} from '@dagster-io/ui-components/editor'; import * as React from 'react'; -import {Link} from 'react-router-dom'; +import {useParams} from 'react-router-dom'; +import {InstanceConcurrencyKeyInfo} from './InstanceConcurrencyKeyInfo'; import {InstancePageContext} from './InstancePageContext'; import {InstanceTabs} from './InstanceTabs'; import {ConcurrencyTable} from './VirtualizedInstanceConcurrencyTable'; import {gql, useMutation, useQuery} from '../apollo-client'; import { - ConcurrencyKeyDetailsQuery, - ConcurrencyKeyDetailsQueryVariables, - ConcurrencyLimitFragment, - ConcurrencyStepFragment, - DeleteConcurrencyLimitMutation, - DeleteConcurrencyLimitMutationVariables, - FreeConcurrencySlotsMutation, - FreeConcurrencySlotsMutationVariables, InstanceConcurrencyLimitsQuery, InstanceConcurrencyLimitsQueryVariables, RunQueueConfigFragment, - RunsForConcurrencyKeyQuery, - RunsForConcurrencyKeyQueryVariables, SetConcurrencyLimitMutation, SetConcurrencyLimitMutationVariables, } from './types/InstanceConcurrency.types'; -import {showSharedToaster} from '../app/DomUtils'; -import { - FIFTEEN_SECONDS, - QueryRefreshCountdown, - QueryRefreshState, - useQueryRefreshAtInterval, -} from '../app/QueryRefresh'; import {COMMON_COLLATOR} from '../app/Util'; import {useTrackPageView} from '../app/analytics'; -import {RunStatus} from '../graphql/types'; import {useDocumentTitle} from '../hooks/useDocumentTitle'; -import {useQueryPersistedState} from '../hooks/useQueryPersistedState'; -import {RunStatusDot} from '../runs/RunStatusDots'; -import {failedStatuses} from '../runs/RunStatuses'; -import {titleForRun} from '../runs/RunUtils'; -import {TimeElapsed} from '../runs/TimeElapsed'; const DEFAULT_MIN_VALUE = 1; const DEFAULT_MAX_VALUE = 1000; export const InstanceConcurrencyPageContent = React.memo(() => { + const params = useParams(); + const currentPathStr = (params as any)['0']; + const currentPath: string[] = React.useMemo( + () => + (currentPathStr || '') + .split('/') + .filter((x: string) => x) + .map(decodeURIComponent), + [currentPathStr], + ); + + const concurrencyKey = currentPath.length && currentPath[0]; + if (!concurrencyKey) { + return ; + } + + return ; +}); + +export const InstanceConcurrencyIndexContent = React.memo(() => { useTrackPageView(); useDocumentTitle('Concurrency'); - const [selectedKey, setSelectedKey] = useQueryPersistedState({ - queryKey: 'key', - }); const queryResult = useQuery< InstanceConcurrencyLimitsQuery, InstanceConcurrencyLimitsQueryVariables @@ -81,30 +69,25 @@ export const InstanceConcurrencyPageContent = React.memo(() => { notifyOnNetworkStatusChange: true, }); - const refreshState = useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS); const {data} = queryResult; return (
{data ? ( <> - + + limit.concurrencyKey)} + hasSupport={data.instance.supportsConcurrencyLimits} + refetch={queryResult.refetch} + minValue={data.instance.minConcurrencyLimitValue} + maxValue={data.instance.maxConcurrencyLimitValue} + /> - limit.concurrencyKey)} - hasSupport={data.instance.supportsConcurrencyLimits} - refetch={queryResult.refetch} - minValue={data.instance.minConcurrencyLimitValue} - maxValue={data.instance.maxConcurrencyLimitValue} - selectedKey={selectedKey} - onSelectKey={setSelectedKey} - /> ) : ( @@ -123,7 +106,7 @@ export const InstanceConcurrencyPage = () => { title={{pageTitle}} tabs={} /> - + ); }; @@ -132,29 +115,13 @@ export const InstanceConcurrencyPage = () => { // eslint-disable-next-line import/no-default-export export default InstanceConcurrencyPage; -type DialogAction = - | { - actionType: 'add'; - } - | { - actionType: 'edit'; - concurrencyKey: string; - } - | { - actionType: 'delete'; - concurrencyKey: string; - } - | undefined; - -export const RunConcurrencyContent = ({ +const RunConcurrencyContent = ({ hasRunQueue, runQueueConfig, onEdit, - refreshState, }: { hasRunQueue: boolean; runQueueConfig: RunQueueConfigFragment | null | undefined; - refreshState?: QueryRefreshState; onEdit?: () => void; }) => { if (!hasRunQueue) { @@ -165,8 +132,7 @@ export const RunConcurrencyContent = ({ border="bottom" flex={{direction: 'row', alignItems: 'center', justifyContent: 'space-between'}} > - Run concurrency - {refreshState ? : null} + Run tag concurrency Run concurrency is not supported with this run coordinator. To enable run concurrency @@ -225,20 +191,14 @@ export const RunConcurrencyContent = ({ return ( <> - + {infoContent} {settings_content} ); }; -const RunConcurrencyLimitHeader = ({ - onEdit, - refreshState, -}: { - onEdit?: () => void; - refreshState?: QueryRefreshState; -}) => ( +const RunConcurrencyLimitHeader = ({onEdit}: {onEdit?: () => void}) => ( Run concurrency - {refreshState ? : null} {onEdit ? ( ) : null} @@ -428,7 +322,7 @@ const ConcurrencyLimitHeader = ({ ) => setSearch(e.target.value)} /> @@ -504,7 +398,7 @@ const AddConcurrencyLimitDialog = ({ /> - Concurrency limit ({minValue}-{maxValue}): + Pool limit ({minValue}-{maxValue}): void; - onComplete: () => void; - minValue: number; - maxValue: number; -}) => { - const [isSubmitting, setIsSubmitting] = React.useState(false); - const [limitInput, setLimitInput] = React.useState(''); - - React.useEffect(() => { - setLimitInput(''); - }, [open]); - - const [setConcurrencyLimit] = useMutation< - SetConcurrencyLimitMutation, - SetConcurrencyLimitMutationVariables - >(SET_CONCURRENCY_LIMIT_MUTATION); - - const save = async () => { - setIsSubmitting(true); - await setConcurrencyLimit({ - variables: {concurrencyKey, limit: parseInt(limitInput!.trim())}, - }); - setIsSubmitting(false); - onComplete(); - onClose(); - }; - - const title = ( - <> - Edit {concurrencyKey} - - ); - - return ( - - - Concurrency key: - - {concurrencyKey} - - - Concurrency limit ({minValue}-{maxValue}): - - - setLimitInput(e.target.value)} - placeholder={`${minValue} - ${maxValue}`} - /> - - - - - {isSubmitting ? ( - - ) : ( - - )} - - - ); -}; - -const DeleteConcurrencyLimitDialog = ({ - concurrencyKey, - open, - onClose, - onComplete, -}: { - concurrencyKey: string; - open: boolean; - onClose: () => void; - onComplete: () => void; -}) => { - const [isSubmitting, setIsSubmitting] = React.useState(false); - - const [deleteConcurrencyLimit] = useMutation< - DeleteConcurrencyLimitMutation, - DeleteConcurrencyLimitMutationVariables - >(DELETE_CONCURRENCY_LIMIT_MUTATION); - - const save = async () => { - setIsSubmitting(true); - await deleteConcurrencyLimit({variables: {concurrencyKey}}); - setIsSubmitting(false); - onComplete(); - onClose(); - }; - - const title = ( - <> - Delete {concurrencyKey} - - ); - return ( - - - Delete concurrency limit {concurrencyKey}? - - - - {isSubmitting ? ( - - ) : ( - - )} - - - ); -}; - -const ConcurrencyActionMenu = ({ - pendingStep, - onUpdate, -}: { - pendingStep: ConcurrencyStepFragment; - onUpdate: () => void; -}) => { - const [freeSlots] = useMutation< - FreeConcurrencySlotsMutation, - FreeConcurrencySlotsMutationVariables - >(FREE_CONCURRENCY_SLOTS_MUTATION); - - return ( - - { - const resp = await freeSlots({ - variables: {runId: pendingStep.runId, stepKey: pendingStep.stepKey}, - }); - if (resp.data?.freeConcurrencySlots) { - onUpdate(); - await showSharedToaster({ - intent: 'success', - icon: 'copy_to_clipboard_done', - message: 'Freed concurrency slot', - }); - } - }} - /> - { - await showSharedToaster({message: 'Freeing concurrency slots...'}); - const resp = await freeSlots({variables: {runId: pendingStep.runId}}); - if (resp.data?.freeConcurrencySlots) { - onUpdate(); - await showSharedToaster({ - intent: 'success', - icon: 'copy_to_clipboard_done', - message: 'Freed concurrency slots', - }); - } - }} - /> - - } - position="bottom-right" - > - - - ); -}; - -const ConcurrencyStepsDialog = ({ - concurrencyKey, - onClose, - title, - onUpdate, -}: { - concurrencyKey?: string | null; - title: string | React.ReactNode; - onClose: () => void; - onUpdate: () => void; -}) => { - const queryResult = useQuery( - CONCURRENCY_KEY_DETAILS_QUERY, - { - variables: { - concurrencyKey: concurrencyKey || '', - }, - skip: !concurrencyKey, - }, - ); - useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS); - const {data} = queryResult; - const refetch = React.useCallback(() => { - queryResult.refetch(); - onUpdate(); - }, [queryResult, onUpdate]); - - return ( - - - {!data ? ( - - - - ) : ( - - )} - - - - - - ); -}; - -const PendingStepsTable = ({ - keyInfo, - refresh, -}: { - keyInfo: ConcurrencyLimitFragment; - refresh: () => void; -}) => { - const runIds = [...new Set(keyInfo.pendingSteps.map((step) => step.runId))]; - const queryResult = useQuery( - RUNS_FOR_CONCURRENCY_KEY_QUERY, - { - variables: { - filter: {runIds}, - }, - skip: !keyInfo.pendingSteps.length, - }, - ); - const statusByRunId: {[id: string]: RunStatus} = {}; - const runs = - queryResult.data?.pipelineRunsOrError.__typename === 'Runs' - ? queryResult.data.pipelineRunsOrError.results - : []; - runs.forEach((run) => { - statusByRunId[run.id] = run.status; - }); - - const steps = [...keyInfo.pendingSteps]; - steps.sort((a, b) => { - if (a.priority && b.priority && a.priority !== b.priority) { - return a.priority - b.priority; - } - return a.enqueuedTimestamp - b.enqueuedTimestamp; - }); - const assignedSteps = steps.filter((step) => !!step.assignedTimestamp); - const pendingSteps = steps.filter((step) => !step.assignedTimestamp); - - const tableHeader = ( - - - Run ID - Step key - Assigned - Queued - - - Priority - - - - - - - - - ); - - if (!steps.length) { - return ( - - {tableHeader} - - - - - -
- - There are no active or pending steps for this concurrency key. - -
- ); - } - - return ( - - {tableHeader} - - {assignedSteps.map((step) => ( - - ))} - - - {pendingSteps.map((step) => ( - - ))} - -
- ); -}; - -const PendingStepRow = ({ - step, - statusByRunId, - onUpdate, -}: { - step: ConcurrencyStepFragment; - statusByRunId: {[id: string]: RunStatus}; - onUpdate: () => void; -}) => { - const runStatus = statusByRunId[step.runId]; - return ( - - - {runStatus ? ( - - - - {titleForRun({id: step.runId})} - {failedStatuses.has(runStatus) ? ( - - - - ) : null} - - - ) : ( - {titleForRun({id: step.runId})} - )} - - - {step.stepKey} - - - {step.assignedTimestamp ? ( - - ) : ( - '-' - )} - - - {step.enqueuedTimestamp ? ( - - ) : ( - '-' - )} - - {step.priority} - - - - - ); -}; - -const CONCURRENCY_STEP_FRAGMENT = gql` - fragment ConcurrencyStepFragment on PendingConcurrencyStep { - runId - stepKey - enqueuedTimestamp - assignedTimestamp - priority - } -`; -const CONCURRENCY_LIMIT_FRAGMENT = gql` - fragment ConcurrencyLimitFragment on ConcurrencyKeyInfo { - concurrencyKey - configuredLimit - slotCount - claimedSlots { - runId - stepKey - } - pendingSteps { - ...ConcurrencyStepFragment - } - } - ${CONCURRENCY_STEP_FRAGMENT} -`; const RUN_QUEUE_CONFIG_FRAGMENT = gql` fragment RunQueueConfigFragment on RunQueueConfig { maxConcurrentRuns @@ -981,11 +433,10 @@ const RUN_QUEUE_CONFIG_FRAGMENT = gql` } `; -export const INSTANCE_CONCURRENCY_LIMITS_QUERY = gql` +const INSTANCE_CONCURRENCY_LIMITS_QUERY = gql` query InstanceConcurrencyLimitsQuery { instance { id - info supportsConcurrencyLimits runQueuingSupported runQueueConfig { @@ -1007,40 +458,3 @@ const SET_CONCURRENCY_LIMIT_MUTATION = gql` setConcurrencyLimit(concurrencyKey: $concurrencyKey, limit: $limit) } `; - -const DELETE_CONCURRENCY_LIMIT_MUTATION = gql` - mutation DeleteConcurrencyLimit($concurrencyKey: String!) { - deleteConcurrencyLimit(concurrencyKey: $concurrencyKey) - } -`; - -export const FREE_CONCURRENCY_SLOTS_MUTATION = gql` - mutation FreeConcurrencySlots($runId: String!, $stepKey: String) { - freeConcurrencySlots(runId: $runId, stepKey: $stepKey) - } -`; - -export const CONCURRENCY_KEY_DETAILS_QUERY = gql` - query ConcurrencyKeyDetailsQuery($concurrencyKey: String!) { - instance { - id - concurrencyLimit(concurrencyKey: $concurrencyKey) { - ...ConcurrencyLimitFragment - } - } - } - ${CONCURRENCY_LIMIT_FRAGMENT} -`; - -const RUNS_FOR_CONCURRENCY_KEY_QUERY = gql` - query RunsForConcurrencyKeyQuery($filter: RunsFilter, $limit: Int) { - pipelineRunsOrError(filter: $filter, limit: $limit) { - ... on Runs { - results { - id - status - } - } - } - } -`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/instance/InstanceConcurrencyKeyInfo.tsx b/js_modules/dagster-ui/packages/ui-core/src/instance/InstanceConcurrencyKeyInfo.tsx new file mode 100644 index 0000000000000..773c4507a74c6 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/instance/InstanceConcurrencyKeyInfo.tsx @@ -0,0 +1,622 @@ +import { + Box, + Button, + Colors, + Dialog, + DialogBody, + DialogFooter, + Heading, + Icon, + Menu, + MenuItem, + MetadataTableWIP, + Mono, + Popover, + Spinner, + Subheading, + Table, + TextInput, + Tooltip, +} from '@dagster-io/ui-components'; +import * as React from 'react'; +import {Link} from 'react-router-dom'; + +import {gql, useMutation, useQuery} from '../apollo-client'; +import { + ConcurrencyLimitFragment, + ConcurrencyStepFragment, + DeleteConcurrencyLimitMutation, + DeleteConcurrencyLimitMutationVariables, + FreeConcurrencySlotsMutation, + FreeConcurrencySlotsMutationVariables, + SetConcurrencyLimitMutation, + SetConcurrencyLimitMutationVariables, +} from './types/InstanceConcurrency.types'; +import { + ConcurrencyKeyDetailsQuery, + ConcurrencyKeyDetailsQueryVariables, + RunsForConcurrencyKeyQuery, + RunsForConcurrencyKeyQueryVariables, +} from './types/InstanceConcurrencyKeyInfo.types'; +import {showSharedToaster} from '../app/DomUtils'; +import { + FIFTEEN_SECONDS, + QueryRefreshCountdown, + useQueryRefreshAtInterval, +} from '../app/QueryRefresh'; +import {useTrackPageView} from '../app/analytics'; +import {RunStatus} from '../graphql/types'; +import {useDocumentTitle} from '../hooks/useDocumentTitle'; +import {RunStatusDot} from '../runs/RunStatusDots'; +import {failedStatuses} from '../runs/RunStatuses'; +import {titleForRun} from '../runs/RunUtils'; +import {TimeElapsed} from '../runs/TimeElapsed'; + +const DEFAULT_MIN_VALUE = 1; +const DEFAULT_MAX_VALUE = 1000; + +export const InstanceConcurrencyKeyInfo = ({concurrencyKey}: {concurrencyKey: string}) => { + useTrackPageView(); + useDocumentTitle(`Pool: ${concurrencyKey}`); + const [showEdit, setShowEdit] = React.useState(); + const [showDelete, setShowDelete] = React.useState(false); + const queryResult = useQuery( + CONCURRENCY_KEY_DETAILS_QUERY, + { + variables: {concurrencyKey}, + }, + ); + const {data, refetch} = queryResult; + const concurrencyLimit = data?.instance.concurrencyLimit; + const refreshState = useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS); + const onDelete = () => { + // navigate to main page, show toast + console.log('deletedddd!!!'); + showSharedToaster({ + icon: 'trash', + intent: 'success', + message: 'Deleted pool limit', + }); + }; + + return ( + <> +
+ {concurrencyLimit ? ( + + + + +
+ Pools +
+
/
+
{concurrencyKey}
+
+
+ + + + setShowDelete(true)} + /> + + } + > + + + + + + +
+ + In progress + + +
+ ) : ( + + + + )} +
+ setShowEdit(false)} + onComplete={refetch} + minValue={data?.instance.minConcurrencyLimitValue || DEFAULT_MIN_VALUE} + maxValue={data?.instance.maxConcurrencyLimitValue || DEFAULT_MAX_VALUE} + /> + setShowDelete(false)} + onComplete={onDelete} + /> + + ); +}; + +const isValidLimit = ( + concurrencyLimit?: string, + minLimitValue: number = DEFAULT_MIN_VALUE, + maxLimitValue: number = DEFAULT_MAX_VALUE, +) => { + if (!concurrencyLimit) { + return false; + } + const value = parseInt(concurrencyLimit); + if (isNaN(value)) { + return false; + } + if (String(value) !== concurrencyLimit.trim()) { + return false; + } + return value >= minLimitValue && value <= maxLimitValue; +}; + +const EditConcurrencyLimitDialog = ({ + concurrencyKey, + open, + onClose, + onComplete, + minValue, + maxValue, +}: { + concurrencyKey: string; + open: boolean; + onClose: () => void; + onComplete: () => void; + minValue: number; + maxValue: number; +}) => { + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [limitInput, setLimitInput] = React.useState(''); + + React.useEffect(() => { + setLimitInput(''); + }, [open]); + + const [setConcurrencyLimit] = useMutation< + SetConcurrencyLimitMutation, + SetConcurrencyLimitMutationVariables + >(SET_CONCURRENCY_LIMIT_MUTATION); + + const save = async () => { + setIsSubmitting(true); + await setConcurrencyLimit({ + variables: {concurrencyKey, limit: parseInt(limitInput!.trim())}, + }); + setIsSubmitting(false); + onComplete(); + onClose(); + }; + + const title = ( + <> + Edit {concurrencyKey} + + ); + + return ( + + + Concurrency key: + + {concurrencyKey} + + + Concurrency limit ({minValue}-{maxValue}): + + + setLimitInput(e.target.value)} + placeholder={`${minValue} - ${maxValue}`} + /> + + + + + {isSubmitting ? ( + + ) : ( + + )} + + + ); +}; + +const DeleteConcurrencyLimitDialog = ({ + concurrencyKey, + open, + onClose, + onComplete, +}: { + concurrencyKey: string; + open: boolean; + onClose: () => void; + onComplete: () => void; +}) => { + const [isSubmitting, setIsSubmitting] = React.useState(false); + + const [deleteConcurrencyLimit] = useMutation< + DeleteConcurrencyLimitMutation, + DeleteConcurrencyLimitMutationVariables + >(DELETE_CONCURRENCY_LIMIT_MUTATION); + + const save = async () => { + setIsSubmitting(true); + await deleteConcurrencyLimit({variables: {concurrencyKey}}); + setIsSubmitting(false); + onComplete(); + onClose(); + }; + + const title = ( + <> + Delete {concurrencyKey} + + ); + return ( + + + Delete concurrency limit {concurrencyKey}? + + + + {isSubmitting ? ( + + ) : ( + + )} + + + ); +}; + +const ConcurrencyActionMenu = ({ + pendingStep, + onUpdate, +}: { + pendingStep: ConcurrencyStepFragment; + onUpdate: () => void; +}) => { + const [freeSlots] = useMutation< + FreeConcurrencySlotsMutation, + FreeConcurrencySlotsMutationVariables + >(FREE_CONCURRENCY_SLOTS_MUTATION); + + return ( + + { + const resp = await freeSlots({ + variables: {runId: pendingStep.runId, stepKey: pendingStep.stepKey}, + }); + if (resp.data?.freeConcurrencySlots) { + onUpdate(); + await showSharedToaster({ + intent: 'success', + icon: 'copy_to_clipboard_done', + message: 'Freed concurrency slot', + }); + } + }} + /> + { + await showSharedToaster({message: 'Freeing concurrency slots...'}); + const resp = await freeSlots({variables: {runId: pendingStep.runId}}); + if (resp.data?.freeConcurrencySlots) { + onUpdate(); + await showSharedToaster({ + intent: 'success', + icon: 'copy_to_clipboard_done', + message: 'Freed concurrency slots', + }); + } + }} + /> + + } + position="bottom-right" + > + + + ); +}; + +const PendingStepsTable = ({ + keyInfo, + refresh, +}: { + keyInfo: ConcurrencyLimitFragment; + refresh: () => void; +}) => { + const runIds = [...new Set(keyInfo.pendingSteps.map((step) => step.runId))]; + const queryResult = useQuery( + RUNS_FOR_CONCURRENCY_KEY_QUERY, + { + variables: { + filter: {runIds}, + }, + skip: !keyInfo.pendingSteps.length, + }, + ); + const statusByRunId: {[id: string]: RunStatus} = {}; + const runs = + queryResult.data?.pipelineRunsOrError.__typename === 'Runs' + ? queryResult.data.pipelineRunsOrError.results + : []; + runs.forEach((run) => { + statusByRunId[run.id] = run.status; + }); + + const steps = [...keyInfo.pendingSteps]; + steps.sort((a, b) => { + if (a.priority && b.priority && a.priority !== b.priority) { + return a.priority - b.priority; + } + return a.enqueuedTimestamp - b.enqueuedTimestamp; + }); + const assignedSteps = steps.filter((step) => !!step.assignedTimestamp); + const pendingSteps = steps.filter((step) => !step.assignedTimestamp); + + const tableHeader = ( + + + Run ID + Step key + Assigned + Queued + + + Priority + + + + + + + + + ); + + if (!steps.length) { + return ( + + {tableHeader} + + + + + +
+ + There are no active or pending steps for this pool. + +
+ ); + } + + return ( + + {tableHeader} + + {assignedSteps.map((step) => ( + + ))} + + + {pendingSteps.map((step) => ( + + ))} + +
+ ); +}; + +const PendingStepRow = ({ + step, + statusByRunId, + onUpdate, +}: { + step: ConcurrencyStepFragment; + statusByRunId: {[id: string]: RunStatus}; + onUpdate: () => void; +}) => { + const runStatus = statusByRunId[step.runId]; + return ( + + + {runStatus ? ( + + + + {titleForRun({id: step.runId})} + {failedStatuses.has(runStatus) ? ( + + + + ) : null} + + + ) : ( + {titleForRun({id: step.runId})} + )} + + + {step.stepKey} + + + {step.assignedTimestamp ? ( + + ) : ( + '-' + )} + + + {step.enqueuedTimestamp ? ( + + ) : ( + '-' + )} + + {step.priority} + + + + + ); +}; + +const CONCURRENCY_STEP_FRAGMENT = gql` + fragment ConcurrencyStepFragment on PendingConcurrencyStep { + runId + stepKey + enqueuedTimestamp + assignedTimestamp + priority + } +`; +const CONCURRENCY_LIMIT_FRAGMENT = gql` + fragment ConcurrencyLimitFragment on ConcurrencyKeyInfo { + concurrencyKey + configuredLimit + slotCount + claimedSlots { + runId + stepKey + } + pendingSteps { + ...ConcurrencyStepFragment + } + } + ${CONCURRENCY_STEP_FRAGMENT} +`; + +const SET_CONCURRENCY_LIMIT_MUTATION = gql` + mutation SetConcurrencyLimit($concurrencyKey: String!, $limit: Int!) { + setConcurrencyLimit(concurrencyKey: $concurrencyKey, limit: $limit) + } +`; + +const DELETE_CONCURRENCY_LIMIT_MUTATION = gql` + mutation DeleteConcurrencyLimit($concurrencyKey: String!) { + deleteConcurrencyLimit(concurrencyKey: $concurrencyKey) + } +`; + +export const FREE_CONCURRENCY_SLOTS_MUTATION = gql` + mutation FreeConcurrencySlots($runId: String!, $stepKey: String) { + freeConcurrencySlots(runId: $runId, stepKey: $stepKey) + } +`; + +export const CONCURRENCY_KEY_DETAILS_QUERY = gql` + query ConcurrencyKeyDetailsQuery($concurrencyKey: String!) { + instance { + id + minConcurrencyLimitValue + maxConcurrencyLimitValue + concurrencyLimit(concurrencyKey: $concurrencyKey) { + ...ConcurrencyLimitFragment + } + } + } + ${CONCURRENCY_LIMIT_FRAGMENT} +`; + +const RUNS_FOR_CONCURRENCY_KEY_QUERY = gql` + query RunsForConcurrencyKeyQuery($filter: RunsFilter, $limit: Int) { + pipelineRunsOrError(filter: $filter, limit: $limit) { + ... on Runs { + results { + id + status + } + } + } + } +`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/instance/VirtualizedInstanceConcurrencyTable.tsx b/js_modules/dagster-ui/packages/ui-core/src/instance/VirtualizedInstanceConcurrencyTable.tsx index 24bd0c5b149d6..7984449a8bc4d 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/instance/VirtualizedInstanceConcurrencyTable.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/instance/VirtualizedInstanceConcurrencyTable.tsx @@ -1,16 +1,7 @@ -import { - Box, - Button, - ButtonLink, - Icon, - Menu, - MenuItem, - Popover, - Tag, - useDelayedState, -} from '@dagster-io/ui-components'; +import {Box, Icon, useDelayedState} from '@dagster-io/ui-components'; import {useVirtualizer} from '@tanstack/react-virtual'; import {useRef} from 'react'; +import {Link} from 'react-router-dom'; import styled from 'styled-components'; import {gql, useQuery} from '../apollo-client'; @@ -24,17 +15,7 @@ import {LoadingOrNone} from '../workspace/VirtualizedWorkspaceTable'; const TEMPLATE_COLUMNS = '1fr 150px 150px 150px 150px 150px'; -export const ConcurrencyTable = ({ - concurrencyKeys, - onEdit, - onDelete, - onSelect, -}: { - concurrencyKeys: string[]; - onEdit: (key: string) => void; - onDelete: (key: string) => void; - onSelect: (key: string | undefined) => void; -}) => { +export const ConcurrencyTable = ({concurrencyKeys}: {concurrencyKeys: string[]}) => { const parentRef = useRef(null); const rowVirtualizer = useVirtualizer({ @@ -58,9 +39,6 @@ export const ConcurrencyTable = ({ @@ -80,22 +58,15 @@ const ConcurrencyHeader = () => { Assigned steps Pending steps All steps - ); }; const ConcurrencyRow = ({ concurrencyKey, - onEdit, - onDelete, - onSelect, height, start, }: { concurrencyKey: string; - onDelete: (key: string) => void; - onEdit: (key: string) => void; - onSelect: (key: string | undefined) => void; height: number; start: number; }) => { @@ -114,11 +85,15 @@ const ConcurrencyRow = ({ const {data} = queryResult; const limit = data?.instance.concurrencyLimit; + const path = `/deployment/concurrency/${concurrencyKey}`; return ( - <>{concurrencyKey} + + + {concurrencyKey} + {limit ?
{limit.slotCount}
: } @@ -141,61 +116,16 @@ const ConcurrencyRow = ({ {limit ? ( {limit.pendingSteps.length} - - { - onSelect(limit.concurrencyKey); - }} - > - View all - - ) : ( )}
- - -
); }; -const ConcurrencyLimitActionMenu = ({ - concurrencyKey, - onDelete, - onEdit, -}: { - concurrencyKey: string; - onEdit: (key: string) => void; - onDelete: (key: string) => void; -}) => { - return ( - - onEdit(concurrencyKey)} /> - onDelete(concurrencyKey)} - /> - - } - position="bottom-left" - > -