mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-26 14:16:07 +02:00
Rework tag and tag edit placement, add other feedback
- admin message shown primarily if exists - comment demo summary tab
This commit is contained in:
@@ -68,6 +68,7 @@
|
||||
export let height = "400px";
|
||||
|
||||
const ccconfig = getContext("cc-config")
|
||||
const showFootprint = !!ccconfig[`job_view_showFootprint`];
|
||||
|
||||
const footprintData = job?.footprint?.map((jf) => {
|
||||
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
|
||||
@@ -165,101 +166,142 @@
|
||||
}
|
||||
}
|
||||
|
||||
function writeSummary(fpd) {
|
||||
// Hardcoded! Needs to be retrieved from globalMetrics
|
||||
const performanceMetrics = ['flops_any', 'mem_bw'];
|
||||
const utilizationMetrics = ['cpu_load', 'acc_utilization'];
|
||||
const energyMetrics = ['cpu_power'];
|
||||
/*
|
||||
function writeSummary(fpd) {
|
||||
// Hardcoded! Needs to be retrieved from globalMetrics
|
||||
const performanceMetrics = ['flops_any', 'mem_bw'];
|
||||
const utilizationMetrics = ['cpu_load', 'acc_utilization'];
|
||||
const energyMetrics = ['cpu_power'];
|
||||
|
||||
let performanceScore = 0;
|
||||
let utilizationScore = 0;
|
||||
let energyScore = 0;
|
||||
let performanceScore = 0;
|
||||
let utilizationScore = 0;
|
||||
let energyScore = 0;
|
||||
|
||||
let performanceMetricsCounted = 0;
|
||||
let utilizationMetricsCounted = 0;
|
||||
let energyMetricsCounted = 0;
|
||||
let performanceMetricsCounted = 0;
|
||||
let utilizationMetricsCounted = 0;
|
||||
let energyMetricsCounted = 0;
|
||||
|
||||
fpd.forEach(metric => {
|
||||
console.log('Metric, Impact', metric.name, metric.impact)
|
||||
if (performanceMetrics.includes(metric.name)) {
|
||||
performanceScore += metric.impact
|
||||
performanceMetricsCounted += 1
|
||||
} else if (utilizationMetrics.includes(metric.name)) {
|
||||
utilizationScore += metric.impact
|
||||
utilizationMetricsCounted += 1
|
||||
} else if (energyMetrics.includes(metric.name)) {
|
||||
energyScore += metric.impact
|
||||
energyMetricsCounted += 1
|
||||
fpd.forEach(metric => {
|
||||
console.log('Metric, Impact', metric.name, metric.impact)
|
||||
if (performanceMetrics.includes(metric.name)) {
|
||||
performanceScore += metric.impact
|
||||
performanceMetricsCounted += 1
|
||||
} else if (utilizationMetrics.includes(metric.name)) {
|
||||
utilizationScore += metric.impact
|
||||
utilizationMetricsCounted += 1
|
||||
} else if (energyMetrics.includes(metric.name)) {
|
||||
energyScore += metric.impact
|
||||
energyMetricsCounted += 1
|
||||
}
|
||||
});
|
||||
|
||||
performanceScore = (performanceMetricsCounted == 0) ? performanceScore : (performanceScore / performanceMetricsCounted);
|
||||
utilizationScore = (utilizationMetricsCounted == 0) ? utilizationScore : (utilizationScore / utilizationMetricsCounted);
|
||||
energyScore = (energyMetricsCounted == 0) ? energyScore : (energyScore / energyMetricsCounted);
|
||||
|
||||
let res = [];
|
||||
|
||||
console.log('Perf', performanceScore, performanceMetricsCounted)
|
||||
console.log('Util', utilizationScore, utilizationMetricsCounted)
|
||||
console.log('Energy', energyScore, energyMetricsCounted)
|
||||
|
||||
if (performanceScore == 1) {
|
||||
res.push('<b>Performance:</b> Your job performs well.')
|
||||
} else if (performanceScore != 0) {
|
||||
res.push('<b>Performance:</b> Your job performs suboptimal.')
|
||||
}
|
||||
});
|
||||
|
||||
performanceScore = (performanceMetricsCounted == 0) ? performanceScore : (performanceScore / performanceMetricsCounted);
|
||||
utilizationScore = (utilizationMetricsCounted == 0) ? utilizationScore : (utilizationScore / utilizationMetricsCounted);
|
||||
energyScore = (energyMetricsCounted == 0) ? energyScore : (energyScore / energyMetricsCounted);
|
||||
if (utilizationScore == 1) {
|
||||
res.push('<b>Utilization:</b> Your job utilizes resources well.')
|
||||
} else if (utilizationScore != 0) {
|
||||
res.push('<b>Utilization:</b> Your job utilizes resources suboptimal.')
|
||||
}
|
||||
|
||||
let res = [];
|
||||
if (energyScore == 1) {
|
||||
res.push('<b>Energy:</b> Your job has good energy values.')
|
||||
} else if (energyScore != 0) {
|
||||
res.push('<b>Energy:</b> Your job consumes more energy than necessary.')
|
||||
}
|
||||
|
||||
console.log('Perf', performanceScore, performanceMetricsCounted)
|
||||
console.log('Util', utilizationScore, utilizationMetricsCounted)
|
||||
console.log('Energy', energyScore, energyMetricsCounted)
|
||||
return res;
|
||||
};
|
||||
|
||||
if (performanceScore == 1) {
|
||||
res.push('<b>Performance:</b> Your job performs well.')
|
||||
} else if (performanceScore != 0) {
|
||||
res.push('<b>Performance:</b> Your job performs suboptimal.')
|
||||
}
|
||||
|
||||
if (utilizationScore == 1) {
|
||||
res.push('<b>Utilization:</b> Your job utilizes resources well.')
|
||||
} else if (utilizationScore != 0) {
|
||||
res.push('<b>Utilization:</b> Your job utilizes resources suboptimal.')
|
||||
}
|
||||
|
||||
if (energyScore == 1) {
|
||||
res.push('<b>Energy:</b> Your job has good energy values.')
|
||||
} else if (energyScore != 0) {
|
||||
res.push('<b>Energy:</b> Your job consumes more energy than necessary.')
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
$: summaryMessages = writeSummary(footprintData)
|
||||
$: summaryMessages = writeSummary(footprintData)
|
||||
*/
|
||||
</script>
|
||||
|
||||
<Card class="overflow-auto" style="width: {width}; height: {height}">
|
||||
<TabContent> <!-- on:tab={(e) => (status = e.detail)} -->
|
||||
<TabPane tabId="foot" tab="Footprint" active>
|
||||
<CardBody>
|
||||
{#each footprintData as fpd, index}
|
||||
{#if fpd.impact !== 4}
|
||||
<div class="mb-1 d-flex justify-content-between">
|
||||
<div> <b>{fpd.name} ({fpd.stat})</b></div>
|
||||
<div
|
||||
class="cursor-help d-inline-flex"
|
||||
id={`footprint-${job.jobId}-${index}`}
|
||||
>
|
||||
<div class="mx-1">
|
||||
{#if fpd.impact === 3 || fpd.impact === -1}
|
||||
<Icon name="exclamation-triangle-fill" class="text-danger" />
|
||||
{:else if fpd.impact === 2}
|
||||
<Icon name="exclamation-triangle" class="text-warning" />
|
||||
{/if}
|
||||
{#if fpd.impact === 3}
|
||||
<Icon name="emoji-frown" class="text-danger" />
|
||||
{:else if fpd.impact === 2}
|
||||
<Icon name="emoji-neutral" class="text-warning" />
|
||||
{:else if fpd.impact === 1}
|
||||
<Icon name="emoji-smile" class="text-success" />
|
||||
{:else if fpd.impact === 0}
|
||||
<Icon name="emoji-laughing" class="text-info" />
|
||||
{:else if fpd.impact === -1}
|
||||
<Icon name="emoji-dizzy" class="text-danger" />
|
||||
{/if}
|
||||
{#if showFootprint}
|
||||
<TabPane tabId="foot" tab="Footprint" active>
|
||||
<CardBody>
|
||||
{#each footprintData as fpd, index}
|
||||
{#if fpd.impact !== 4}
|
||||
<div class="mb-1 d-flex justify-content-between">
|
||||
<div> <b>{fpd.name} ({fpd.stat})</b></div>
|
||||
<div
|
||||
class="cursor-help d-inline-flex"
|
||||
id={`footprint-${job.jobId}-${index}`}
|
||||
>
|
||||
<div class="mx-1">
|
||||
{#if fpd.impact === 3 || fpd.impact === -1}
|
||||
<Icon name="exclamation-triangle-fill" class="text-danger" />
|
||||
{:else if fpd.impact === 2}
|
||||
<Icon name="exclamation-triangle" class="text-warning" />
|
||||
{/if}
|
||||
{#if fpd.impact === 3}
|
||||
<Icon name="emoji-frown" class="text-danger" />
|
||||
{:else if fpd.impact === 2}
|
||||
<Icon name="emoji-neutral" class="text-warning" />
|
||||
{:else if fpd.impact === 1}
|
||||
<Icon name="emoji-smile" class="text-success" />
|
||||
{:else if fpd.impact === 0}
|
||||
<Icon name="emoji-laughing" class="text-info" />
|
||||
{:else if fpd.impact === -1}
|
||||
<Icon name="emoji-dizzy" class="text-danger" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{fpd.value} / {fpd.peak}
|
||||
{fpd.unit}
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
target={`footprint-${job.jobId}-${index}`}
|
||||
placement="right"
|
||||
>{fpd.message}</Tooltip
|
||||
>
|
||||
</div>
|
||||
<Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
|
||||
{#if fpd.dir}
|
||||
<Col xs="1">
|
||||
<Icon name="caret-left-fill" />
|
||||
</Col>
|
||||
{/if}
|
||||
<Col xs="11" class="align-content-center">
|
||||
<Progress value={fpd.value} max={fpd.peak} color={fpd.color} />
|
||||
</Col>
|
||||
{#if !fpd.dir}
|
||||
<Col xs="1">
|
||||
<Icon name="caret-right-fill" />
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
{:else}
|
||||
<div class="mb-1 d-flex justify-content-between">
|
||||
<div>
|
||||
{fpd.value} / {fpd.peak}
|
||||
{fpd.unit}
|
||||
<b>{fpd.name} ({fpd.stat})</b>
|
||||
</div>
|
||||
<div
|
||||
class="cursor-help d-inline-flex"
|
||||
id={`footprint-${job.jobId}-${index}`}
|
||||
>
|
||||
<div class="mx-1">
|
||||
<Icon name="info-circle"/>
|
||||
</div>
|
||||
<div>
|
||||
{fpd.value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
@@ -267,49 +309,12 @@
|
||||
placement="right"
|
||||
>{fpd.message}</Tooltip
|
||||
>
|
||||
</div>
|
||||
<Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
|
||||
{#if fpd.dir}
|
||||
<Col xs="1">
|
||||
<Icon name="caret-left-fill" />
|
||||
</Col>
|
||||
{/if}
|
||||
<Col xs="11" class="align-content-center">
|
||||
<Progress value={fpd.value} max={fpd.peak} color={fpd.color} />
|
||||
</Col>
|
||||
{#if !fpd.dir}
|
||||
<Col xs="1">
|
||||
<Icon name="caret-right-fill" />
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
{:else}
|
||||
<div class="mb-1 d-flex justify-content-between">
|
||||
<div>
|
||||
<b>{fpd.name} ({fpd.stat})</b>
|
||||
</div>
|
||||
<div
|
||||
class="cursor-help d-inline-flex"
|
||||
id={`footprint-${job.jobId}-${index}`}
|
||||
>
|
||||
<div class="mx-1">
|
||||
<Icon name="info-circle"/>
|
||||
</div>
|
||||
<div>
|
||||
{fpd.value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
target={`footprint-${job.jobId}-${index}`}
|
||||
placement="right"
|
||||
>{fpd.message}</Tooltip
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</CardBody>
|
||||
</TabPane>
|
||||
<TabPane tabId="polar" tab="Polar">
|
||||
{/if}
|
||||
{/each}
|
||||
</CardBody>
|
||||
</TabPane>
|
||||
{/if}
|
||||
<TabPane tabId="polar" tab="Polar" active={!showFootprint}>
|
||||
<CardBody>
|
||||
<Polar
|
||||
{footprintData}
|
||||
@@ -317,19 +322,21 @@
|
||||
/>
|
||||
</CardBody>
|
||||
</TabPane>
|
||||
<TabPane tabId="summary" tab="Summary">
|
||||
<CardBody>
|
||||
<p>Based on footprint data, this job performs as follows:</p>
|
||||
<hr/>
|
||||
<ul>
|
||||
{#each summaryMessages as sm}
|
||||
<li>
|
||||
{@html sm}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</CardBody>
|
||||
</TabPane>
|
||||
<!--
|
||||
<TabPane tabId="summary" tab="Summary">
|
||||
<CardBody>
|
||||
<p>Based on footprint data, this job performs as follows:</p>
|
||||
<hr/>
|
||||
<ul>
|
||||
{#each summaryMessages as sm}
|
||||
<li>
|
||||
{@html sm}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</CardBody>
|
||||
</TabPane>
|
||||
-->
|
||||
</TabContent>
|
||||
</Card>
|
||||
|
||||
|
@@ -1,456 +0,0 @@
|
||||
<!--
|
||||
@component Job View Subcomponent; allows management of job tags by deletion or new entries
|
||||
|
||||
Properties:
|
||||
- `job Object`: The job object
|
||||
- `jobTags [Number]`: The array of currently designated tags
|
||||
- `username String`: Empty string if auth. is disabled, otherwise the username as string
|
||||
- `authlevel Number`: The current users authentication level
|
||||
- `roles [Number]`: Enum containing available roles
|
||||
- `renderModal Bool?`: If component is rendered as bootstrap modal button [Default: true]
|
||||
-->
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Icon,
|
||||
Button,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Spinner,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
Alert,
|
||||
Tooltip,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { fuzzySearchTags } from "../generic/utils.js";
|
||||
import Tag from "../generic/helper/Tag.svelte";
|
||||
|
||||
export let job;
|
||||
export let jobTags = job.tags;
|
||||
export let username;
|
||||
export let authlevel;
|
||||
export let roles;
|
||||
export let renderModal = true;
|
||||
|
||||
let allTags = getContext("tags"),
|
||||
initialized = getContext("initialized");
|
||||
let newTagType = "",
|
||||
newTagName = "",
|
||||
newTagScope = username;
|
||||
let filterTerm = "";
|
||||
let pendingChange = false;
|
||||
let isOpen = false;
|
||||
const isAdmin = (roles && authlevel >= roles.admin);
|
||||
|
||||
const client = getContextClient();
|
||||
|
||||
const createTagMutation = ({ type, name, scope }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($type: String!, $name: String!, $scope: String!) {
|
||||
createTag(type: $type, name: $name, scope: $scope) {
|
||||
id
|
||||
type
|
||||
name
|
||||
scope
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { type, name, scope },
|
||||
});
|
||||
};
|
||||
|
||||
const addTagsToJobMutation = ({ job, tagIds }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($job: ID!, $tagIds: [ID!]!) {
|
||||
addTagsToJob(job: $job, tagIds: $tagIds) {
|
||||
id
|
||||
type
|
||||
name
|
||||
scope
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { job, tagIds },
|
||||
});
|
||||
};
|
||||
|
||||
const removeTagsFromJobMutation = ({ job, tagIds }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($job: ID!, $tagIds: [ID!]!) {
|
||||
removeTagsFromJob(job: $job, tagIds: $tagIds) {
|
||||
id
|
||||
type
|
||||
name
|
||||
scope
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { job, tagIds },
|
||||
});
|
||||
};
|
||||
|
||||
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags));
|
||||
$: usedTagsFiltered = matchJobTags(jobTags, allTagsFiltered, 'used');
|
||||
$: unusedTagsFiltered = matchJobTags(jobTags, allTagsFiltered, 'unused');
|
||||
|
||||
$: {
|
||||
newTagType = "";
|
||||
newTagName = "";
|
||||
let parts = filterTerm.split(":").map((s) => s.trim());
|
||||
if (parts.length == 2 && parts.every((s) => s.length > 0)) {
|
||||
newTagType = parts[0];
|
||||
newTagName = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
for (let tag of allTagsFiltered)
|
||||
if (tag.type == type && tag.name == name) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function createTag(type, name, scope) {
|
||||
pendingChange = true;
|
||||
createTagMutation({ type: type, name: name, scope: scope }).subscribe((res) => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
pendingChange = false;
|
||||
allTags = [...allTags, res.data.createTag];
|
||||
newTagType = "";
|
||||
newTagName = "";
|
||||
addTagToJob(res.data.createTag);
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addTagToJob(tag) {
|
||||
pendingChange = tag.id;
|
||||
addTagsToJobMutation({ job: job.id, tagIds: [tag.id] }).subscribe((res) => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
jobTags = job.tags = res.data.addTagsToJob;
|
||||
pendingChange = false;
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeTagFromJob(tag) {
|
||||
pendingChange = tag.id;
|
||||
removeTagsFromJobMutation({ job: job.id, tagIds: [tag.id] }).subscribe(
|
||||
(res) => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
jobTags = job.tags = res.data.removeTagsFromJob;
|
||||
pendingChange = false;
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if renderModal}
|
||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||
<ModalHeader>
|
||||
Manage Tags
|
||||
{#if pendingChange !== false}
|
||||
<Spinner size="sm" secondary />
|
||||
{:else}
|
||||
<Icon name="tags" />
|
||||
{/if}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<InputGroup class="mb-3">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search Tags"
|
||||
bind:value={filterTerm}
|
||||
/>
|
||||
<InputGroupText id={`tag-management-info-modal`} style="cursor:help; font-size:larger;align-content:center;">
|
||||
<Icon name=info-circle/>
|
||||
</InputGroupText>
|
||||
<Tooltip
|
||||
target={`tag-management-info-modal`}
|
||||
placement="right">
|
||||
Search using "type: name". If no tag matches your search, a
|
||||
button for creating a new one will appear.
|
||||
</Tooltip>
|
||||
</InputGroup>
|
||||
|
||||
<div class="mb-3 scroll-group">
|
||||
{#if usedTagsFiltered.length > 0}
|
||||
<ListGroup class="mb-3">
|
||||
{#each usedTagsFiltered as utag}
|
||||
<ListGroupItem color="light">
|
||||
<Tag tag={utag} />
|
||||
|
||||
<span style="float: right;">
|
||||
{#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 attached tags matching.</i>
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
{:else}
|
||||
<ListGroup class="mb-3">
|
||||
<ListGroupItem disabled>
|
||||
<i>Job has no attached tags.</i>
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
{/if}
|
||||
|
||||
{#if unusedTagsFiltered.length > 0}
|
||||
<ListGroup class="">
|
||||
{#each unusedTagsFiltered as uutag}
|
||||
<ListGroupItem color="secondary">
|
||||
<Tag tag={uutag} />
|
||||
|
||||
<span style="float: right;">
|
||||
{#if pendingChange === uutag.id}
|
||||
<Spinner size="sm" secondary />
|
||||
{:else}
|
||||
<Button
|
||||
size="sm"
|
||||
color="success"
|
||||
on:click={() => addTagToJob(uutag)}
|
||||
>
|
||||
<Icon name="plus" />
|
||||
</Button>
|
||||
{/if}
|
||||
</span>
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
{:else if filterTerm !== ""}
|
||||
<ListGroup class="">
|
||||
<ListGroupItem disabled>
|
||||
<i>No unused tags matching.</i>
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
{:else}
|
||||
<ListGroup class="">
|
||||
<ListGroupItem disabled>
|
||||
<i>No unused tags available.</i>
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#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
|
||||
outline
|
||||
style="width:100%;"
|
||||
color="success"
|
||||
on:click={(e) => (
|
||||
e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
|
||||
)}
|
||||
>
|
||||
Add new tag:
|
||||
<Tag tag={{ type: newTagType, name: newTagName, scope: newTagScope }} clickable={false}/>
|
||||
</Button>
|
||||
</Col>
|
||||
{#if isAdmin}
|
||||
<Col xs={5} md={12} lg={5} xl={12} xxl={5} class="mb-2" style="align-content:center;">
|
||||
<Input type="select" bind:value={newTagScope}>
|
||||
<option value={username}>Scope: Private</option>
|
||||
<option value={"global"}>Scope: Global</option>
|
||||
<option value={"admin"}>Scope: Admin</option>
|
||||
</Input>
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
{:else if filterTerm !== "" && allTagsFiltered.length == 0}
|
||||
<Alert color="info">
|
||||
Search Term is not a valid Tag (<code>type: name</code>)
|
||||
</Alert>
|
||||
{:else if filterTerm == "" && unusedTagsFiltered.length == 0}
|
||||
<Alert color="info">
|
||||
Type "<code>type: name</code>" into the search field to create a new tag.
|
||||
</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
|
||||
type="text"
|
||||
placeholder="Search Tags"
|
||||
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>
|
||||
|
||||
{#if usedTagsFiltered.length > 0}
|
||||
<ListGroup class="mb-3">
|
||||
{#each usedTagsFiltered as utag}
|
||||
<ListGroupItem color="light">
|
||||
<Tag tag={utag} />
|
||||
|
||||
<span style="float: right;">
|
||||
{#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 attached tags matching.</i>
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
{:else}
|
||||
<ListGroup class="mb-3">
|
||||
<ListGroupItem disabled>
|
||||
<i>Job has no attached tags.</i>
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
{/if}
|
||||
|
||||
{#if unusedTagsFiltered.length > 0}
|
||||
<ListGroup class="mb-3">
|
||||
{#each unusedTagsFiltered as uutag}
|
||||
<ListGroupItem color="secondary">
|
||||
<Tag tag={uutag} />
|
||||
|
||||
<span style="float: right;">
|
||||
{#if pendingChange === uutag.id}
|
||||
<Spinner size="sm" secondary />
|
||||
{:else}
|
||||
<Button
|
||||
size="sm"
|
||||
color="success"
|
||||
on:click={() => addTagToJob(uutag)}
|
||||
>
|
||||
<Icon name="plus" />
|
||||
</Button>
|
||||
{/if}
|
||||
</span>
|
||||
</ListGroupItem>
|
||||
{/each}
|
||||
</ListGroup>
|
||||
{:else if filterTerm !== ""}
|
||||
<ListGroup class="mb-3">
|
||||
<ListGroupItem disabled>
|
||||
<i>No unused tags matching.</i>
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
{:else}
|
||||
<ListGroup class="mb-3">
|
||||
<ListGroupItem disabled>
|
||||
<i>No unused tags available.</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
|
||||
outline
|
||||
style="width:100%;"
|
||||
color="success"
|
||||
on:click={(e) => (
|
||||
e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
|
||||
)}
|
||||
>
|
||||
Add new tag:
|
||||
<Tag tag={{ type: newTagType, name: newTagName, scope: newTagScope }} clickable={false}/>
|
||||
</Button>
|
||||
</Col>
|
||||
{#if isAdmin}
|
||||
<Col xs={5} md={12} lg={5} xl={12} xxl={5} class="mb-2" style="align-content:center;">
|
||||
<Input type="select" bind:value={newTagScope}>
|
||||
<option value={username}>Scope: Private</option>
|
||||
<option value={"global"}>Scope: Global</option>
|
||||
<option value={"admin"}>Scope: Admin</option>
|
||||
</Input>
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
{:else if filterTerm !== "" && allTagsFiltered.length == 0}
|
||||
<Alert color="info">
|
||||
Search Term is not a valid Tag (<code>type: name</code>)
|
||||
</Alert>
|
||||
{:else if filterTerm == "" && unusedTagsFiltered.length == 0}
|
||||
<Alert color="info">
|
||||
Type "<code>type: name</code>" into the search field to create a new tag.
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.scroll-group {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user