Skip to content

Commit 202a1f0

Browse files
authored
feat(frontend): add styled ResourcePicker component
This commit adds a new ResourcePicker component for choosing Resources in the create/edit forms. The component uses the ResourceBadge for consistent Resource colors and icons. It supports single and multi select and incorporates the outdated/sync button. The following forms are updated to use the new component: - create Job page - create Experiment page - Entrypoint Queues select - EntryPoint assign Plugins dropdown - edit Artifacts page
1 parent 36960e6 commit 202a1f0

13 files changed

Lines changed: 385 additions & 419 deletions

src/frontend/src/assets/main.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,5 +178,5 @@ legend {
178178
}
179179

180180
.bg-red-light {
181-
background-color: rgba(244, 67, 54, 0.02);
181+
background-color: rgba(244, 67, 54, 0.06);
182182
}
Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,22 @@
11
<template>
2-
<q-select
3-
outlined
4-
dense
2+
<ResourcePicker
53
v-model="selectedPlugins"
6-
use-input
7-
use-chips
8-
multiple
9-
option-label="name"
10-
option-value="id"
11-
input-debounce="100"
124
:options="pluginOptions"
5+
resourceType="plugin"
6+
label="Plugins:"
137
@filter="getPlugins"
148
@add="(added) => addPlugin(added.value)"
159
@remove="(removed) => removePlugin(removed.value)"
16-
>
17-
<template v-slot:before>
18-
<div class="field-label">Plugins:</div>
19-
</template>
20-
<template v-slot:selected>
21-
<div>
22-
<div
23-
v-for="(plugin, i) in selectedPlugins"
24-
:key="plugin.id"
25-
:class="i > 0 ? 'q-mt-xs' : ''"
26-
>
27-
<q-chip
28-
removable
29-
color="secondary"
30-
text-color="white"
31-
class="q-ml-xs "
32-
@remove="selectedPlugins.splice(i, 1); removePlugin(plugin)"
33-
>
34-
{{ plugin.name }}
35-
<q-badge
36-
v-if="!plugin.latestSnapshot"
37-
color="red"
38-
label="outdated"
39-
rounded
40-
class="q-ml-xs"
41-
/>
42-
</q-chip>
43-
<q-btn
44-
v-if="!plugin.latestSnapshot"
45-
round
46-
color="red"
47-
icon="sync"
48-
size="sm"
49-
@click.stop="syncPlugin(plugin.id, i)"
50-
>
51-
<q-tooltip>
52-
Sync to latest version of plugin
53-
</q-tooltip>
54-
</q-btn>
55-
</div>
56-
</div>
57-
</template>
58-
</q-select>
10+
:stacked-badges="true"
11+
@sync="(plugin, index) => syncPlugin(plugin.id, index)"
12+
/>
5913
</template>
6014

