improve job list toolbar layouting, smaller layout fixes

This commit is contained in:
Christoph Kluge 2024-10-07 17:36:40 +02:00
parent 7243dbe763
commit 37415fa261
9 changed files with 291 additions and 246 deletions

View File

@ -307,7 +307,7 @@
<Spinner /> <Spinner />
</Col> </Col>
{/if} {/if}
<Col xs="auto"> <Col xs="auto" class="mb-2 mb-lg-0">
{#if $initq.error} {#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
{:else if cluster} {:else if cluster}

View File

@ -13,6 +13,7 @@
Row, Row,
Col, Col,
Button, Button,
ButtonGroup,
Icon, Icon,
Card, Card,
Spinner, Spinner,
@ -55,19 +56,25 @@
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
</script> </script>
<Row> <!-- ROW1: Status-->
{#if $initq.fetching} {#if $initq.fetching}
<Col xs="auto"> <Row class="mb-3">
<Col>
<Spinner /> <Spinner />
</Col> </Col>
{:else if $initq.error} </Row>
<Col xs="auto"> {:else if $initq.error}
<Row class="mb-3">
<Col>
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
</Col> </Col>
{/if} </Row>
</Row> {/if}
<Row>
<Col xs="auto"> <!-- ROW2: Tools-->
<Row cols={{ xs: 1, md: 2, lg: 4}} class="mb-3">
<Col lg="2" class="mb-2 mb-lg-0">
<ButtonGroup class="w-100">
<Button outline color="primary" on:click={() => (isSortingOpen = true)}> <Button outline color="primary" on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up" /> Sorting <Icon name="sort-up" /> Sorting
</Button> </Button>
@ -78,13 +85,12 @@
> >
<Icon name="graph-up" /> Metrics <Icon name="graph-up" /> Metrics
</Button> </Button>
<Button disabled outline </ButtonGroup>
>{matchedJobs == null ? "Loading..." : `${matchedJobs} jobs`}</Button
>
</Col> </Col>
<Col xs="auto"> <Col lg="4" xl="{(presetProject !== '') ? 5 : 6}" class="mb-1 mb-lg-0">
<Filters <Filters
{filterPresets} {filterPresets}
{matchedJobs}
bind:this={filterComponent} bind:this={filterComponent}
on:update-filters={({ detail }) => { on:update-filters={({ detail }) => {
selectedCluster = detail.filters[0]?.cluster selectedCluster = detail.filters[0]?.cluster
@ -94,8 +100,7 @@
}} }}
/> />
</Col> </Col>
<Col lg="3" xl="{(presetProject !== '') ? 3 : 2}" class="mb-2 mb-lg-0">
<Col xs="3" style="margin-left: auto;">
<TextFilter <TextFilter
{presetProject} {presetProject}
bind:authlevel bind:authlevel
@ -103,21 +108,22 @@
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)} on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
/> />
</Col> </Col>
<Col xs="2"> <Col lg="3" xl="2" class="mb-1 mb-lg-0">
<Refresher on:refresh={() => { <Refresher on:refresh={() => {
jobList.refreshJobs() jobList.refreshJobs()
jobList.refreshAllMetrics() jobList.refreshAllMetrics()
}} /> }} />
</Col> </Col>
</Row> </Row>
<br />
<!-- ROW3: Job List-->
<Row> <Row>
<Col> <Col>
<JobList <JobList
bind:this={jobList}
bind:metrics bind:metrics
bind:sorting bind:sorting
bind:matchedJobs bind:matchedJobs
bind:this={jobList}
bind:showFootprint bind:showFootprint
/> />
</Col> </Col>

View File

