Skip to content

Commit 2a5f418

Browse files
committed
activity search form
1 parent 816ad60 commit 2a5f418

File tree

11 files changed

+389
-1
lines changed

11 files changed

+389
-1
lines changed

src/components/menus/AppMenu.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
Info,
2222
LogOut,
2323
Network,
24+
Search as SearchIcon,
2425
Tags,
2526
} from 'lucide-svelte';
2627
import PlanDevWordmarkDark from '../../assets/plandev-logo-dark.svg?component';
@@ -95,6 +96,10 @@
9596
<Boxes size={16} />
9697
External Sources
9798
</MenuLink>
99+
<MenuLink className="text-sm py-1.5" href="{base}/search">
100+
<SearchIcon size={16} />
101+
Activity Search
102+
</MenuLink>
98103
</div>
99104

100105
<!-- Sequencing Column -->
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<svelte:options immutable={true} />
2+
3+
<script lang="ts">
4+
import { searchColumns, searchResults } from '../../stores/search';
5+
import type { User } from '../../types/app';
6+
import CssGrid from '../ui/CssGrid.svelte';
7+
import CssGridGutter from '../ui/CssGridGutter.svelte';
8+
import SearchPanel from './SearchPanel.svelte';
9+
import SearchResults from './SearchResults.svelte';
10+
11+
export let user: User | null;
12+
</script>
13+
14+
<CssGrid columns={$searchColumns}>
15+
<SearchPanel {user} />
16+
17+
<CssGridGutter track={1} type="column" />
18+
19+
<SearchResults {user} activities={$searchResults} />
20+
</CssGrid>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<svelte:options immutable={true} />
2+
3+
<script lang="ts">
4+
import { Button, Input as InputStellar, Label } from '@nasa-jpl/stellar-svelte';
5+
import { hasSearched, searchResults } from '../../stores/search';
6+
import type { User } from '../../types/app';
7+
import effects from '../../utilities/effects';
8+
import Panel from '../ui/Panel.svelte';
9+
import SectionTitle from '../ui/SectionTitle.svelte';
10+
11+
export let user: User | null;
12+
13+
let filterActType: string = '';
14+
let filterActName: string = '';
15+
let filterArgName: string = '';
16+
let filterArgValue: string = '';
17+
let filterTagValue: string = '';
18+
19+
async function onSearch() {
20+
hasSearched.set(true);
21+
22+
const filterArgs: [name: string, value: string | number | boolean][] = [];
23+
if (filterArgName || filterArgValue) {
24+
if (filterArgValue.toLowerCase() === 'true') {
25+
filterArgs.push([filterArgName, true]);
26+
} else if (filterArgValue.toLowerCase() === 'false') {
27+
filterArgs.push([filterArgName, false]);
28+
} else if (!isNaN(Number(filterArgValue))) {
29+
filterArgs.push([filterArgName, Number(filterArgValue)]);
30+
} else {
31+
filterArgs.push([filterArgName, filterArgValue]);
32+
}
33+
}
34+
const results = await effects.searchActivities(filterActType, filterActName, filterArgs, filterTagValue, user);
35+
if (results) {
36+
searchResults.set(results);
37+
}
38+
}
39+
</script>
40+
41+
<Panel overflowYBody="hidden">
42+
<svelte:fragment slot="header">
43+
<SectionTitle>Search for activities across plans</SectionTitle>
44+
</svelte:fragment>
45+
46+
<svelte:fragment slot="body">
47+
<form on:submit|preventDefault={onSearch} class="flex flex-col">
48+
<fieldset>
49+
<Label for="activity-type-input">Activity Type</Label>
50+
<InputStellar
51+
bind:value={filterActType}
52+
id="activity-type-input"
53+
placeholder="Activity Type"
54+
autocomplete="off"
55+
class="w-[300px]"
56+
sizeVariant="xs"
57+
aria-label="Activity Type"
58+
/>
59+
</fieldset>
60+
<fieldset>
61+
<Label for="activity-name-input">Activity Name</Label>
62+
<InputStellar
63+
bind:value={filterActName}
64+
id="activity-name-input"
65+
placeholder="Activity Name"
66+
autocomplete="off"
67+
class="w-[300px]"
68+
sizeVariant="xs"
69+
aria-label="Activity Name"
70+
/>
71+
</fieldset>
72+
<fieldset>
73+
<Label for="argument-name-input">Argument Name</Label>
74+
<InputStellar
75+
bind:value={filterArgName}
76+
id="argument-name-input"
77+
placeholder="Argument Name"
78+
autocomplete="off"
79+
class="w-[300px]"
80+
sizeVariant="xs"
81+
aria-label="Argument Name"
82+
/>
83+
<Label for="argument-value-input">Argument Value</Label>
84+
<InputStellar
85+
bind:value={filterArgValue}
86+
id="argument-value-input"
87+
placeholder="Argument Value"
88+
autocomplete="off"
89+
class="w-[300px]"
90+
sizeVariant="xs"
91+
aria-label="Argument Value"
92+
/>
93+
</fieldset>
94+
<fieldset>
95+
<Label for="tag-value-input">Tag Value</Label>
96+
<InputStellar
97+
bind:value={filterTagValue}
98+
id="tag-value-input"
99+
placeholder="Tag Value"
100+
autocomplete="off"
101+
class="w-[300px]"
102+
sizeVariant="xs"
103+
aria-label="Tag Value"
104+
/>
105+
</fieldset>
106+
<fieldset class="my-4">
107+
<Button
108+
type="submit"
109+
class="w-full"
110+
disabled={filterActType === '' &&
111+
filterActName === '' &&
112+
filterArgName === '' &&
113+
filterArgValue === '' &&
114+
filterTagValue === ''}>Search</Button
115+
>
116+
</fieldset>
117+
</form>
118+
</svelte:fragment>
119+
</Panel>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<svelte:options immutable={true} />
2+
3+
<script lang="ts">
4+
import { hasSearched, searchResults } from '../../stores/search';
5+
import type { ActivityDirectiveSearchResult } from '../../types/activity';
6+
import type { User } from '../../types/app';
7+
import SingleActionDataGrid from '../ui/DataGrid/SingleActionDataGrid.svelte';
8+
import Panel from '../ui/Panel.svelte';
9+
import SectionTitle from '../ui/SectionTitle.svelte';
10+
11+
export let user: User | null;
12+
export let activities: ActivityDirectiveSearchResult[] | null = $searchResults;
13+
14+
const columnDefs = [
15+
{
16+
field: 'name',
17+
headerName: 'Activity Directive Name',
18+
},
19+
{
20+
field: 'type',
21+
headerName: 'Activity Type',
22+
},
23+
{
24+
field: 'plan.name',
25+
headerName: 'Plan Name',
26+
},
27+
];
28+
29+
function getUrlForActivity(activity: ActivityDirectiveSearchResult): string {
30+
return `/plans/${activity.plan_id}?activityId=${activity.directive_id}`;
31+
}
32+
33+
function onRowClicked(event: CustomEvent) {
34+
const activity: ActivityDirectiveSearchResult = event.detail.data;
35+
const url = getUrlForActivity(activity);
36+
window.open(url, '_blank');
37+
}
38+
</script>
39+
40+
<Panel overflowYBody="hidden">
41+
<svelte:fragment slot="header">
42+
<SectionTitle>Results</SectionTitle>
43+
</svelte:fragment>
44+
45+
<svelte:fragment slot="body">
46+
<SingleActionDataGrid
47+
idKey="directive_id"
48+
{columnDefs}
49+
items={activities ?? []}
50+
{user}
51+
itemDisplayText="Search Results"
52+
on:rowClicked={onRowClicked}
53+
hasDeletePermission={false}
54+
loading={$hasSearched && activities === null}
55+
noRowsOverlayText="No Results Found"
56+
/>
57+
</svelte:fragment>
58+
</Panel>

