Skip to content

Commit

Permalink
Time systems plugin (#1342)
Browse files Browse the repository at this point in the history
* add UI plugin architecture with initial support scoped to time systems
* use time systems plugin to display and parse times throughout the plans and plan pages
* load time plugin if env var set
* hold shift to display additional time formats in timeline tooltip when hovering over activities and resources

---------

Co-authored-by: Chet Joswig <[email protected]>
  • Loading branch information
AaronPlave and joswig authored Jul 19, 2024
1 parent 3c53739 commit 397f2d5
Show file tree
Hide file tree
Showing 43 changed files with 1,065 additions and 427 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ PUBLIC_HASURA_CLIENT_URL=http://localhost:8080/v1/graphql
PUBLIC_HASURA_SERVER_URL=http://localhost:8080/v1/graphql
PUBLIC_HASURA_WEB_SOCKET_URL=ws://localhost:8080/v1/graphql
PUBLIC_AUTH_SSO_ENABLED=false
PUBLIC_TIME_PLUGIN_ENABLED=false
# VITE_HOST=localhost.jpl.nasa.gov
# VITE_HTTPS=true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ e2e-test-results
node_modules
/package
/static/version.json
/static/resources/*
/.svelte-kit
test-results
unit-test-results
Expand Down
21 changes: 11 additions & 10 deletions docs/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

This document provides detailed information about environment variables for Aerie UI.

| Name | Description | Type | Default |
| -------------------------------- | --------------------------------------------------------------------------------------------------------- | -------- | -------------------------------- |
| `ORIGIN` | Url of where the UI is served from. See the [Svelte Kit Adapter Node docs][svelte-kit-adapter-node-docs]. | `string` | http://localhost |
| `PUBLIC_AERIE_FILE_STORE_PREFIX` | Prefix to prepend to files uploaded through simulation configuration. | `string` | /usr/src/app/merlin_file_store/ |
| `PUBLIC_AUTH_SSO_ENABLED` | Whether to use the SSO-based auth flow, or the /login page auth flow | `string` | false |
| `PUBLIC_GATEWAY_CLIENT_URL` | Url of the Gateway as called from the client (i.e. web browser) | `string` | http://localhost:9000 |
| `PUBLIC_GATEWAY_SERVER_URL` | Url of the Gateway as called from the server (i.e. Node.js container) | `string` | http://localhost:9000 |
| `PUBLIC_HASURA_CLIENT_URL` | Url of Hasura as called from the client (i.e. web browser) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_SERVER_URL` | Url of Hasura as called from the server (i.e. Node.js container) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_WEB_SOCKET_URL` | Url of Hasura called to establish a web-socket connection from the client | `string` | ws://localhost:8080/v1/graphql |
| Name | Description | Type | Default |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------- |
| `ORIGIN` | Url of where the UI is served from. See the [Svelte Kit Adapter Node docs][svelte-kit-adapter-node-docs]. | `string` | http://localhost |
| `PUBLIC_AERIE_FILE_STORE_PREFIX` | Prefix to prepend to files uploaded through simulation configuration. | `string` | /usr/src/app/merlin_file_store/ |
| `PUBLIC_AUTH_SSO_ENABLED` | Whether to use the SSO-based auth flow, or the /login page auth flow | `string` | false |
| `PUBLIC_GATEWAY_CLIENT_URL` | Url of the Gateway as called from the client (i.e. web browser) | `string` | http://localhost:9000 |
| `PUBLIC_GATEWAY_SERVER_URL` | Url of the Gateway as called from the server (i.e. Node.js container) | `string` | http://localhost:9000 |
| `PUBLIC_HASURA_CLIENT_URL` | Url of Hasura as called from the client (i.e. web browser) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_SERVER_URL` | Url of Hasura as called from the server (i.e. Node.js container) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_WEB_SOCKET_URL` | Url of Hasura called to establish a web-socket connection from the client | `string` | ws://localhost:8080/v1/graphql |
| `PUBLIC_TIME_PLUGIN_ENABLED` | Whether the client should load a user-supplied `time-plugin.js` plugin from the `static/resources` directory. | `string` | false |
2 changes: 1 addition & 1 deletion e2e-tests/tests/plan-merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ test.afterAll(async () => {
});

test.describe.serial('Plan Merge', () => {
const newActivityStartTime: string = '2022-005T00:00:00.000';
const newActivityStartTime: string = '2022-005T00:00:00';
const planBranchName = uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals] });

test('Add an activity to the parent plan', async () => {
Expand Down
23 changes: 15 additions & 8 deletions src/components/activity/ActivityDirectiveChangelog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import HistoryIcon from '@nasa-jpl/stellar/icons/history.svg?component';
import { createEventDispatcher, onMount } from 'svelte';
import { plan } from '../../stores/plan';
import { plugins } from '../../stores/plugins';
import type {
ActivityDirective,
ActivityDirectiveRevision,
Expand All @@ -15,7 +16,7 @@
import effects from '../../utilities/effects';
import { permissionHandler } from '../../utilities/permissionHandler';
import { featurePermissions } from '../../utilities/permissions';
import { convertUsToDurationString, getDoyTimeFromInterval } from '../../utilities/time';
import { convertUsToDurationString, formatDate, getUnixEpochTimeFromInterval } from '../../utilities/time';
import { tooltip } from '../../utilities/tooltip';
const dispatch = createEventDispatcher<{
Expand Down Expand Up @@ -48,7 +49,7 @@
let effectiveRevisionArguments: (ArgumentsMap | undefined)[];
$: effectiveRevisionArguments = [];
function formatDate(dateString: string) {
function formatRevisionDate(dateString: string) {
return new Date(dateString).toLocaleString(undefined, {
day: 'numeric',
hour: 'numeric',
Expand Down Expand Up @@ -112,12 +113,18 @@
// Manually check remaining fields that could have changed and require extra formatting
if (current.start_offset !== previous.start_offset) {
const currentStartTimeDoy = getDoyTimeFromInterval(planStartTimeYmd, current.start_offset);
const previousStartTimeDoy = getDoyTimeFromInterval(planStartTimeYmd, previous.start_offset);
const currentStartTime = formatDate(
new Date(getUnixEpochTimeFromInterval(planStartTimeYmd, current.start_offset)),
$plugins.time.primary.format,
);
const previousStartTime = formatDate(
new Date(getUnixEpochTimeFromInterval(planStartTimeYmd, previous.start_offset)),
$plugins.time.primary.format,
);
differences['Start Time (UTC)'] = {
currentValue: currentStartTimeDoy,
previousValue: previousStartTimeDoy,
differences[`Start Time (${$plugins.time.primary.label})`] = {
currentValue: currentStartTime,
previousValue: previousStartTime,
};
}
Expand Down Expand Up @@ -240,7 +247,7 @@
{#if activityRevisionChangeMap.length}
{#each activityRevisions as revision, i}
<div class="activity-revision">
<div class="date st-typography-medium">{formatDate(revision.changed_at)}</div>
<div class="date st-typography-medium">{formatRevisionDate(revision.changed_at)}</div>
<div
class="change-summary st-typography-body"
use:tooltip={{ content: activityRevisionChangeMap[i].name, placement: 'top' }}
Expand Down
42 changes: 26 additions & 16 deletions src/components/activity/ActivityDirectiveForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { activityErrorRollupsMap, activityValidationErrors } from '../../stores/errors';
import { field } from '../../stores/form';
import { plan, planReadOnly } from '../../stores/plan';
import { plugins } from '../../stores/plugins';
import type {
ActivityDirective,
ActivityDirectiveId,
Expand All @@ -37,9 +38,9 @@
import { permissionHandler } from '../../utilities/permissionHandler';
import { featurePermissions } from '../../utilities/permissions';
import { pluralize } from '../../utilities/text';
import { getDoyTime, getDoyTimeFromInterval, getIntervalFromDoyRange } from '../../utilities/time';
import { formatDate, getDoyTime, getIntervalFromDoyRange, getUnixEpochTimeFromInterval } from '../../utilities/time';
import { tooltip } from '../../utilities/tooltip';
import { required, timestamp } from '../../utilities/validators';
import { required } from '../../utilities/validators';
import Collapse from '../Collapse.svelte';
import ActivityMetadataField from '../activityMetadata/ActivityMetadataField.svelte';
import DatePickerField from '../form/DatePickerField.svelte';
Expand Down Expand Up @@ -83,11 +84,8 @@
let numOfUserChanges: number = 0;
let parameterErrorMap: Record<string, string[]> = {};
let parametersWithErrorsCount: number = 0;
let startTimeDoy: string = getDoyTimeFromInterval(
planStartTimeYmd,
revision ? revision.start_offset : activityDirective.start_offset,
);
let startTimeDoyField: FieldStore<string> = field<string>(startTimeDoy, [required, timestamp]);
let startTime: string;
let startTimeField: FieldStore<string>;
$: if (user !== null && $plan !== null) {
hasUpdatePermission =
Expand All @@ -99,11 +97,17 @@
$: highlightKeysMap = keyByBoolean(highlightKeys);
$: activityType =
(activityTypes ?? []).find(({ name: activityTypeName }) => activityDirective?.type === activityTypeName) ?? null;
$: startTimeDoy = getDoyTimeFromInterval(
planStartTimeYmd,
revision ? revision.start_offset : activityDirective.start_offset,
);
$: startTimeDoyField.validateAndSet(startTimeDoy);
$: {
const startTimeMs = getUnixEpochTimeFromInterval(
planStartTimeYmd,
revision ? revision.start_offset : activityDirective.start_offset,
);
startTime = formatDate(new Date(startTimeMs), $plugins.time.primary.format);
}
$: startTimeField = field<string>(startTime, [required, $plugins.time.primary.validate]);
$: activityNameField = field<string>(activityDirective.name);
$: startTimeField.validateAndSet(startTime);
$: activityNameField.validateAndSet(activityDirective.name);
$: if (activityType && activityDirective.arguments) {
Expand Down Expand Up @@ -369,10 +373,15 @@
}
function onUpdateStartTime() {
if ($startTimeDoyField.valid && startTimeDoy !== $startTimeDoyField.value) {
if ($startTimeField.valid && startTime !== $startTimeField.value) {
const { id } = activityDirective;
const planStartTimeDoy = getDoyTime(new Date(planStartTimeYmd));
const start_offset = getIntervalFromDoyRange(planStartTimeDoy, $startTimeDoyField.value);
const startTimeDate = $plugins.time.primary.parse($startTimeField.value);
if (!startTimeDate) {
return;
}
const startTimeDoy = getDoyTime(startTimeDate);
const start_offset = getIntervalFromDoyRange(planStartTimeDoy, startTimeDoy);
if ($plan) {
effects.updateActivityDirective($plan, id, { start_offset }, activityType, user);
}
Expand Down Expand Up @@ -556,9 +565,10 @@

<Highlight highlight={highlightKeysMap.start_offset}>
<DatePickerField
useFallback={!$plugins.time.enableDatePicker}
disabled={!editable || activityDirective.anchor_id !== null}
field={startTimeDoyField}
label="Start Time (UTC) - YYYY-DDDThh:mm:ss"
field={startTimeField}
label={`Start Time (${$plugins.time.primary.label}) - ${$plugins.time.primary.formatString}`}
layout="inline"
name="start-time"
use={[
Expand Down
38 changes: 31 additions & 7 deletions src/components/activity/ActivityDirectivesTablePanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,30 @@
<script lang="ts">
import TableFillIcon from '@nasa-jpl/stellar/icons/table_fill.svg?component';
import TableFitIcon from '@nasa-jpl/stellar/icons/table_fit.svg?component';
import type { ColDef, ColumnResizedEvent, ColumnState, ValueGetterParams } from 'ag-grid-community';
import type {
ColDef,
ColumnResizedEvent,
ColumnState,
ICellRendererParams,
ValueGetterParams,
} from 'ag-grid-community';
import { debounce } from 'lodash-es';
import { InvalidDate } from '../../constants/time';
import { activityDirectivesMap, selectActivity, selectedActivityDirectiveId } from '../../stores/activities';
import { activityErrorRollupsMap } from '../../stores/errors';
import { plan, planReadOnly } from '../../stores/plan';
import { plugins } from '../../stores/plugins';
import { view, viewTogglePanel, viewUpdateActivityDirectivesTable } from '../../stores/views';
import type { ActivityDirective } from '../../types/activity';
import type { User } from '../../types/app';
import type { AutoSizeColumns, ViewGridSection, ViewTable } from '../../types/view';
import { filterEmpty } from '../../utilities/generic';
import { getDoyTime, getUnixEpochTimeFromInterval } from '../../utilities/time';
import { formatDate, getUnixEpochTimeFromInterval } from '../../utilities/time';
import { tooltip } from '../../utilities/tooltip';
import GridMenu from '../menus/GridMenu.svelte';
import type DataGrid from '../ui/DataGrid/DataGrid.svelte';
import DataGrid from '../ui/DataGrid/DataGrid.svelte';
import { tagsCellRenderer, tagsFilterValueGetter } from '../ui/DataGrid/DataGridTags';
import IconCellRenderer from '../ui/IconCellRenderer.svelte';
import Panel from '../ui/Panel.svelte';
import ActivityDirectivesTable from './ActivityDirectivesTable.svelte';
import ActivityTableMenu from './ActivityTableMenu.svelte';
Expand All @@ -39,6 +48,7 @@
$: activityDirectivesTable = $view?.definition.plan.activityDirectivesTable;
$: autoSizeColumns = activityDirectivesTable?.autoSizeColumns;
/* eslint-disable sort-keys */
$: defaultColumnDefinitions = {
anchor_id: {
field: 'anchor_id',
Expand Down Expand Up @@ -162,18 +172,32 @@
resizable: true,
sortable: true,
},
start_time_ms: {
derived_start_time: {
field: 'start_time_ms',
filter: 'text',
headerName: 'Absolute Start Time (UTC)',
hide: true,
headerName: `Absolute Start Time (${$plugins.time.primary.label})`,
hide: false,
resizable: true,
sortable: true,
valueGetter: (params: ValueGetterParams<ActivityDirective>) => {
if ($plan && params && params.data && typeof params.data.start_time_ms === 'number') {
return getDoyTime(new Date(params.data.start_time_ms), false);
return formatDate(new Date(params.data.start_time_ms), $plugins.time.primary.format);
}
return '';
},
cellRenderer: (params: ICellRendererParams<ActivityDirective>) => {
if (params.value !== InvalidDate) {
return params.value;
}
const div = document.createElement('div');
new IconCellRenderer({
props: { type: 'error' },
target: div,
});
return div;
},
},
tags: {
autoHeight: true,
Expand Down
1 change: 0 additions & 1 deletion src/components/activity/ActivityFormPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,6 @@
activityTypes={$activityTypes}
filteredExpansionSequences={$filteredExpansionSequences}
modelId={$modelId}
planStartTimeYmd={$plan.start_time}
simulationDatasetId={$simulationDatasetId}
span={$selectedSpan}
spansMap={$spansMap}
Expand Down
28 changes: 16 additions & 12 deletions src/components/activity/ActivitySpanForm.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<svelte:options immutable={true} />

<script lang="ts">
import { plugins } from '../../stores/plugins';
import type { ActivityType } from '../../types/activity';
import type { User } from '../../types/app';
import type { ExpansionSequence } from '../../types/expansion';
Expand All @@ -10,7 +11,7 @@
import { getSpanRootParent } from '../../utilities/activities';
import effects from '../../utilities/effects';
import { getFormParameters } from '../../utilities/parameters';
import { getDoyTimeFromInterval, getUnixEpochTime } from '../../utilities/time';
import { formatDate } from '../../utilities/time';
import { tooltip } from '../../utilities/tooltip';
import Collapse from '../Collapse.svelte';
import Input from '../form/Input.svelte';
Expand All @@ -20,15 +21,14 @@
export let activityTypes: ActivityType[] = [];
export let filteredExpansionSequences: ExpansionSequence[] = [];
export let modelId: number;
export let planStartTimeYmd: string;
export let simulationDatasetId: number = -1;
export let span: Span;
export let spansMap: SpansMap = {};
export let spanUtilityMaps: SpanUtilityMaps;
export let user: User | null;
let activityType: ActivityType | null = null;
let endTimeDoy: string | null = null;
let endTime: string | null = null;
let formParametersComputedAttributes: FormParameter[] = [];
let formParameters: FormParameter[] = [];
let hasComputedAttributes: boolean = false;
Expand All @@ -37,18 +37,18 @@
let rootSpan: Span | null;
let rootSpanHasChildren: boolean;
let seqId: string | null;
let startTimeDoy: string;
let startTime: string;
$: activityType = (activityTypes ?? []).find(({ name: activityTypeName }) => span.type === activityTypeName) ?? null;
$: rootSpan = getSpanRootParent(spansMap, span.id);
$: rootSpanHasChildren = (rootSpan && spanUtilityMaps.spanIdToChildIdsMap[rootSpan.id]?.length > 0) ?? false;
$: startTimeDoy = getDoyTimeFromInterval(planStartTimeYmd, span.start_offset);
$: startTime = formatDate(new Date(span.startMs), $plugins.time.primary.format);
$: if (span.duration) {
const startTimeISO = new Date(getUnixEpochTime(startTimeDoy)).toISOString();
endTimeDoy = getDoyTimeFromInterval(startTimeISO, span.duration);
endTime = formatDate(new Date(span.endMs), $plugins.time.primary.format);
} else {
endTimeDoy = null;
endTime = null;
}
$: if (activityType && span.attributes.arguments) {
Expand Down Expand Up @@ -168,13 +168,17 @@
</Input>

<Input layout="inline">
<label use:tooltip={{ content: 'Start Time (UTC)', placement: 'top' }} for="startTime">Start Time (UTC)</label>
<input class="st-input w-100" disabled name="startTime" value={startTimeDoy} />
<label use:tooltip={{ content: 'Start Time', placement: 'top' }} for="startTime">
Start Time ({$plugins.time.primary.label})
</label>
<input class="st-input w-100" disabled name="startTime" value={startTime} />
</Input>

<Input layout="inline">
<label use:tooltip={{ content: 'End Time (UTC)', placement: 'top' }} for="endTime">End Time (UTC)</label>
<input class="st-input w-100" disabled name="endTime" value={endTimeDoy ?? 'None'} />
<label use:tooltip={{ content: 'End Time', placement: 'top' }} for="endTime">
End Time ({$plugins.time.primary.label})
</label>
<input class="st-input w-100" disabled name="endTime" value={endTime ?? 'None'} />
</Input>
</Collapse>
</fieldset>
Expand Down
Loading

0 comments on commit 397f2d5

Please sign in to comment.