From dff98cfc4e9d1ed126923797252bd0c4e5fbf78b Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 5 Feb 2024 13:31:21 -0800 Subject: [PATCH] Constraint status improvements and fixes (#1107) * Modify status text and change Incomplete color to blue since it is used for indeterminate loading * Add new constraint stores to better track different aspects of constraint status * Use indeterminate progress for expansion and scheduling goal panel status * Indeterminate Status icon support * Pass indeterminate prop through PanelHeaderActions * Improvements and bug fixes to constraint status and display * Move status enums to enum file --- src/components/app/NavButton.svelte | 3 +- .../constraints/ConstraintListItem.svelte | 30 +++++- .../constraints/ConstraintsPanel.svelte | 33 +++++-- .../expansion/ExpansionPanel.svelte | 2 +- .../menus/ActivityStatusMenu.svelte | 2 +- src/components/menus/ViewMenu.svelte | 2 +- src/components/plan/PlanNavButton.svelte | 10 +- .../scheduling/SchedulingGoalsPanel.svelte | 2 +- .../SimulationHistoryDataset.svelte | 6 +- .../simulation/SimulationPanel.svelte | 2 +- src/components/ui/PanelHeaderActions.svelte | 5 +- src/components/ui/StatusBadge.svelte | 29 +++++- src/enums/status.ts | 10 ++ src/routes/plans/[id]/+page.svelte | 96 +++++++++++++++++-- src/stores/constraints.ts | 32 ++++++- src/stores/expansion.ts | 2 +- src/stores/scheduling.ts | 2 +- src/stores/simulation.ts | 2 +- src/utilities/effects.ts | 17 +++- src/utilities/simulation.ts | 9 +- src/utilities/status.ts | 15 +-- 21 files changed, 250 insertions(+), 61 deletions(-) create mode 100644 src/enums/status.ts diff --git a/src/components/app/NavButton.svelte b/src/components/app/NavButton.svelte index 2f6f4b721e..7ab34374cc 100644 --- a/src/components/app/NavButton.svelte +++ b/src/components/app/NavButton.svelte @@ -1,6 +1,7 @@ {#if status} - + {/if} diff --git a/src/components/ui/StatusBadge.svelte b/src/components/ui/StatusBadge.svelte index 9d726d6716..d435b0a2c0 100644 --- a/src/components/ui/StatusBadge.svelte +++ b/src/components/ui/StatusBadge.svelte @@ -2,14 +2,18 @@ import CheckIcon from '@nasa-jpl/stellar/icons/check.svg?component'; import CloseIcon from '@nasa-jpl/stellar/icons/close.svg?component'; import EditingIcon from '@nasa-jpl/stellar/icons/editing.svg?component'; + import IncompleteIcon from '@nasa-jpl/stellar/icons/incomplete.svg?component'; import MinusIcon from '@nasa-jpl/stellar/icons/minus.svg?component'; import ThreeDotsIcon from '@nasa-jpl/stellar/icons/three_dot_horizontal.svg?component'; import WarningIcon from '@nasa-jpl/stellar/icons/warning.svg?component'; - import { getColorForStatus, Status, statusColors } from '../../utilities/status'; + import HourglassIcon from 'bootstrap-icons/icons/hourglass-top.svg?component'; + import { Status } from '../../enums/status'; + import { getColorForStatus, statusColors } from '../../utilities/status'; import { tooltip } from '../../utilities/tooltip'; import ProgressRadial from './ProgressRadial.svelte'; export let badgeText: string | undefined = undefined; + export let indeterminate: boolean = false; export let status: Status | null = null; export let showTooltip: boolean = true; export let prefix: string = ''; @@ -18,13 +22,14 @@ let color: string = statusColors.gray; $: color = getColorForStatus(status); + $: fgColor = status === Status.Modified || status === Status.Unchecked ? '#110d3e' : 'unset'; {#if status !== null} {#if badgeText === undefined} @@ -35,16 +40,24 @@ {:else if status === Status.Canceled} {:else if status === Status.Incomplete} - + {#if indeterminate} + + + + {:else} + + {/if} {:else if status === Status.Modified} - + + {:else if status === Status.Unchecked} + {:else if status === Status.Pending} {:else if status === Status.PartialSuccess} {/if} {:else} -
+
{badgeText}
{/if} @@ -82,4 +95,10 @@ font-size: 12px; padding: 0.5px 5px; } + + .status-badge--incomplete-indeterminate :global(svg.st-icon) { + display: flex; + height: 11px; + width: 11px; + } diff --git a/src/enums/status.ts b/src/enums/status.ts new file mode 100644 index 0000000000..b2f3cca369 --- /dev/null +++ b/src/enums/status.ts @@ -0,0 +1,10 @@ +export enum Status { + Canceled = 'Canceled', + Complete = 'Complete', + Failed = 'Failed', + Incomplete = 'Incomplete', + Unchecked = 'Unchecked', + Modified = 'Modified', + Pending = 'Pending', + PartialSuccess = 'Partial Success', +} diff --git a/src/routes/plans/[id]/+page.svelte b/src/routes/plans/[id]/+page.svelte index a8bd18d126..a744b768d6 100644 --- a/src/routes/plans/[id]/+page.svelte +++ b/src/routes/plans/[id]/+page.svelte @@ -27,8 +27,10 @@ import CssGrid from '../../../components/ui/CssGrid.svelte'; import PlanGrid from '../../../components/ui/PlanGrid.svelte'; import ProgressLinear from '../../../components/ui/ProgressLinear.svelte'; + import StatusBadge from '../../../components/ui/StatusBadge.svelte'; import { PlanStatusMessages } from '../../../enums/planStatusMessages'; import { SearchParameters } from '../../../enums/searchParameters'; + import { Status } from '../../../enums/status'; import { activityDirectiveValidationStatuses, activityDirectives, @@ -37,7 +39,14 @@ selectActivity, selectedActivityDirectiveId, } from '../../../stores/activities'; - import { checkConstraintsStatus, constraintResponseMap, resetConstraintStores } from '../../../stores/constraints'; + import { + checkConstraintsStatus, + constraintResponseMap, + constraintsStatus, + constraintsViolationStatus, + resetConstraintStores, + uncheckedConstraintCount, + } from '../../../stores/constraints'; import { activityErrorRollups, allErrors, @@ -107,7 +116,7 @@ import { featurePermissions } from '../../../utilities/permissions'; import { formatSimulationQueuePosition, - getHumanReadableSimulationStatus, + getHumanReadableStatus, getSimulationExtent, getSimulationProgress, getSimulationProgressColor, @@ -115,7 +124,8 @@ getSimulationStatus, getSimulationTimestamp, } from '../../../utilities/simulation'; - import { Status, statusColors } from '../../../utilities/status'; + import { statusColors } from '../../../utilities/status'; + import { pluralize } from '../../../utilities/text'; import { getUnixEpochTime } from '../../../utilities/time'; import { tooltip } from '../../../utilities/tooltip'; import type { PageData } from './$types'; @@ -366,6 +376,14 @@ selectedSimulationStatus = getSimulationStatus($simulationDatasetLatest); } + $: numConstraintsViolated = Object.values($constraintResponseMap).filter( + response => response.results.violations?.length, + ).length; + + $: numConstraintsWithErrors = Object.values($constraintResponseMap).filter( + response => response.errors?.length, + ).length; + onDestroy(() => { resetActivityStores(); resetConstraintStores(); @@ -570,7 +588,7 @@
- {getHumanReadableSimulationStatus(getSimulationStatus($simulationDatasetLatest))}: + {getHumanReadableStatus(getSimulationStatus($simulationDatasetLatest))}: {#if selectedSimulationStatus === Status.Pending && $simulationDatasetLatest}
{formatSimulationQueuePosition( @@ -613,18 +631,65 @@ menuTitle="Constraint Status" buttonText="Check Constraints" hasPermission={hasCheckConstraintsPermission} + disabled={$simulationStatus !== Status.Complete} + statusBadgeText={($checkConstraintsStatus === Status.Complete || $checkConstraintsStatus === Status.Failed) && + numConstraintsViolated + numConstraintsWithErrors + $uncheckedConstraintCount > 0 + ? `${numConstraintsViolated + numConstraintsWithErrors + $uncheckedConstraintCount}` + : undefined} + buttonTooltipContent={$simulationStatus !== Status.Complete ? 'Completed simulation required' : ''} permissionError={$planReadOnly ? PlanStatusMessages.READ_ONLY : 'You do not have permission to run a constraint check'} - status={$checkConstraintsStatus} + status={$constraintsStatus} + showStatusInMenu={false} on:click={() => $plan && effects.checkConstraints($plan, data.user)} + indeterminate > -
- Constraints violated: {Object.values($constraintResponseMap).filter( - response => response.results.violations?.length, - ).length} +
+ {#if $checkConstraintsStatus} +
+ + Check constraints: {getHumanReadableStatus($checkConstraintsStatus)} +
+ {#if $checkConstraintsStatus === Status.Complete || $checkConstraintsStatus === Status.Failed} +
+ + {#if numConstraintsViolated > 0} +
+ {numConstraintsViolated} constraint{pluralize(numConstraintsViolated)} + {numConstraintsViolated !== 1 ? 'have' : 'has'} violations +
+ {:else} + No constraint violations + {/if} +
+ {#if $simulationStatus !== Status.Complete} +
+ + Simulation out-of-date +
+ {/if} + {#if numConstraintsWithErrors > 0} +
+ +
+ {numConstraintsWithErrors} constraint{pluralize(numConstraintsWithErrors)} + {numConstraintsWithErrors !== 1 ? 'have' : 'has'} compile errors +
+
+ {/if} + {#if $uncheckedConstraintCount > 0} +
+ + {$uncheckedConstraintCount} unchecked constraint{pluralize($uncheckedConstraintCount)} +
+ {/if} + {/if} + {:else} +
Constraints not checked
+ {/if}
@@ -644,6 +709,7 @@ } unsatisfied` : ''} on:click={() => effects.schedule(true, $plan, data.user)} + indeterminate > @@ -772,4 +838,16 @@ .cancel-button:hover { background: rgba(219, 81, 57, 0.08); } + + .constraints-status { + display: flex; + flex-direction: column; + gap: 4px; + } + + .constraints-status-item { + align-items: center; + display: flex; + gap: 8px; + } diff --git a/src/stores/constraints.ts b/src/stores/constraints.ts index 0953353047..4614f83072 100644 --- a/src/stores/constraints.ts +++ b/src/stores/constraints.ts @@ -1,8 +1,8 @@ import { keyBy } from 'lodash-es'; import { derived, get, writable, type Readable, type Writable } from 'svelte/store'; +import { Status } from '../enums/status'; import type { Constraint, ConstraintResponse, ConstraintResultWithName } from '../types/constraint'; import gql from '../utilities/gql'; -import type { Status } from '../utilities/status'; import { modelId, planId, planStartTimeMs } from './plan'; import { gqlSubscribable } from './subscribable'; @@ -33,6 +33,7 @@ export const constraintVisibilityMap: Readable ); export const checkConstraintsStatus: Writable = writable(null); +export const constraintsViolationStatus: Writable = writable(null); export const rawConstraintResponses: Writable = writable([]); @@ -63,6 +64,35 @@ export const constraintResponseMap: Readable = derived( + [constraints, constraintResponseMap], + ([$constraints, $constraintResponseMap]) => { + return $constraints.reduce((count, prev) => { + if (!(prev.id in $constraintResponseMap)) { + count++; + } + return count; + }, 0); + }, +); + +export const constraintsStatus: Readable = derived( + [checkConstraintsStatus, constraintsViolationStatus, uncheckedConstraintCount], + ([$checkConstraintsStatus, $constraintsViolationStatus, $uncheckedConstraintCount]) => { + if (!$checkConstraintsStatus) { + return null; + } + if ($checkConstraintsStatus !== Status.Complete) { + return $checkConstraintsStatus; + } + if ($uncheckedConstraintCount > 0) { + return Status.Unchecked; + } + + return $constraintsViolationStatus; + }, +); + export const visibleConstraintResults: Readable = derived( [constraintResponseMap, constraintVisibilityMap], ([$constraintResponseMap, $constraintVisibilityMap]) => diff --git a/src/stores/expansion.ts b/src/stores/expansion.ts index 13b3b63d98..f3a2a2183f 100644 --- a/src/stores/expansion.ts +++ b/src/stores/expansion.ts @@ -1,7 +1,7 @@ import { derived, writable, type Readable, type Writable } from 'svelte/store'; +import type { Status } from '../enums/status'; import type { ExpansionRuleSlim, ExpansionSequence, ExpansionSet } from '../types/expansion'; import gql from '../utilities/gql'; -import type { Status } from '../utilities/status'; import { simulationDatasetId } from './simulation'; import { gqlSubscribable } from './subscribable'; diff --git a/src/stores/scheduling.ts b/src/stores/scheduling.ts index ea2a9cec91..03ca263126 100644 --- a/src/stores/scheduling.ts +++ b/src/stores/scheduling.ts @@ -1,4 +1,5 @@ import { derived, writable, type Readable, type Writable } from 'svelte/store'; +import type { Status } from '../enums/status'; import { plan } from '../stores/plan'; import type { SchedulingCondition, @@ -8,7 +9,6 @@ import type { SchedulingSpecGoal, } from '../types/scheduling'; import gql from '../utilities/gql'; -import type { Status } from '../utilities/status'; import { gqlSubscribable } from './subscribable'; /* Writeable. */ diff --git a/src/stores/simulation.ts b/src/stores/simulation.ts index 1a027f4f18..c161394d1d 100644 --- a/src/stores/simulation.ts +++ b/src/stores/simulation.ts @@ -1,5 +1,6 @@ import { keyBy } from 'lodash-es'; import { derived, writable, type Readable, type Writable } from 'svelte/store'; +import { Status } from '../enums/status'; import type { Resource, ResourceType, @@ -15,7 +16,6 @@ import type { import { createSpanUtilityMaps } from '../utilities/activities'; import gql from '../utilities/gql'; import { getSimulationProgress } from '../utilities/simulation'; -import { Status } from '../utilities/status'; import { modelId, planId, planRevision } from './plan'; import { gqlSubscribable } from './subscribable'; import { view } from './views'; diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index 5aaccfbd35..8e05719851 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -4,8 +4,9 @@ import { env } from '$env/dynamic/public'; import type { CommandDictionary as AmpcsCommandDictionary } from '@nasa-jpl/aerie-ampcs'; import { get } from 'svelte/store'; import { SearchParameters } from '../enums/searchParameters'; +import { Status } from '../enums/status'; import { activityDirectives, activityDirectivesMap, selectedActivityDirectiveId } from '../stores/activities'; -import { checkConstraintsStatus, rawConstraintResponses } from '../stores/constraints'; +import { checkConstraintsStatus, constraintsViolationStatus, rawConstraintResponses } from '../stores/constraints'; import { catchError, catchSchedulingError } from '../stores/errors'; import { createExpansionRuleError, @@ -153,7 +154,6 @@ import { import { queryPermissions } from './permissions'; import { reqExtension, reqGateway, reqHasura } from './requests'; import { sampleProfiles } from './resources'; -import { Status } from './status'; import { pluralize } from './text'; import { getDoyTime, getDoyTimeFromInterval, getIntervalFromDoyRange } from './time'; import { createRow, duplicateRow } from './timeline'; @@ -301,6 +301,7 @@ const effects = { async checkConstraints(plan: Plan, user: User | null): Promise { try { checkConstraintsStatus.set(Status.Incomplete); + constraintsViolationStatus.set(null); if (plan !== null) { const { id: planId } = plan; const data = await reqHasura( @@ -322,12 +323,20 @@ const effects = { constraintResponse => !constraintResponse.success, ); + const anyViolations = successfulConstraintResults.reduce((bool, prev) => { + if (prev.violations && prev.violations.length > 0) { + bool = true; + } + return bool; + }, false); + constraintsViolationStatus.set(anyViolations ? Status.Failed : Status.Complete); + if (successfulConstraintResults.length === 0 && data.constraintResponses.length > 0) { showFailureToast('All Constraints Failed'); checkConstraintsStatus.set(Status.Failed); } else if (successfulConstraintResults.length !== data.constraintResponses.length) { - showFailureToast('Partial Constraints Checked'); - checkConstraintsStatus.set(successfulConstraintResults.length !== 0 ? Status.Incomplete : Status.Failed); + showFailureToast('Constraints Partially Checked'); + checkConstraintsStatus.set(successfulConstraintResults.length !== 0 ? Status.Failed : Status.Failed); } else { showSuccessToast('All Constraints Checked'); checkConstraintsStatus.set(Status.Complete); diff --git a/src/utilities/simulation.ts b/src/utilities/simulation.ts index 5d461e9781..638458aa55 100644 --- a/src/utilities/simulation.ts +++ b/src/utilities/simulation.ts @@ -1,6 +1,7 @@ +import { Status } from '../enums/status'; import type { SimulationDataset, SimulationDatasetSlim } from '../types/simulation'; import { compare, getNumberWithOrdinal } from './generic'; -import { Status, statusColors } from './status'; +import { statusColors } from './status'; import { getDoyTime, getUnixEpochTimeFromInterval } from './time'; /** @@ -59,14 +60,16 @@ export function getSimulationTimestamp(simulationDataset: SimulationDataset): st } /** - * Returns a human readable string representing a Simulation Status + * Returns a human readable string representing a Status */ -export function getHumanReadableSimulationStatus(status: Status | null): string { +export function getHumanReadableStatus(status: Status | null): string { if (!status) { return 'Unknown'; } if (status === Status.Complete) { return Status.Complete; + } else if (status === Status.PartialSuccess) { + return 'Partially Succeeded'; } else if (status === Status.Failed) { return Status.Failed; } else if (status === Status.Incomplete) { diff --git a/src/utilities/status.ts b/src/utilities/status.ts index 789d475adb..9a6610640d 100644 --- a/src/utilities/status.ts +++ b/src/utilities/status.ts @@ -1,14 +1,7 @@ -export enum Status { - Canceled = 'Canceled', - Complete = 'Complete', - Failed = 'Failed', - Incomplete = 'Incomplete', - Modified = 'Modified', - Pending = 'Pending', - PartialSuccess = 'PartialSuccess', -} +import { Status } from '../enums/status'; export const statusColors: Record = { + blue: '#2f80ed', gray: '#bec0c2', green: '#0eaf0a', orange: '#c58b00', @@ -25,7 +18,9 @@ export function getColorForStatus(status: Status | null): string { } else if (status === Status.Failed || status === Status.Canceled) { return statusColors.red; } else if (status === Status.Incomplete) { - return statusColors.gray; + return statusColors.blue; + } else if (status === Status.Unchecked) { + return statusColors.yellow; } else if (status === Status.Modified) { return statusColors.yellow; } else if (status === Status.Pending) {