src/routes/search/+layout.svelte

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<svelte:options immutable={true} />
2+
3+
<script lang="ts">
4+
import Nav from '../../components/app/Nav.svelte';
5+
import CssGrid from '../../components/ui/CssGrid.svelte';
6+
</script>
7+
8+
<CssGrid rows="var(--nav-header-height) calc(100vh - var(--nav-header-height))">
9+
<Nav>
10+
<span class="scheduling-title" slot="title">Search</span>
11+
</Nav>
12+
13+
<slot />
14+
</CssGrid>

src/routes/search/+page.svelte

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<svelte:options immutable={true} />
2+
3+
<script lang="ts">
4+
import PageTitle from '../../components/app/PageTitle.svelte';
5+
import Search from '../../components/search/Search.svelte';
6+
import { getUserStore } from '../../stores/user';
7+
8+
const user = getUserStore();
9+
</script>
10+
11+
<PageTitle title="Search" />
12+
13+
<Search user={$user} />

src/routes/search/+page.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { PageLoad } from './$types';
2+
3+
export const load: PageLoad = async ({ parent }) => {
4+
const { user } = await parent();
5+
6+
return {
7+
user,
8+
};
9+
};

src/stores/search.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { writable, type Writable } from 'svelte/store';
2+
import type { ActivityDirectiveSearchResult } from '../types/activity';
3+
4+
/* Writeable. */
5+
export const searchColumns: Writable<string> = writable('1fr 3px 1fr');
6+
7+
export const hasSearched: Writable<boolean> = writable(false);
8+
9+
export const searchResults: Writable<ActivityDirectiveSearchResult[] | null> = writable(null);

src/types/activity.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import type { PartialWith, UserId } from './app';
44
import type { ActivityDirectiveValidationFailures } from './errors';
55
import type { ExpansionRuleSlim } from './expansion';
66
import type { ArgumentsMap, ParametersMap } from './parameter';
7+
import type { PlanSchema } from './plan';
78
import type { ValueSchema } from './schema';
8-
import type { Tag } from './tags';
9+
import type { Tag, TagsInsertInput } from './tags';
910

1011
export type ActivityType = {
1112
computed_attributes_value_schema: ValueSchema;
@@ -130,3 +131,16 @@ export type PlanSnapshotActivity = Omit<ActivityDirective, 'anchor_validations'
130131
export type PlanSnapshotActivityDB = Omit<ActivityDirectiveDB, 'anchor_validations' | 'applied_preset' | 'plan_id'> & {
131132
snapshot_id: number;
132133
};
134+
135+
export interface ActivityDirectiveSearchResult {
136+
applied_preset: AppliedPreset['preset_applied']['name'] | null;
137+
arguments: ArgumentsMap;
138+
directive_id: number;
139+
name: string;
140+
plan: Pick<PlanSchema, 'model_id' | 'name'>;
141+
plan_id: number;
142+
tags: {
143+
tag: TagsInsertInput;
144+
}[];
145+
type: string;
146+
}

0 commit comments

Comments
 (0)