Skip to content
Draft
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
149 changes: 126 additions & 23 deletions src/components/plan/PlanForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@
import type { PlanSnapshot as PlanSnapshotType } from '../../types/plan-snapshot';
import type { PlanTagsInsertInput, Tag, TagsChangeEvent } from '../../types/tags';
import effects from '../../utilities/effects';
import { showConfirmModal } from '../../utilities/modal';
import { permissionHandler } from '../../utilities/permissionHandler';
import { featurePermissions } from '../../utilities/permissions';
import { exportPlan } from '../../utilities/plan';
import { convertDoyToYmd, formatDate, getShortISOForDate } from '../../utilities/time';
import { computeDurationString, computePlanTimeUpdate, exportPlan } from '../../utilities/plan';
import { convertDoyToYmd, formatDate, getDoyTime, getShortISOForDate } from '../../utilities/time';
import { tooltip } from '../../utilities/tooltip';
import { removeQueryParam, setQueryParam } from '../../utilities/url';
import { required, unique } from '../../utilities/validators';
import Collapse from '../Collapse.svelte';
import Loading from '../Loading.svelte';
import DatePickerField from '../form/DatePickerField.svelte';
import Field from '../form/Field.svelte';
import Input from '../form/Input.svelte';
import CardList from '../ui/CardList.svelte';
Expand Down Expand Up @@ -63,19 +65,28 @@
),
]);
let planExporting: boolean = false;
let planStartTime: string = '';
let planEndTime: string = '';
let durationString: string = 'None';

$: startTimeField = field<string>('', [required, $plugins.time.primary.validate]);
$: endTimeField = field<string>('', [required, $plugins.time.primary.validate]);