6115
<script setup>
6216
import { ref, watch } from 'vue'
6317
import * as api from '@/services/dataApi'
18+
import ResourcePicker from '@/components/ResourcePicker.vue'
19+
import * as notify from '../notify'
6420
6521
const selectedPlugins = defineModel('selectedPlugins')
6622
const originalSelectedPluginIds = ref([])
@@ -96,8 +52,10 @@ async function syncPlugin(pluginId, index) {
9652
const res = await api.getItem('plugins', pluginId)
9753
selectedPlugins.value.splice(index, 1, res.data)
9854
pluginIDsToUpdate.value.push(pluginId)
55+
notify.success(`Synced '${res.data.name}'`)
9956
} catch(err) {
10057
console.warn(err)
58+
notify.error(err?.response?.data?.message || 'Failed to sync plugin')
10159
}
10260
}
10361
@@ -113,4 +71,4 @@ function removePlugin(plugin) {
11371
pluginIDsToUpdate.value = pluginIDsToUpdate.value.filter((id) => id !== plugin.id)
11472
}
11573
116-
</script>
74+
</script>
Lines changed: 102 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,120 @@
11
<template>
2-
<q-chip
3-
:color="styles.color"
4-
square
5-
outline
6-
clickable
7-
@click.stop="openResource"
8-
@auxclick.stop="onAuxClick"
9-
>
10-
<q-icon
11-
v-if="styles.icon"
12-
:name="styles.icon"
13-
size="xs"
14-
class="q-mr-sm"
15-
/>
16-
{{ resource?.name }}
17-
<span
18-
v-if="resource?.deleted"
19-
class="q-ml-sm"
2+
<span class="q-gutter-x-xs" :class="{ 'rb-stacked': props.stacked }">
3+
<q-chip
4+
:color="styles.color"
5+
square
6+
outline
7+
:clickable="props.clickable"
8+
:removable="removable"
9+
@remove="$emit('remove')"
10+
@click.stop="openResource"
11+
@auxclick.stop="onAuxClick"
2012
>
21-
22-
</span>
23-
<q-tooltip v-if="resource?.deleted">
24-
This <span class="text-capitalize">{{ resourceType }}</span> has been deleted
25-
</q-tooltip>
26-
<q-tooltip
27-
v-else
28-
class="text-capitalize"
29-
>
30-
Go To: {{ resourceType }} (ID {{ resource.id
31-
}}{{ resource.snapshotId ? `, Snapshot ${resource.snapshotId}` : "" }})
32-
</q-tooltip>
33-
34-
<q-menu context-menu>
35-
<q-list dense>
36-
<q-item
37-
clickable
38-
v-close-popup
39-
@click.stop="openResource"
40-
>
41-
<q-item-section>Open</q-item-section>
42-
</q-item>
43-
<q-item
44-
clickable
45-
v-close-popup
46-
@click.stop="openInNewTab"
13+
<span ref="chipTarget" class="rb-label">
14+
<q-icon
15+
v-if="styles.icon"
16+
:name="styles.icon"
17+
size="xs"
18+
class="q-mr-sm"
19+
/>
20+
{{ resource?.name }}
21+
<span
22+
v-if="resource?.deleted"
23+
class="q-ml-sm"
4724
>
48-
<q-item-section>Open In New Tab</q-item-section>
49-
</q-item>
50-
</q-list>
51-
</q-menu>
52-
</q-chip>
25+
26+
</span>
27+
28+
<q-badge
29+
v-if="resource?.latestSnapshot === false"
30+
color="red"
31+
label="outdated"
32+
rounded
33+
class="q-ml-xs"
34+
/>
35+
</span>
36+
37+
<q-btn
38+
v-if="resource?.latestSnapshot === false"
39+
round
40+
dense
41+
color="red"
42+
icon="sync"
43+
size="xs"
44+
padding="xs"
45+
class="q-ml-xs"
46+
@click.stop="$emit('sync')"
47+
>
48+
<q-tooltip>
49+
Sync to latest version
50+
</q-tooltip>
51+
</q-btn>
52+
53+
<q-tooltip v-if="resource?.deleted" :target="chipTarget">
54+
This <span class="text-capitalize">{{ resourceType }}</span> has been deleted
55+
</q-tooltip>
56+
<q-tooltip
57+
v-else-if="props.clickable"
58+
class="text-capitalize"
59+
:target="chipTarget"
60+
>
61+
Go To: {{ resourceType }} (ID {{ resource.id
62+
}}{{ resource.snapshotId ? `, Snapshot ${resource.snapshotId}` : "" }})
63+
</q-tooltip>
64+
65+
<q-menu context-menu>
66+
<q-list dense>
67+
<q-item
68+
clickable
69+
v-close-popup
70+
@click.stop="openResource"
71+
>
72+
<q-item-section>Open</q-item-section>
73+
</q-item>
74+
<q-item
75+
clickable
76+
v-close-popup
77+
@click.stop="openInNewTab"
78+
>
79+
<q-item-section>Open In New Tab</q-item-section>
80+
</q-item>
81+
</q-list>
82+
</q-menu>
83+
</q-chip>
84+
85+
86+
</span>
5387
</template>
5488

