diff --git a/.env b/.env
index eb70d13e36..32b99a5e66 100644
--- a/.env
+++ b/.env
@@ -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
diff --git a/.gitignore b/.gitignore
index fab4586cb8..4ab86311c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ e2e-test-results
node_modules
/package
/static/version.json
+/static/resources/*
/.svelte-kit
test-results
unit-test-results
diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md
index e5a6aebfb0..2329043c58 100644
--- a/docs/ENVIRONMENT.md
+++ b/docs/ENVIRONMENT.md
@@ -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 |
diff --git a/e2e-tests/tests/plan-merge.test.ts b/e2e-tests/tests/plan-merge.test.ts
index 7656c06953..22a900baa0 100644
--- a/e2e-tests/tests/plan-merge.test.ts
+++ b/e2e-tests/tests/plan-merge.test.ts
@@ -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 () => {
diff --git a/src/components/activity/ActivityDirectiveChangelog.svelte b/src/components/activity/ActivityDirectiveChangelog.svelte
index b167451216..c4011255af 100644
--- a/src/components/activity/ActivityDirectiveChangelog.svelte
+++ b/src/components/activity/ActivityDirectiveChangelog.svelte
@@ -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,
@@ -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<{
@@ -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',
@@ -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,
};
}
@@ -240,7 +247,7 @@
{#if activityRevisionChangeMap.length}
{#each activityRevisions as revision, i}
-
{formatDate(revision.changed_at)}
+
{formatRevisionDate(revision.changed_at)}
= {};
let parametersWithErrorsCount: number = 0;
- let startTimeDoy: string = getDoyTimeFromInterval(
- planStartTimeYmd,
- revision ? revision.start_offset : activityDirective.start_offset,
- );
- let startTimeDoyField: FieldStore
= field(startTimeDoy, [required, timestamp]);
+ let startTime: string;
+ let startTimeField: FieldStore;
$: if (user !== null && $plan !== null) {
hasUpdatePermission =
@@ -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(startTime, [required, $plugins.time.primary.validate]);
+ $: activityNameField = field(activityDirective.name);
+ $: startTimeField.validateAndSet(startTime);
$: activityNameField.validateAndSet(activityDirective.name);
$: if (activityType && activityDirective.arguments) {
@@ -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);
}
@@ -556,9 +565,10 @@
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';
@@ -39,6 +48,7 @@
$: activityDirectivesTable = $view?.definition.plan.activityDirectivesTable;
$: autoSizeColumns = activityDirectivesTable?.autoSizeColumns;
+ /* eslint-disable sort-keys */
$: defaultColumnDefinitions = {
anchor_id: {
field: 'anchor_id',
@@ -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) => {
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) => {
+ if (params.value !== InvalidDate) {
+ return params.value;
+ }
+ const div = document.createElement('div');
+
+ new IconCellRenderer({
+ props: { type: 'error' },
+ target: div,
+ });
+
+ return div;
+ },
},
tags: {
autoHeight: true,
diff --git a/src/components/activity/ActivityFormPanel.svelte b/src/components/activity/ActivityFormPanel.svelte
index 2b68fc6827..2bb4f751c4 100644
--- a/src/components/activity/ActivityFormPanel.svelte
+++ b/src/components/activity/ActivityFormPanel.svelte
@@ -231,7 +231,6 @@
activityTypes={$activityTypes}
filteredExpansionSequences={$filteredExpansionSequences}
modelId={$modelId}
- planStartTimeYmd={$plan.start_time}
simulationDatasetId={$simulationDatasetId}
span={$selectedSpan}
spansMap={$spansMap}
diff --git a/src/components/activity/ActivitySpanForm.svelte b/src/components/activity/ActivitySpanForm.svelte
index 34a4b665b2..23cc37d016 100644
--- a/src/components/activity/ActivitySpanForm.svelte
+++ b/src/components/activity/ActivitySpanForm.svelte
@@ -1,6 +1,7 @@
-
-
- {#if label}
- {#if layout === 'inline'}
-
- {:else}
-
+{#if !useFallback}
+
+
+ {#if label}
+ {#if layout === 'inline'}
+
+ {:else}
+
+ {/if}
{/if}
- {/if}
-
-
-
-
-
-
+
+
+
+
+
+
+{:else}
+
+
+
+
+ {}} />
+
+
+
+{/if}
diff --git a/src/components/model/ModelAssociationsListItem.svelte b/src/components/model/ModelAssociationsListItem.svelte
index cc199d274d..f4c4316e59 100644
--- a/src/components/model/ModelAssociationsListItem.svelte
+++ b/src/components/model/ModelAssociationsListItem.svelte
@@ -8,7 +8,6 @@
import { getTarget } from '../../utilities/generic';
import { permissionHandler } from '../../utilities/permissionHandler';
import { tooltip } from '../../utilities/tooltip';
- import Input from '../form/Input.svelte';
export let hasEditPermission: boolean = false;
export let isSelected: boolean = false;
@@ -121,42 +120,40 @@