Skip to content

Commit 0c96646

Browse files
Add support for uploading activities as JSON (#1843)
Add support for uploading activities as JSON --------- Co-authored-by: duranb <duran.bry@gmail.com>
1 parent b6774da commit 0c96646

File tree

8 files changed

+262
-9
lines changed

8 files changed

+262
-9
lines changed

e2e-tests/data/activities.json

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"activities": [
3+
{
4+
"anchor_id": null,
5+
"anchored_to_start": true,
6+
"arguments": {},
7+
"id": 3229,
8+
"name": "BananaNap",
9+
"start_offset": "03:28:50.386",
10+
"start_time_ms": 1704079730386,
11+
"tags": [],
12+
"type": "BananaNap"
13+
},
14+
{
15+
"anchor_id": null,
16+
"anchored_to_start": true,
17+
"arguments": {
18+
"duration": 3154000000000000
19+
},
20+
"id": 3230,
21+
"name": "ControllableDurationActivity",
22+
"start_offset": "11:06:17.9",
23+
"start_time_ms": 1704107177900,
24+
"tags": [],
25+
"type": "ControllableDurationActivity"
26+
},
27+
{
28+
"anchor_id": null,
29+
"anchored_to_start": true,
30+
"arguments": {
31+
"producer": "Testing"
32+
},
33+
"id": 3231,
34+
"name": "ChangeProducer",
35+
"start_offset": "08:23:12.265",
36+
"start_time_ms": 1704097392265,
37+
"tags": [],
38+
"type": "ChangeProducer"
39+
},
40+
{
41+
"anchor_id": null,
42+
"anchored_to_start": true,
43+
"arguments": {},
44+
"id": 3232,
45+
"name": "BananaNap",
46+
"start_offset": "03:28:50.386",
47+
"start_time_ms": 1704079730386,
48+
"tags": [],
49+
"type": "BananaNap"
50+
},
51+
{
52+
"anchor_id": 3231,
53+
"anchored_to_start": true,
54+
"arguments": {
55+
"duration": 3154000000000000
56+
},
57+
"id": 3233,
58+
"name": "ControllableDurationActivity",
59+
"start_offset": "11:06:17.9",
60+
"start_time_ms": 1704137370165,
61+
"tags": [],
62+
"type": "ControllableDurationActivity"
63+
},
64+
{
65+
"anchor_id": 3229,
66+
"anchored_to_start": true,
67+
"arguments": {
68+
"producer": "Testing"
69+
},
70+
"id": 3234,
71+
"name": "ChangeProducer",
72+
"start_offset": "08:23:12.265",
73+
"start_time_ms": 1704109922651,
74+
"tags": [],
75+
"type": "ChangeProducer"
76+
}
77+
]
78+
}

e2e-tests/fixtures/Plan.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,12 @@ export class Plan {
299299
await this.panelActivityForm.getByPlaceholder('Enter preset name').blur();
300300
}
301301