5589
<script setup>
56-
import { computed, inject } from "vue"
90+
import { computed, inject, ref } from "vue"
5791
import { getResourceStyle } from "@/services/resourceStyles"
5892
import { useRouter } from "vue-router"
5993
94+
const emit = defineEmits(['sync', 'remove'])
6095
const router = useRouter()
6196
const darkMode = inject("darkMode")
6297
6398
const props = defineProps({
6499
resource: Object,
65100
resourceType: String,
101+
removable: { type: Boolean, default: false },
102+
clickable: { type: Boolean, default: true },
103+
// When true, place the badge on its own line
104+
stacked: { type: Boolean, default: false }
66105
})
67106
107+
const chipTarget = ref()
108+
68109
const styles = computed(() => {
69110
return getResourceStyle(props.resourceType, darkMode.value)
70111
})
71112
72113
const formattedUrl = computed(() => {
73114
const url = props.resource?.url?.replace(/^\/api\/v1/, "")
74-
if (!url) return null
115+
if (!url) {
116+
return `/${props.resourceType}s/${props.resource.id}`
117+
}
75118
76119
const parts = url.split("/").filter(Boolean)
77120
@@ -106,3 +149,11 @@ function openResource(event) {
106149
router.push(formattedUrl.value)
107150
}
108151
</script>
152+
153+
<style scoped>
154+
.rb-stacked {
155+
display: block;
156+
width: 100%;
157+
margin-top: 4px;
158+
}
159+
</style>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<template>
2+
<q-select
3+
:options="options"
4+
:model-value="modelValue"
5+
@update:model-value="(val) => $emit('update:model-value', val)"
6+
outlined
7+
dense
8+
use-input
9+
:multiple="multiple"
10+
map-options
11+
option-label="name"
12+
option-value="id"
13+
input-debounce="300"
14+
v-bind="$attrs"
15+
>
16+
<template v-slot:before>
17+
<div v-if="label" class="field-label">{{ label }}</div>
18+
</template>
19+
<template v-if="multiple" v-slot:selected-item="scope">
20+
<ResourceBadge
21+
:resource="scope.opt"
22+
:resourceType="resourceType"
23+
:removable="!$attrs.disable"
24+
:clickable="false"
25+
:stacked="stackedBadges"
26+
@remove="scope.removeAtIndex(scope.index)"
27+
@sync="$emit('sync', scope.opt, scope.index)"
28+
/>
29+
</template>
30+
<template v-else v-slot:selected>
31+
<ResourceBadge
32+
v-if="modelValue && typeof modelValue === 'object'"
33+
:resource="modelValue"
34+
:resourceType="resourceType"
35+
:removable="false"
36+
:clickable="false"
37+
@sync="$emit('sync', modelValue)"
38+
/>
39+
</template>
40+
<template v-slot:option="scope">
41+
<q-item
42+
v-bind="scope.itemProps"
43+
:active="scope.selected"
44+
:active-class="darkMode ? 'bg-blue-grey-9 text-white' : 'bg-blue-grey-1'"
45+
>
46+
<q-item-section avatar>
47+
<q-icon :name="styles.icon" :color="styles.color" />
48+
</q-item-section>
49+
<q-item-section>
50+
<q-item-label>{{ scope.opt.name }}</q-item-label>
51+
<q-item-label caption v-if="scope.opt.description">
52+
{{ scope.opt.description }}
53+
</q-item-label>
54+
<slot name="option-extra" :opt="scope.opt" />
55+
</q-item-section>
56+
</q-item>
57+
</template>
58+
<template v-slot:hint>
59+
<slot name="hint" />
60+
</template>
61+
</q-select>
62+
</template>
63+
64+
<script setup>
65+
import ResourceBadge from '@/components/ResourceBadge.vue'
66+
import { computed, inject } from "vue"
67+
import { getResourceStyle } from "@/services/resourceStyles"
68+
69+
const darkMode = inject("darkMode")
70+
71+
const styles = computed(() => {
72+
return getResourceStyle(props.resourceType, darkMode.value)
73+
})
74+
75+
const props = defineProps({
76+
modelValue: {
77+
// Accept array for multi-select and object/null for single-select
78+
default: () => [],
79+
},
80+
options: {
81+
type: Array,
82+
default: () => [],
83+
},
84+
resourceType: {
85+
type: String
86+
},
87+
label: {
88+
type: String
89+
},
90+
multiple: {
91+
type: Boolean,
92+
default: true,
93+
},
94+
stackedBadges: {
95+
type: Boolean,
96+
default: false,
97+
},
98+
})
99+
100+
defineEmits(["update:model-value", "sync"]);
101+
102+
</script>
103+
104+
<style scoped>
105+
:deep(.q-field__append) {
106+
margin-top: 3px;
107+
}
108+
</style>

0 commit comments

Comments
 (0)