feat: move tag management to new job view header

This commit is contained in:
Christoph Kluge 2024-09-09 18:06:13 +02:00
parent 704620baff
commit b3ed2afebe
5 changed files with 206 additions and 79 deletions

View File

@ -233,7 +233,7 @@
</script> </script>
<Row class="mb-0 mb-xxl-2"> <Row class="mb-0 mb-xxl-2">
<!-- Column 1: Job Info, Concurrent Jobs, Admin Message if found--> <!-- Column 1: Job Info, Job Tags, Concurrent Jobs, Admin Message if found-->
<Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0"> <Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0">
{#if $initq.error} {#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
@ -245,6 +245,11 @@
<JobInfo job={$initq.data.job} {jobTags} /> <JobInfo job={$initq.data.job} {jobTags} />
</CardBody> </CardBody>
</TabPane> </TabPane>
<TabPane tabId="job-tags" tab="Job Tags">
<CardBody>
<TagManagement job={$initq.data.job} {username} {authlevel} {roles} bind:jobTags renderModal={false}/>
</CardBody>
</TabPane>
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0} {#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
<TabPane tabId="shared-jobs"> <TabPane tabId="shared-jobs">
<span slot="tab"> <span slot="tab">
@ -321,15 +326,10 @@
<hr/> <hr/>
<Row class="mb-2"> <Row class="mb-2">
<Col xs="auto">
{#if $initq.data}
<TagManagement job={$initq.data.job} {username} {authlevel} {roles} bind:jobTags />
{/if}
</Col>
<Col xs="auto"> <Col xs="auto">
{#if $initq.data} {#if $initq.data}
<Button outline on:click={() => (isMetricsSelectionOpen = true)}> <Button outline on:click={() => (isMetricsSelectionOpen = true)}>
<Icon name="graph-up" /> Metrics <Icon name="graph-up" /> Select Metrics
</Button> </Button>
{/if} {/if}
</Col> </Col>

View File

@ -208,7 +208,7 @@
<Tooltip <Tooltip
target={`footprint-${job.jobId}-${index}`} target={`footprint-${job.jobId}-${index}`}
placement="right" placement="right"
offset={[0, 20]}>{fpd.message}</Tooltip >{fpd.message}</Tooltip
> >
</div> </div>
<Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}"> <Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
@ -246,7 +246,7 @@
<Tooltip <Tooltip
target={`footprint-${job.jobId}-${index}`} target={`footprint-${job.jobId}-${index}`}
placement="right" placement="right"
offset={[0, 20]}>{fpd.message}</Tooltip >{fpd.message}</Tooltip
> >
{/if} {/if}
{/each} {/each}

View File

@ -265,7 +265,7 @@
<Tooltip <Tooltip
target={`footprint-${job.jobId}-${index}`} target={`footprint-${job.jobId}-${index}`}
placement="right" placement="right"
offset={[0, 20]}>{fpd.message}</Tooltip >{fpd.message}</Tooltip
> >
</div> </div>
<Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}"> <Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
@ -303,7 +303,7 @@
<Tooltip <Tooltip
target={`footprint-${job.jobId}-${index}`} target={`footprint-${job.jobId}-${index}`}
placement="right" placement="right"
offset={[0, 20]}>{fpd.message}</Tooltip >{fpd.message}</Tooltip
> >
{/if} {/if}
{/each} {/each}

View File

@ -89,7 +89,7 @@
<tr> <tr>
<th> <th>
<Button outline on:click={() => (isMetricSelectionOpen = true)}> <Button outline on:click={() => (isMetricSelectionOpen = true)}>
Metrics <Icon name="graph-up" /> Select Metrics
</Button> </Button>
</th> </th>
{#each selectedMetrics as metric} {#each selectedMetrics as metric}

View File

@ -7,21 +7,28 @@
- `username String`: Empty string if auth. is disabled, otherwise the username as string - `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level - `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles - `roles [Number]`: Enum containing available roles
- `renderModal Bool?`: If component is rendered as bootstrap modal button [Default: true]
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import { gql, getContextClient, mutationStore } from "@urql/svelte"; import { gql, getContextClient, mutationStore } from "@urql/svelte";
import { import {
Row,
Col,
Icon, Icon,
Button, Button,
ListGroup,
ListGroupItem, ListGroupItem,
Input,
InputGroup,
InputGroupText,
Spinner, Spinner,
Modal, Modal,
Input,
ModalBody, ModalBody,
ModalHeader, ModalHeader,
ModalFooter, ModalFooter,
Alert, Alert,
Tooltip,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { fuzzySearchTags } from "../generic/utils.js"; import { fuzzySearchTags } from "../generic/utils.js";
import Tag from "../generic/helper/Tag.svelte"; import Tag from "../generic/helper/Tag.svelte";
@ -31,6 +38,7 @@
export let username; export let username;
export let authlevel; export let authlevel;
export let roles; export let roles;
export let renderModal = true;
let allTags = getContext("tags"), let allTags = getContext("tags"),
initialized = getContext("initialized"); initialized = getContext("initialized");
@ -40,6 +48,7 @@
let filterTerm = ""; let filterTerm = "";
let pendingChange = false; let pendingChange = false;
let isOpen = false; let isOpen = false;
const isAdmin = (roles && authlevel >= roles.admin);
const client = getContextClient(); const client = getContextClient();
@ -94,8 +103,9 @@
}); });
}; };
let allTagsFiltered; // $initialized is in there because when it becomes true, allTags is initailzed.
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags)); $: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags));
$: usedTagsFiltered = matchJobTags(jobTags, allTagsFiltered, 'used');
$: unusedTagsFiltered = matchJobTags(jobTags, allTagsFiltered, 'unused');
$: { $: {
newTagType = ""; newTagType = "";
@ -107,6 +117,16 @@
} }
} }
function matchJobTags(tags, availableTags, type) {
const jobTagIds = tags.map((t) => t.id)
if (type == 'used') {
return availableTags.filter((at) => jobTagIds.includes(at.id))
} else if (type == 'unused') {
return availableTags.filter((at) => !jobTagIds.includes(at.id))
}
return []
}
function isNewTag(type, name) { function isNewTag(type, name) {
for (let tag of allTagsFiltered) for (let tag of allTagsFiltered)
if (tag.type == type && tag.name == name) return false; if (tag.type == type && tag.name == name) return false;
@ -155,103 +175,210 @@
} }
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> {#if renderModal}
<ModalHeader> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
Manage Tags <ModalHeader>
{#if pendingChange !== false} Manage Tags
<Spinner size="sm" secondary /> {#if pendingChange !== false}
{:else} <Spinner size="sm" secondary />
<Icon name="tags" /> {:else}
{/if} <Icon name="tags" />
</ModalHeader> {/if}
<ModalBody> </ModalHeader>
<ModalBody>
<Input
style="width: 100%;"
type="text"
placeholder="Search Tags"
bind:value={filterTerm}
/>
<Alert color="info">
Search using "<code>type: name</code>". If no tag matches your search, a
button for creating a new one will appear.
</Alert>
<br />
<ul class="list-group">
{#each allTagsFiltered as tag}
<ListGroupItem>
<Tag {tag} />
<span style="float: right;">
{#if pendingChange === tag.id}
<Spinner size="sm" secondary />
{:else if job.tags.find((t) => t.id == tag.id)}
<Button
size="sm"
outline
color="danger"
on:click={() => removeTagFromJob(tag)}
>
<Icon name="x" />
</Button>
{:else}
<Button
size="sm"
outline
color="success"
on:click={() => addTagToJob(tag)}
>
<Icon name="plus" />
</Button>
{/if}
</span>
</ListGroupItem>
{:else}
<ListGroupItem disabled>
<i>No tags matching</i>
</ListGroupItem>
{/each}
</ul>
<br />
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
<div class="d-flex">
<Button
style="margin-right: 10px;"
outline
color="success"
on:click={(e) => (
e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
)}
>
Create & Add Tag:
<Tag tag={{ type: newTagType, name: newTagName, scope: newTagScope }} clickable={false}/>
</Button>
{#if roles && authlevel >= roles.admin}
<select
style="max-width: 175px;"
class="form-select"
bind:value={newTagScope}
>
<option value={username}>Scope: Private</option>
<option value={"global"}>Scope: Global</option>
<option value={"admin"}>Scope: Admin</option>
</select>
{/if}
</div>
{:else if allTagsFiltered.length == 0}
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert>
{/if}
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>
<Button outline on:click={() => (isOpen = true)}>
Manage Tags <Icon name="tags" />
</Button>
{:else}
<InputGroup class="mb-3">
<Input <Input
style="width: 100%;"
type="text" type="text"
placeholder="Search Tags" placeholder="Search Tags"
bind:value={filterTerm} bind:value={filterTerm}
/> />
<InputGroupText id={`tag-management-info`} style="cursor:help; font-size:larger;align-content:center;">
<Icon name=info-circle/>
</InputGroupText>
<Tooltip
target={`tag-management-info`}
placement="right">
Search using "type: name". If no tag matches your search, a
button for creating a new one will appear.
</Tooltip>
</InputGroup>
<br /> {#if usedTagsFiltered.length > 0}
<ListGroup class="mb-3">
<Alert color="info"> {#each usedTagsFiltered as utag}
Search using "<code>type: name</code>". If no tag matches your search, a <ListGroupItem color="primary">
button for creating a new one will appear. <Tag tag={utag} />
</Alert>
<ul class="list-group">
{#each allTagsFiltered as tag}
<ListGroupItem>
<Tag {tag} />
<span style="float: right;"> <span style="float: right;">
{#if pendingChange === tag.id} {#if pendingChange === utag.id}
<Spinner size="sm" secondary />
{:else}
<Button
size="sm"
color="danger"
on:click={() => removeTagFromJob(utag)}
>
<Icon name="x" />
</Button>
{/if}
</span>
</ListGroupItem>
{/each}
</ListGroup>
{:else if filterTerm !== ""}
<ListGroup class="mb-3">
<ListGroupItem disabled>
<i>No used tags matching</i>
</ListGroupItem>
</ListGroup>
{/if}
{#if unusedTagsFiltered.length > 0}
<ListGroup class="mb-3">
{#each unusedTagsFiltered as uutag}
<ListGroupItem color="dark">
<Tag tag={uutag} />
<span style="float: right;">
{#if pendingChange === uutag.id}
<Spinner size="sm" secondary /> <Spinner size="sm" secondary />
{:else if job.tags.find((t) => t.id == tag.id)}
<Button
size="sm"
outline
color="danger"
on:click={() => removeTagFromJob(tag)}
>
<Icon name="x" />
</Button>
{:else} {:else}
<Button <Button
size="sm" size="sm"
outline
color="success" color="success"
on:click={() => addTagToJob(tag)} on:click={() => addTagToJob(uutag)}
> >
<Icon name="plus" /> <Icon name="plus" />
</Button> </Button>
{/if} {/if}
</span> </span>
</ListGroupItem> </ListGroupItem>
{:else}
<ListGroupItem disabled>
<i>No tags matching</i>
</ListGroupItem>
{/each} {/each}
</ul> </ListGroup>
<br /> {:else if filterTerm !== ""}
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)} <ListGroup class="mb-3">
<div class="d-flex"> <ListGroupItem disabled>
<i>No unused tags matching</i>
</ListGroupItem>
</ListGroup>
{/if}
{#if newTagType && newTagName && isNewTag(newTagType, newTagName)}
<Row>
<Col xs={isAdmin ? 7 : 12} md={12} lg={isAdmin ? 7 : 12} xl={12} xxl={isAdmin ? 7 : 12} class="mb-2">
<Button <Button
style="margin-right: 10px;"
outline outline
style="width:100%;"
color="success" color="success"
on:click={(e) => ( on:click={(e) => (
e.preventDefault(), createTag(newTagType, newTagName, newTagScope) e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
)} )}
> >
Create & Add Tag: Add new tag:
<Tag tag={{ type: newTagType, name: newTagName, scope: newTagScope }} clickable={false}/> <Tag tag={{ type: newTagType, name: newTagName, scope: newTagScope }} clickable={false}/>
</Button> </Button>
{#if roles && authlevel >= roles.admin} </Col>
<select {#if isAdmin}
style="max-width: 175px;" <Col xs={5} md={12} lg={5} xl={12} xxl={5} class="mb-2" style="align-content:center;">
class="form-select" <Input type="select" bind:value={newTagScope}>
bind:value={newTagScope}
>
<option value={username}>Scope: Private</option> <option value={username}>Scope: Private</option>
<option value={"global"}>Scope: Global</option> <option value={"global"}>Scope: Global</option>
<option value={"admin"}>Scope: Admin</option> <option value={"admin"}>Scope: Admin</option>
</select> </Input>
{/if} </Col>
</div> {/if}
{:else if allTagsFiltered.length == 0} </Row>
<Alert>Search Term is not a valid Tag (<code>type: name</code>)</Alert> {:else if allTagsFiltered.length == 0}
{/if} <Alert color="info">Search Term is not a valid Tag (<code>type: name</code>)</Alert>
</ModalBody> {/if}
<ModalFooter> {/if}
<Button color="primary" on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>
<Button outline on:click={() => (isOpen = true)}>
Manage Tags <Icon name="tags" />
</Button>
<style> <style>
ul.list-group { ul.list-group {