302-
async fillExternalDatasetFileInput(importFilePath: string) {
303-
const inputFile = this.page.locator('input[name="file"]');
302+
async fillFileInput(importFilePath: string) {
303+
const inputFile = this.page
304+
.getByRole('tabpanel')
305+
.filter({ hasText: 'Activity, Resource, Event Types' })
306+
.first()
307+
.locator('input[name="file"]');
304308
await setFileInputByFilepath(this.page, inputFile, importFilePath);
305309
}
306310

@@ -620,10 +624,18 @@ export class Plan {
620624
this.sequenceExpansionOutputModal = page.locator(`.modal:has-text("Sequence ID")`);
621625
}
622626

627+
async uploadActivities(importFilePath: string) {
628+
await this.panelActivityTypes.getByRole('tab', { exact: true, name: 'Activities' }).click();
629+
await this.panelActivityTypes.getByRole('button', { exact: true, name: 'Upload Activities' }).click();
630+
await this.fillFileInput(importFilePath);
631+
await expect(this.panelActivityTypes.getByRole('button', { exact: true, name: 'Upload' })).toBeEnabled();
632+
await this.panelActivityTypes.getByRole('button', { exact: true, name: 'Upload' }).click();
633+
}
634+
623635
async uploadExternalDatasets(importFilePath: string) {
624636
await this.panelActivityTypes.getByRole('tab', { exact: true, name: 'Resources' }).click();
625637
await this.panelActivityTypes.getByRole('button', { exact: true, name: 'Upload Resources' }).click();
626-
await this.fillExternalDatasetFileInput(importFilePath);
638+
await this.fillFileInput(importFilePath);
627639
await expect(this.panelActivityTypes.getByRole('button', { exact: true, name: 'Upload' })).toBeEnabled();
628640
await this.panelActivityTypes.getByRole('button', { exact: true, name: 'Upload' }).click();
629641
}

e2e-tests/tests/plan-resources.test.ts renamed to e2e-tests/tests/plan-activities-resources-external-events.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ test.afterAll(async () => {
1212
await teardownTest(setup);
1313
});
1414

15-
test.describe.serial('Plan Resources', () => {
15+
test.describe.serial('Plan Activities, Resources, External Events', () => {
1616
test('Uploading external plan dataset file - JSON', async () => {
1717
await setup.plan.uploadExternalDatasets('e2e-tests/data/external-dataset.json');
1818
await expect(setup.plan.panelActivityTypes.getByText('/awake')).toBeVisible();
@@ -25,4 +25,13 @@ test.describe.serial('Plan Resources', () => {
2525
await expect(setup.plan.panelActivityTypes.getByText('BatteryStateOfCharge')).toBeVisible();
2626
await expect(setup.plan.panelActivityTypes.getByText('Temperature')).toBeVisible();
2727
});
28+
29+
test('Uploading activities', async () => {
30+
await setup.plan.uploadActivities('e2e-tests/data/activities.json');
31+
await expect(setup.plan.panelActivityDirectivesTable.getByRole('row', { name: 'BananaNap' })).toHaveCount(2);
32+
await expect(setup.plan.panelActivityDirectivesTable.getByRole('row', { name: 'ChangeProducer' })).toHaveCount(2);
33+
await expect(
34+
setup.plan.panelActivityDirectivesTable.getByRole('row', { name: 'ControllableDurationActivity' }),
35+
).toHaveCount(2);
36+
});
2837
});

e2e-tests/tests/plan-activities.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,11 @@ test.describe.serial('Plan Activities', () => {
8181

8282
await setFileInputByFilepath(
8383
setup.page,
84-
setup.page.locator('input[type="file"]'),
84+
setup.page.getByRole('tabpanel').filter({ hasText: 'Selected Activity' }).first().locator('input[type="file"]'),
8585
'./e2e-tests/data/valid-view.json',
8686
);
8787

8888
const errorBadge = await setup.page.locator('.input-error-badge-root');
89-
9089
expect(errorBadge).not.toBeAttached();
9190
});
9291

src/components/ActivityList.svelte

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,54 @@
11
<svelte:options immutable={true} />
22

33
<script lang="ts">
4-
import { planModelActivityTypes, subsystemTags } from '../stores/plan';
4+
import CloseIcon from '@nasa-jpl/stellar/icons/close.svg?component';
5+
import UploadIcon from '@nasa-jpl/stellar/icons/upload.svg?component';
6+
import { plan, planModelActivityTypes, subsystemTags } from '../stores/plan';
57
import type { ActivityType } from '../types/activity';
8+
import type { User } from '../types/app';
69
import type { TimelineItemType } from '../types/timeline';
10+
import effects from '../utilities/effects';
11+
import { permissionHandler } from '../utilities/permissionHandler';
12+
import { featurePermissions } from '../utilities/permissions';
13+
import { tooltip } from '../utilities/tooltip';
714
import TimelineItemList from './TimelineItemList.svelte';
15+
import Input from './form/Input.svelte';
16+
17+
export let user: User | null;
18+
19+
const uploadPermissionError: string = 'You do not have permission to upload activities.';
20+
21+
let hasUploadPermission: boolean = false;
22+
let isUploadVisible: boolean = false;
23+
let uploadFiles: FileList | undefined;
24+
let uploadFileInput: HTMLInputElement;
25+
26+
$: if (user !== null && $plan !== null) {
27+
hasUploadPermission = featurePermissions.activityDirective.canCreate(user, $plan);
28+
}
829
930
function getFilterValueFromItem(item: TimelineItemType) {
1031
return (item as ActivityType).subsystem_tag?.id ?? -1;
1132
}
33+
34+
function onShowUpload() {
35+
isUploadVisible = true;
36+
}
37+
38+
function onHideUpload() {
39+
isUploadVisible = false;
40+
}
41+
42+
async function onUpload() {
43+
if (uploadFiles !== undefined) {
44+
if ($plan && uploadFiles?.length) {
45+
await effects.uploadActivities($plan, uploadFiles, user);
46+
}
47+
uploadFileInput.value = '';
48+
uploadFiles = undefined;
49+
onHideUpload();
50+
}
51+
}
1252
</script>
1353

1454
<TimelineItemList
@@ -19,4 +59,86 @@
1959
{getFilterValueFromItem}
2060
filterOptions={$subsystemTags.map(s => ({ color: s.color || '', label: s.name, value: s.id }))}
2161
filterName="Subsystem"
22-
/>
62+
>
63+
<div slot="header" class="upload-container" hidden={!isUploadVisible}>
64+
<button class="close-upload" type="button" on:click={onHideUpload}>
65+
<CloseIcon />
66+
</button>
67+
<Input layout="stacked">
68+
<label class="st-typography-body" for="file">Activity File</label>
69+
<input
70+
class="w-full text-xs"
71+
name="file"
72+
type="file"
73+
accept="application/json,.csv,.txt"
74+
bind:files={uploadFiles}
75+
bind:this={uploadFileInput}
76+
use:permissionHandler={{
77+
hasPermission: hasUploadPermission,
78+
permissionError: uploadPermissionError,
79+
}}
80+
/>
81+
</Input>
82+
<div class="upload-button-container">
83+
<button
84+
class="st-button secondary"
85+
disabled={!uploadFiles?.length}
86+
on:click={onUpload}
87+
use:permissionHandler={{
88+
hasPermission: hasUploadPermission,
89+
permissionError: uploadPermissionError,
90+
}}
91+
>
92+
Upload
93+
</button>
94+
</div>
95+
</div>
96+
<svelte:fragment slot="button">
97+
<button
98+
class="st-button secondary"
99+
on:click={onShowUpload}
100+
use:permissionHandler={{
101+
hasPermission: hasUploadPermission,
102+
permissionError: uploadPermissionError,
103+
}}
104+
use:tooltip={{ content: 'Upload Activities' }}
105+
>
106+
<UploadIcon />
107+
</button>
108+
</svelte:fragment>
109+
</TimelineItemList>
110+
111+
<style>
112+
.upload-container {
113+
background: var(--st-gray-15);
114+
border-radius: 5px;
115+
margin: 5px;
116+
padding: 8px 11px 8px;
117+
position: relative;
118+
}
119+
120+
.upload-container[hidden] {
121+
display: none;
122+
}
123+
124+
.upload-container {
125+
display: grid;
126+
row-gap: 8px;
127+
}
128+
129+
.upload-container :global(.upload-button-container) {
130+
display: flex;
131+
flex-flow: row-reverse;
132+
}
133+
134+
.upload-container :global(.close-upload) {
135+
background: none;
136+
border: 0;
137+
cursor: pointer;
138+
height: 1.3rem;
139+
padding: 0;
140+
position: absolute;
141+
right: 3px;
142+
top: 3px;
143+
}
144+
</style>

src/components/activity/TimelineItemsPanel.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<Tab class="timeline-items-tab text-xs"><ExternalEventIcon /> Events</Tab>
3333
</svelte:fragment>
3434
<TabPanel>
35-
<ActivityList />
35+
<ActivityList {user} />
3636
</TabPanel>
3737
<TabPanel>
3838
<ResourceList {user} />

src/utilities/effects.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8486,6 +8486,33 @@ const effects = {
84868486
return null;
84878487
},
84888488

8489+
async uploadActivities(plan: Plan, files: FileList, user: User | null): Promise<number | null> {
8490+
try {
8491+
if (!gatewayPermissions.CREATE_ACTIVITY_DIRECTIVES(user, plan)) {
8492+
throwPermissionError('add activities');
8493+
}
8494+
8495+
const file: File = files[0];
8496+
8497+
const body = new FormData();
8498+
body.append('plan_id', `${plan.id}`);
8499+
body.append('activity_file', file, file.name);
8500+
8501+
const uploadedActivities = await reqGateway<number | null>('/uploadActivities', 'POST', body, user, true);
8502+
8503+
if (uploadedActivities != null) {
8504+
showSuccessToast('Activities Uploaded Successfully');
8505+
logMessage(`Uploaded ${uploadedActivities} activites from file '${file.name}'`);
8506+
return uploadedActivities;
8507+
}
8508+
throw Error('Uploaded activities not found');
8509+
} catch (e) {
8510+
catchError('Unable to upload activities', e as Error);
8511+
showFailureToast('Activity Upload Failed');
8512+
return null;
8513+
}
8514+
},
8515+
84898516
async uploadDictionary(
84908517
dictionary: string,
84918518
user: User | null,

src/utilities/permissions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,12 @@ const gatewayPermissions = {
13171317
isUserAdmin(user) || (getPermission(queries, user) && (isPlanOwner(user, plan) || isPlanCollaborator(user, plan)))
13181318
);
13191319
},
1320+
CREATE_ACTIVITY_DIRECTIVES: (user: User | null, plan: PlanWithOwners): boolean => {
1321+
const queries = [getFunctionPermission(Queries.INSERT_ACTIVITY_DIRECTIVES)];
1322+
return (
1323+
isUserAdmin(user) || (getPermission(queries, user) && (isPlanOwner(user, plan) || isPlanCollaborator(user, plan)))
1324+
);
1325+
},
13201326
CREATE_EXTERNAL_EVENT_TYPE: (user: User | null) => {
13211327
return isUserAdmin(user) || getPermission([getFunctionPermission(Queries.INSERT_EXTERNAL_EVENT_TYPE)], user);
13221328
},

0 commit comments

Comments
 (0)