@ -104,8 +104,8 @@
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
</script> </script>
<Row> <Row cols={{ xs: 1, md: 2}}>
<Col xs="auto"> <Col xs="12" md="5" lg="4" xl="3" class="mb-2 mb-md-0">
<InputGroup> <InputGroup>
<Button disabled outline> <Button disabled outline>
Search {type.toLowerCase()}s Search {type.toLowerCase()}s
@ -119,7 +119,7 @@
/> />
</InputGroup> </InputGroup>
</Col> </Col>
<Col xs="auto"> <Col xs="12" md="7" lg="8" xl="9">
<Filters <Filters
bind:this={filterComponent} bind:this={filterComponent}
{filterPresets} {filterPresets}
@ -135,10 +135,11 @@
<thead> <thead>
<tr> <tr>
<th scope="col"> <th scope="col">
{{ {#if type === 'USER'}
USER: "Username", Username
PROJECT: "Project Name", {:else if type === 'PROJECT'}
}[type]} Project Name
{/if}
<Button <Button
color={sorting.field == "id" ? "primary" : "light"} color={sorting.field == "id" ? "primary" : "light"}
size="sm" size="sm"

View File

@ -13,6 +13,7 @@
Row, Row,
Col, Col,
Button, Button,
ButtonGroup,
Icon, Icon,
Card, Card,
Spinner, Spinner,
@ -48,6 +49,7 @@
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobList; let jobList;
let jobFilters = []; let jobFilters = [];
let matchedJobs = 0;
let sorting = { field: "startTime", type: "col", order: "DESC" }, let sorting = { field: "startTime", type: "col", order: "DESC" },
isSortingOpen = false; isSortingOpen = false;
let metrics = ccconfig.plot_list_selectedMetrics, let metrics = ccconfig.plot_list_selectedMetrics,
@ -103,22 +105,28 @@
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
</script> </script>
<Row> <!-- ROW1: Status-->
{#if $initq.fetching} {#if $initq.fetching}
<Row>
<Col> <Col>
<Spinner /> <Spinner />
</Col> </Col>
{:else if $initq.error} </Row>
<Col xs="auto"> {:else if $initq.error}
<Row>
<Col>
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
</Col> </Col>
{/if} </Row>
{/if}
<Col xs="auto"> <!-- ROW2: Tools-->
<Row cols={{ xs: 1, md: 2, lg: 4}} class="mb-3">
<Col lg="2" class="mb-2 mb-lg-0">
<ButtonGroup class="w-100">
<Button outline color="primary" on:click={() => (isSortingOpen = true)}> <Button outline color="primary" on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up" /> Sorting <Icon name="sort-up" /> Sorting
</Button> </Button>
<Button <Button
outline outline
color="primary" color="primary"
@ -126,18 +134,12 @@
> >
<Icon name="graph-up" /> Metrics <Icon name="graph-up" /> Metrics
</Button> </Button>
</ButtonGroup>
<Button
outline
color="secondary"
on:click={() => (isHistogramSelectionOpen = true)}
>
<Icon name="bar-chart-line" /> Select Histograms
</Button>
</Col> </Col>
<Col xs="auto"> <Col lg="4" xl="6" class="mb-1 mb-lg-0">
<Filters <Filters
{filterPresets} {filterPresets}
{matchedJobs}
startTimeQuickSelect={true} startTimeQuickSelect={true}
bind:this={filterComponent} bind:this={filterComponent}
on:update-filters={({ detail }) => { on:update-filters={({ detail }) => {
@ -149,20 +151,21 @@
}} }}
/> />
</Col> </Col>
<Col xs="auto" style="margin-left: auto;"> <Col lg="3" xl="2" class="mb-2 mb-lg-0">
<TextFilter <TextFilter
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)} on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
/> />
</Col> </Col>
<Col xs="auto"> <Col lg="3" xl="2" class="mb-1 mb-lg-0">
<Refresher on:refresh={() => { <Refresher on:refresh={() => {
jobList.refreshJobs() jobList.refreshJobs()
jobList.refreshAllMetrics() jobList.refreshAllMetrics()
}} /> }} />
</Col> </Col>
</Row> </Row>
<br />
<Row cols={{ xs: 1, md: 3}}> <!-- ROW3: Base Information-->
<Row cols={{ xs: 1, md: 3}} class="mb-2">
{#if $stats.error} {#if $stats.error}
<Col> <Col>
<Card body color="danger">{$stats.error.message}</Card> <Card body color="danger">{$stats.error.message}</Card>
@ -210,12 +213,12 @@
</tbody> </tbody>
</Table> </Table>
</Col> </Col>
<Col class="text-center"> <Col class="px-1">
<div bind:clientWidth={w1}> <div bind:clientWidth={w1}>
{#key $stats.data.jobsStatistics[0].histDuration} {#key $stats.data.jobsStatistics[0].histDuration}
<Histogram <Histogram
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)} data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
width={w1 - 25} width={w1}
height={histogramHeight} height={histogramHeight}
title="Duration Distribution" title="Duration Distribution"
xlabel="Current Runtimes" xlabel="Current Runtimes"
@ -226,12 +229,12 @@
{/key} {/key}
</div> </div>
</Col> </Col>
<Col class="text-center"> <Col class="px-1">
<div bind:clientWidth={w2}> <div bind:clientWidth={w2}>
{#key $stats.data.jobsStatistics[0].histNumNodes} {#key $stats.data.jobsStatistics[0].histNumNodes}
<Histogram <Histogram
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)} data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
width={w2 - 25} width={w2}
height={histogramHeight} height={histogramHeight}
title="Number of Nodes Distribution" title="Number of Nodes Distribution"
xlabel="Allocated Nodes" xlabel="Allocated Nodes"
@ -245,7 +248,19 @@
{/if} {/if}
</Row> </Row>
{#if metricsInHistograms} <!-- ROW4+5: Selectable Histograms -->
<Row cols={{ xs: 1, md: 5}}>
<Col>
<Button
outline
color="secondary"
on:click={() => (isHistogramSelectionOpen = true)}
>
<Icon name="bar-chart-line" /> Select Histograms
</Button>
</Col>
</Row>
{#if metricsInHistograms?.length > 0}
{#if $stats.error} {#if $stats.error}
<Row> <Row>
<Col> <Col>
@ -259,6 +274,7 @@
</Col> </Col>
</Row> </Row>
{:else} {:else}
<hr class="my-2"/>
{#key $stats.data.jobsStatistics[0].histMetrics} {#key $stats.data.jobsStatistics[0].histMetrics}
<PlotGrid <PlotGrid
let:item let:item
@ -281,11 +297,24 @@
</PlotGrid> </PlotGrid>
{/key} {/key}
{/if} {/if}
{/if} {:else}
<br /> <Row class="mt-2">
<Row>
<Col> <Col>
<JobList bind:metrics bind:sorting bind:this={jobList} bind:showFootprint /> <Card body>No footprint histograms selected.</Card>
</Col>
</Row>
{/if}
<!-- ROW6: JOB LIST-->
<Row class="mt-3">
<Col>
<JobList
bind:this={jobList}
bind:matchedJobs
bind:metrics
bind:sorting
bind:showFootprint
/>
</Col> </Col>
</Row> </Row>

View File

@ -6,6 +6,7 @@
- `filterPresets Object?`: Optional predefined filter values [Default: {}] - `filterPresets Object?`: Optional predefined filter values [Default: {}]
- `disableClusterSelection Bool?`: Is the selection disabled [Default: false] - `disableClusterSelection Bool?`: Is the selection disabled [Default: false]
- `startTimeQuickSelect Bool?`: Render startTime quick selections [Default: false] - `startTimeQuickSelect Bool?`: Render startTime quick selections [Default: false]
- `matchedJobs Number?`: Number of jobs matching the filter [Default: -2]
Events: Events:
- `update-filters, {filters: [Object]?}`: The detail's 'filters' prop are new filter items to be applied - `update-filters, {filters: [Object]?}`: The detail's 'filters' prop are new filter items to be applied
@ -17,11 +18,11 @@
<script> <script>
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { import {
Row,
Col,
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
DropdownToggle, DropdownToggle,
Button,
ButtonGroup,
ButtonDropdown, ButtonDropdown,
Icon, Icon,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
@ -42,6 +43,7 @@
export let filterPresets = {}; export let filterPresets = {};
export let disableClusterSelection = false; export let disableClusterSelection = false;
export let startTimeQuickSelect = false; export let startTimeQuickSelect = false;
export let matchedJobs = -2;
let filters = { let filters = {
projectMatch: filterPresets.projectMatch || "contains", projectMatch: filterPresets.projectMatch || "contains",
@ -217,9 +219,9 @@
} }
</script> </script>
<Row> <!-- Dropdown-Button -->
<Col xs="auto"> <ButtonGroup>
<ButtonDropdown class="cc-dropdown-on-hover"> <ButtonDropdown class="cc-dropdown-on-hover mb-1" style="{(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}">
<DropdownToggle outline caret color="success"> <DropdownToggle outline caret color="success">
<Icon name="sliders" /> <Icon name="sliders" />
Filters Filters
@ -256,7 +258,7 @@
</DropdownItem> </DropdownItem>
{#if startTimeQuickSelect} {#if startTimeQuickSelect}
<DropdownItem divider /> <DropdownItem divider />
<DropdownItem disabled>Start Time Qick Selection</DropdownItem> <DropdownItem disabled>Start Time Quick Selection</DropdownItem>
{#each [{ text: "Last 6hrs", url: "last6h", seconds: 6 * 60 * 60 }, { text: "Last 24hrs", url: "last24h", seconds: 24 * 60 * 60 }, { text: "Last 7 days", url: "last7d", seconds: 7 * 24 * 60 * 60 }, { text: "Last 30 days", url: "last30d", seconds: 30 * 24 * 60 * 60 }] as { text, url, seconds }} {#each [{ text: "Last 6hrs", url: "last6h", seconds: 6 * 60 * 60 }, { text: "Last 24hrs", url: "last24h", seconds: 24 * 60 * 60 }, { text: "Last 7 days", url: "last7d", seconds: 7 * 24 * 60 * 60 }, { text: "Last 30 days", url: "last30d", seconds: 30 * 24 * 60 * 60 }] as { text, url, seconds }}
<DropdownItem <DropdownItem
on:click={() => { on:click={() => {
@ -275,24 +277,30 @@
{/if} {/if}
</DropdownMenu> </DropdownMenu>
</ButtonDropdown> </ButtonDropdown>
</Col> {#if matchedJobs >= -1}
<Col xs="auto"> <Button class="mb-1" style="margin-right: 0.5rem;" disabled outline>
{#if filters.cluster} {matchedJobs == -1 ? 'Loading ...' : `${matchedJobs} jobs`}
</Button>
{/if}
</ButtonGroup>
<!-- SELECTED FILTER PILLS -->
{#if filters.cluster}
<Info icon="cpu" on:click={() => (isClusterOpen = true)}> <Info icon="cpu" on:click={() => (isClusterOpen = true)}>
{filters.cluster} {filters.cluster}
{#if filters.partition} {#if filters.partition}
({filters.partition}) ({filters.partition})
{/if} {/if}
</Info> </Info>
{/if} {/if}
{#if filters.states.length != allJobStates.length} {#if filters.states.length != allJobStates.length}
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}> <Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
{filters.states.join(", ")} {filters.states.join(", ")}
</Info> </Info>
{/if} {/if}
{#if filters.startTime.from || filters.startTime.to} {#if filters.startTime.from || filters.startTime.to}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}> <Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
{#if filters.startTime.text} {#if filters.startTime.text}
{filters.startTime.text} {filters.startTime.text}
@ -302,9 +310,9 @@
).toLocaleString()} ).toLocaleString()}
{/if} {/if}
</Info> </Info>
{/if} {/if}
{#if filters.duration.from || filters.duration.to} {#if filters.duration.from || filters.duration.to}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}> <Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
{Math.floor(filters.duration.from / 3600)}h:{Math.floor( {Math.floor(filters.duration.from / 3600)}h:{Math.floor(
(filters.duration.from % 3600) / 60, (filters.duration.from % 3600) / 60,
@ -313,25 +321,25 @@
(filters.duration.to % 3600) / 60, (filters.duration.to % 3600) / 60,
)}m )}m
</Info> </Info>
{/if} {/if}
{#if filters.duration.lessThan} {#if filters.duration.lessThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}> <Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration less than {Math.floor( Duration less than {Math.floor(
filters.duration.lessThan / 3600, filters.duration.lessThan / 3600,
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m )}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
</Info> </Info>
{/if} {/if}
{#if filters.duration.moreThan} {#if filters.duration.moreThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}> <Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration more than {Math.floor( Duration more than {Math.floor(
filters.duration.moreThan / 3600, filters.duration.moreThan / 3600,
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m )}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
</Info> </Info>
{/if} {/if}
{#if filters.tags.length != 0} {#if filters.tags.length != 0}
<Info icon="tags" on:click={() => (isTagsOpen = true)}> <Info icon="tags" on:click={() => (isTagsOpen = true)}>
{#each filters.tags as tagId} {#each filters.tags as tagId}
{#key tagId} {#key tagId}
@ -339,9 +347,9 @@
{/key} {/key}
{/each} {/each}
</Info> </Info>
{/if} {/if}
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null} {#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}> <Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
{#if isNodesModified} {#if isNodesModified}
Nodes: {filters.numNodes.from} - {filters.numNodes.to} Nodes: {filters.numNodes.from} - {filters.numNodes.to}
@ -358,29 +366,27 @@
.numAccelerators.to} .numAccelerators.to}
{/if} {/if}
</Info> </Info>
{/if} {/if}
{#if filters.node != null} {#if filters.node != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}> <Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Node: {filters.node} Node: {filters.node}
</Info> </Info>
{/if} {/if}
{#if filters.energy.from || filters.energy.to} {#if filters.energy.from || filters.energy.to}
<Info icon="lightning-charge-fill" on:click={() => (isEnergyOpen = true)}> <Info icon="lightning-charge-fill" on:click={() => (isEnergyOpen = true)}>
Total Energy: {filters.energy.from} - {filters.energy.to} Total Energy: {filters.energy.from} - {filters.energy.to}
</Info> </Info>
{/if} {/if}
{#if filters.stats.length > 0} {#if filters.stats.length > 0}
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}> <Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
{filters.stats {filters.stats
.map((stat) => `${stat.text}: ${stat.from} - ${stat.to}`) .map((stat) => `${stat.text}: ${stat.from} - ${stat.to}`)
.join(", ")} .join(", ")}
</Info> </Info>
{/if} {/if}
</Col>
</Row>
<Cluster <Cluster
{disableClusterSelection} {disableClusterSelection}

View File

@ -110,7 +110,7 @@
jobs = [...$jobsStore.data.jobs.items] jobs = [...$jobsStore.data.jobs.items]
} }
$: matchedJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : 0; $: matchedJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity) // Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
export function refreshJobs() { export function refreshJobs() {

View File

@ -13,7 +13,7 @@
export let modified = false; export let modified = false;
</script> </script>
<Button outline color={modified ? "warning" : "primary"} on:click> <Button class="mr-2 mb-1" outline color={modified ? "warning" : "primary"} on:click>
<Icon name={icon} /> <Icon name={icon} />
<slot /> <slot />
</Button> </Button>

View File

@ -34,14 +34,15 @@
<InputGroup> <InputGroup>
<Input <Input
type="select" type="select"
title="Periodic refresh interval"
bind:value={refreshInterval} bind:value={refreshInterval}
on:change={refreshIntervalChanged} on:change={refreshIntervalChanged}
> >
<option value={null}>No periodic refresh</option> <option value={null}>No Interval</option>
<option value={30 * 1000}>Update every 30 seconds</option> <option value={30 * 1000}>30 Seconds</option>
<option value={60 * 1000}>Update every minute</option> <option value={60 * 1000}>60 Seconds</option>
<option value={2 * 60 * 1000}>Update every two minutes</option> <option value={2 * 60 * 1000}>Two Minutes</option>
<option value={5 * 60 * 1000}>Update every 5 minutes</option> <option value={5 * 60 * 1000}>5 Minutes</option>
</Input> </Input>
<Button <Button
outline outline

View File

@ -83,26 +83,28 @@
</script> </script>
<InputGroup> <InputGroup>
<select <Input
style="max-width: 175px;" type="select"
style="max-width: 120px;"
class="form-select" class="form-select"
title="Search Mode"
bind:value={mode} bind:value={mode}
on:change={modeChanged} on:change={modeChanged}
> >
{#if !presetProject} {#if !presetProject}
<option value={"project"}>Search Project</option> <option value={"project"}>Project</option>
{/if} {/if}
{#if roles && authlevel >= roles.manager} {#if roles && authlevel >= roles.manager}
<option value={"user"}>Search User</option> <option value={"user"}>User</option>
{/if} {/if}
<option value={"jobName"}>Search Jobname</option> <option value={"jobName"}>Jobname</option>
</select> </Input>
<Input <Input
type="text" type="text"
bind:value={term} bind:value={term}
on:change={() => termChanged()} on:change={() => termChanged()}
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)} on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
placeholder={presetProject ? `Filter ${mode} in ${scrambleNames ? scramble(presetProject) : presetProject} ...` : `Filter ${mode} ...`} placeholder={presetProject ? `Find ${mode} in ${scrambleNames ? scramble(presetProject) : presetProject} ...` : `Find ${mode} ...`}
/> />
{#if presetProject} {#if presetProject}
<Button title="Reset Project" on:click={resetProject} <Button title="Reset Project" on:click={resetProject}