$: permissionError = $planReadOnly ? PlanStatusMessages.READ_ONLY : 'You do not have permission to edit this plan.';
$: if (plan && plan.model) {
hasCreateSnapshotPermission = featurePermissions.planSnapshot.canCreate(user, plan, plan.model) && !$planReadOnly;
planStartTime = formatDate(new Date(plan.start_time), $plugins.time.primary.format);
const endTime = convertDoyToYmd(plan.end_time_doy);
if (endTime) {
planEndTime = formatDate(new Date(endTime), $plugins.time.primary.format);
} else {
planEndTime = '';
// Initialize start time field from plan
const formattedStartTime = formatDate(new Date(plan.start_time), $plugins.time.primary.format);
if ($startTimeField.value !== formattedStartTime && !$startTimeField.dirty) {
startTimeField.validateAndSet(formattedStartTime);
}
// Initialize end time field from plan
const endTimeYmd = convertDoyToYmd(plan.end_time_doy);
if (endTimeYmd) {
const formattedEndTime = formatDate(new Date(endTimeYmd), $plugins.time.primary.format);
if ($endTimeField.value !== formattedEndTime && !$endTimeField.dirty) {
endTimeField.validateAndSet(formattedEndTime);
}
}
updateDurationString();
}
$: {
if (plan && user) {
Expand Down Expand Up @@ -171,6 +182,73 @@
}
}

function updateDurationString() {
const startTimeMs = $plugins.time.primary.parse($startTimeField.value)?.getTime();
const endTimeMs = $plugins.time.primary.parse($endTimeField.value)?.getTime();
durationString = computeDurationString(startTimeMs, endTimeMs, $startTimeField.valid && $endTimeField.valid);
}

async function onStartTimeChanged() {
updateDurationString();
await updatePlanTimes();
}

async function onEndTimeChanged() {
updateDurationString();
await updatePlanTimes();
}

function revertTimeFields() {
if (!plan) {
return;
}
// Revert start time to original plan value
const originalStartTime = formatDate(new Date(plan.start_time), $plugins.time.primary.format);
startTimeField.validateAndSet(originalStartTime);

// Revert end time to original plan value
const endTimeYmd = convertDoyToYmd(plan.end_time_doy);
if (endTimeYmd) {
const originalEndTime = formatDate(new Date(endTimeYmd), $plugins.time.primary.format);
endTimeField.validateAndSet(originalEndTime);
}

updateDurationString();
}

async function updatePlanTimes() {
if (!plan || !$startTimeField.dirtyAndValid || !$endTimeField.dirtyAndValid) {
return;
}

const startTimeDate = $plugins.time.primary.parse($startTimeField.value);
const endTimeDate = $plugins.time.primary.parse($endTimeField.value);
if (!startTimeDate || !endTimeDate) {
return;
}

const { confirm } = await showConfirmModal(
'Update',
'Are you sure you want to change the time range of this plan? This may affect previous simulations and plan snapshots.',
'Confirm Plan Time Change',
false,
'Cancel',
);

if (!confirm) {
revertTimeFields();
return;
}

const startTimeDoy = getDoyTime(startTimeDate);
const endTimeDoy = getDoyTime(endTimeDate);

const planTimeUpdate = computePlanTimeUpdate(startTimeDoy, endTimeDoy);
if (planTimeUpdate) {
await effects.updatePlan(plan, planTimeUpdate, user);
}
}

async function onExportPlan() {
if (plan && !planExporting && activityDirectivesMap) {
planExporting = true;
Expand Down Expand Up @@ -266,20 +344,45 @@
id="modelVersion"
/>
</Input>
<DatePickerField
disabled={!hasPlanUpdatePermission}
layout="inline"
useFallback={!$plugins.time.enableDatePicker}
field={startTimeField}
label={`Start Time (${$plugins.time.primary.label})`}
name="startTime"
on:change={onStartTimeChanged}
use={[
[
permissionHandler,
{
hasPermission: hasPlanUpdatePermission,
permissionError,
},
],
]}
/>
<DatePickerField
disabled={!hasPlanUpdatePermission}
layout="inline"
useFallback={!$plugins.time.enableDatePicker}
field={endTimeField}
label={`End Time (${$plugins.time.primary.label})`}
name="endTime"
on:change={onEndTimeChanged}
use={[
[
permissionHandler,
{
hasPermission: hasPlanUpdatePermission,
permissionError,
},
],
]}
/>
<Input layout="inline">
<label
use:tooltip={{ content: `Start Time (${$plugins.time.primary.label})`, placement: 'top' }}
for="startTime"
>
Start Time ({$plugins.time.primary.label})
</label>
<input class="st-input w-full" disabled name="startTime" value={planStartTime} id="startTime" />
</Input>
<Input layout="inline">
<label use:tooltip={{ content: `End Time (${$plugins.time.primary.label})`, placement: 'top' }} for="endTime">
End Time ({$plugins.time.primary.label})
</label>
<input class="st-input w-full" disabled name="endTime" value={planEndTime} id="endTime" />
<label use:tooltip={{ content: 'Plan Duration', placement: 'top' }} for="planDuration">Plan Duration</label>
<input class="st-input w-full" disabled name="planDuration" value={durationString} id="planDuration" />
</Input>
<Input layout="inline">
<label use:tooltip={{ content: 'Owner', placement: 'top' }} for="owner">Owner</label>
Expand Down
5 changes: 1 addition & 4 deletions src/components/timeline/LayerDiscrete.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@
import { isDeleteEvent } from '../../utilities/keyboardEvents';
import {
getActivityDirectiveStartTimeMs,
getDoyTime,
getIntervalUnixEpochTime,
getUnixEpochTime,
getUnixEpochTimeFromInterval,
} from '../../utilities/time';
import {
Expand Down Expand Up @@ -118,7 +116,6 @@
let dragActivityDirectiveActive: ActivityDirective | null = null;
let dragStartX: number | null = null;
let minRectSize: number = 4;
let planStartTimeMs: number;
let quadtreeActivityDirectives: Quadtree<QuadtreeRect>;
let quadtreeSpans: Quadtree<QuadtreeRect>;
let quadtreeExternalEvents: Quadtree<QuadtreeRect>;
Expand All @@ -145,8 +142,8 @@
$: canvasHeightDpr = drawHeight * dpr;
$: canvasWidthDpr = drawWidth * dpr;
$: rowHeight = discreteOptions.height;
$: planStartTimeMs = planStartTimeYmd ? new Date(planStartTimeYmd).getTime() : 0;
$: timelineLocked = timelineLockStatus === TimelineLockStatus.Locked;
$: planStartTimeMs = getUnixEpochTime(getDoyTime(new Date(planStartTimeYmd)));

// the following are NOT mutually exclusive.
$: canDrawActivities =
Expand Down
20 changes: 4 additions & 16 deletions src/routes/plans/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
import { parseJSONStream } from '../../utilities/generic';
import { permissionHandler } from '../../utilities/permissionHandler';
import { featurePermissions } from '../../utilities/permissions';
import { exportPlan, isDeprecatedPlanTransfer } from '../../utilities/plan';
import { computeDurationString, exportPlan, isDeprecatedPlanTransfer } from '../../utilities/plan';
import {
convertDoyToYmd,
convertUsToDurationString,
Expand Down Expand Up @@ -538,21 +538,9 @@
}

function updateDurationString() {
if ($startTimeField.valid && $endTimeField.valid) {
let startTimeMs = $plugins.time.primary.parse($startTimeField.value)?.getTime();
let endTimeMs = $plugins.time.primary.parse($endTimeField.value)?.getTime();
if (typeof startTimeMs === 'number' && typeof endTimeMs === 'number') {
durationString = convertUsToDurationString((endTimeMs - startTimeMs) * 1000);

if (!durationString) {
durationString = 'None';
}
} else {
durationString = 'Invalid';
}
} else {
durationString = 'None';
}
const startTimeMs = $plugins.time.primary.parse($startTimeField.value)?.getTime();
const endTimeMs = $plugins.time.primary.parse($endTimeField.value)?.getTime();
durationString = computeDurationString(startTimeMs, endTimeMs, $startTimeField.valid && $endTimeField.valid);
}

async function parsePlanFileStream(stream: ReadableStream) {
Expand Down
21 changes: 10 additions & 11 deletions src/routes/plans/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,13 @@
maxTimeRange,
plan,
planDatasets,
planEndTimeMs,
planId,
planModelActivityTypes,
planModelId,
planReadOnly,
planReadOnlyMergeRequest,
planReadOnlySnapshot,
planStartTimeMs,
planStartTimeYmd,
planTags,
resetPlanStores,
viewTimeRange,
Expand Down Expand Up @@ -158,7 +157,6 @@
} from '../../../utilities/simulation';
import { getHumanReadableStatus, statusColors } from '../../../utilities/status';
import { pluralize } from '../../../utilities/text';
import { getUnixEpochTime } from '../../../utilities/time';
import { showSuccessToast } from '../../../utilities/toast';
import { tooltip } from '../../../utilities/tooltip';
import { getSearchParameterNumber, removeQueryParam, setQueryParam } from '../../../utilities/url';
Expand Down Expand Up @@ -288,9 +286,6 @@
}
$: if (data.initialPlan) {
$initialPlan = data.initialPlan;
$planEndTimeMs = getUnixEpochTime(data.initialPlan.end_time_doy);
$planStartTimeMs = getUnixEpochTime(data.initialPlan.start_time_doy);
$maxTimeRange = { end: $planEndTimeMs, start: $planStartTimeMs };
$simulationDatasetId = -1;

const querySimulationDatasetId = $page.url.searchParams.get(SearchParameters.SIMULATION_DATASET_ID);
Expand Down Expand Up @@ -386,7 +381,7 @@
initializeView({ ...data.initialView });
}

$: if ($initialPlan && $planDatasets) {
$: if ($planId > -1 && $planStartTimeYmd && $planDatasets) {
const datasetNames = [];

for (const dataset of $planDatasets) {
Expand All @@ -403,9 +398,9 @@
$externalResources = [];
effects
.getResourcesExternal(
$initialPlan.id,
$planId,
$simulationDatasetId > -1 ? $simulationDatasetId : null,
$initialPlan.start_time,
$planStartTimeYmd,
data.user,
resourcesExternalAbortController.signal,
)
Expand All @@ -422,15 +417,19 @@
selectActivity(null, null);
}

$: if ($initialPlan && $simulationDataset !== null && getSimulationStatus($simulationDataset) === Status.Complete) {
$: if (
$planStartTimeYmd &&
$simulationDataset !== null &&
getSimulationStatus($simulationDataset) === Status.Complete
) {
const datasetId = $simulationDataset.dataset_id;
simulationDataAbortController?.abort();
simulationDataAbortController = new AbortController();
$initialSpansLoading = true;
effects
.getSpans(
datasetId,
$simulationDataset.simulation_start_time ?? $initialPlan.start_time,
$simulationDataset.simulation_start_time ?? $planStartTimeYmd,
data.user,
simulationDataAbortController.signal,
)
Expand Down
3 changes: 0 additions & 3 deletions src/stores/__mocks__/plan.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,6 @@ export function resetPlanStores() {
createPlanError.set(null);
creatingPlan.set(false);
initialPlan.set(null);
planEndTimeMs.set(0);
planStartTimeMs.set(0);
maxTimeRange.set({ end: 0, start: 0 });
viewTimeRange.set({ end: 0, start: 0 });
}

Expand Down
20 changes: 15 additions & 5 deletions src/stores/activities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { DefaultEffectiveArguments, DefaultEffectiveArgumentsMap } from '..
import type { SpanId } from '../types/simulation';
import { computeActivityDirectivesMap } from '../utilities/activities';
import gql from '../utilities/gql';
import { initialPlan, planId } from './plan';
import { planEndTimeDoy, planId, planStartTimeYmd } from './plan';
import { planSnapshotActivityDirectives, planSnapshotId } from './planSnapshots';
import { selectedSpanId, spansMap, spanUtilityMaps } from './simulation';
import { gqlSubscribable } from './subscribable';
Expand Down Expand Up @@ -67,24 +67,34 @@ export const activityArgumentDefaultsMap: Readable<DefaultEffectiveArgumentsMap>
);

export const activityDirectivesMap = derived(
[activityDirectivesDB, planSnapshotId, planSnapshotActivityDirectives, initialPlan, spansMap, spanUtilityMaps],
[
activityDirectivesDB,
planSnapshotId,
planSnapshotActivityDirectives,
planStartTimeYmd,
planEndTimeDoy,
spansMap,
spanUtilityMaps,
],
([
$activityDirectivesDB,
$planSnapshotId,
$planSnapshotActivityDirectives,
$initialPlan,
$planStartTimeYmd,
$planEndTimeDoy,
$spansMap,
$spanUtilityMaps,
]) => {
if (!$activityDirectivesDB || !$spansMap) {
return null;
}
if ($initialPlan === null) {
if (!$planStartTimeYmd) {
return {};
}
return computeActivityDirectivesMap(
$planSnapshotId !== null ? $planSnapshotActivityDirectives : $activityDirectivesDB || [],
$initialPlan,
$planStartTimeYmd,
$planEndTimeDoy,
$spansMap,
$spanUtilityMaps,
);
Expand Down
Loading
Loading