vue-girder-slicer-cli-ui 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +102 -0
  2. package/dist/api/girderRest.d.ts +36 -0
  3. package/dist/api/girderSlicerApi.d.ts +32 -0
  4. package/dist/components/FileBrowser/DataBrowser.tw.vue.d.ts +177 -0
  5. package/dist/components/FileBrowser/Modal.tw.vue.d.ts +27 -0
  6. package/dist/components/FileBrowser/RootSelection.tw.vue.d.ts +41 -0
  7. package/dist/components/FileBrowser/utils.d.ts +9 -0
  8. package/dist/components/GirderControlsPanel.tw.vue.d.ts +29 -0
  9. package/dist/components/GirderSlicerParameter.tw.vue.d.ts +50 -0
  10. package/dist/components/GirderSlicerTaskButton.tw.vue.d.ts +42 -0
  11. package/dist/components/GirderSlicerTaskCard.tw.vue.d.ts +44 -0
  12. package/dist/components/GirderSlicerTaskMenu.tw.vue.d.ts +49 -0
  13. package/dist/components/GlicerSlicerTaskMenuModal.tw.vue.d.ts +34 -0
  14. package/dist/components/GlicerSlicerTaskMenuModalButton.tw.vue.d.ts +38 -0
  15. package/dist/components/GriderSlicerTasksIntegrated.tw.vue.d.ts +74 -0
  16. package/dist/components/Parameters/BaseParameter.tw.vue.d.ts +21 -0
  17. package/dist/components/Parameters/BooleanParameter.tw.vue.d.ts +18 -0
  18. package/dist/components/Parameters/ColorParameter.tw.vue.d.ts +19 -0
  19. package/dist/components/Parameters/EnumerationParameters.tw.vue.d.ts +19 -0
  20. package/dist/components/Parameters/FileParameter.tw.vue.d.ts +32 -0
  21. package/dist/components/Parameters/RangeParameter.tw.vue.d.ts +19 -0
  22. package/dist/favicon.ico +0 -0
  23. package/dist/girderTypes.d.ts +21 -0
  24. package/dist/index.d.ts +11 -0
  25. package/dist/parser/constraints.d.ts +4 -0
  26. package/dist/parser/convert.d.ts +7 -0
  27. package/dist/parser/defaultValue.d.ts +12 -0
  28. package/dist/parser/group.d.ts +9 -0
  29. package/dist/parser/index.d.ts +8 -0
  30. package/dist/parser/panel.d.ts +11 -0
  31. package/dist/parser/param.d.ts +6 -0
  32. package/dist/parser/parse.d.ts +20 -0
  33. package/dist/parser/parserTypes.d.ts +58 -0
  34. package/dist/parser/widget.d.ts +6 -0
  35. package/dist/style.css +1 -0
  36. package/dist/tailwind.css +1370 -0
  37. package/dist/utils.d.ts +5 -0
  38. package/dist/vue-girder-slicer-cli-ui.js +12987 -0
  39. package/dist/vue-girder-slicer-cli-ui.umd.cjs +59 -0
  40. package/package.json +135 -0
  41. package/src/components/FileBrowser/DataBrowser.tw.vue +842 -0
  42. package/src/components/FileBrowser/Modal.tw.vue +82 -0
  43. package/src/components/FileBrowser/RootSelection.tw.vue +83 -0
  44. package/src/components/FileBrowser/utils.ts +48 -0
  45. package/src/components/GirderControlsPanel.tw.vue +94 -0
  46. package/src/components/GirderSlicerParameter.tw.vue +84 -0
  47. package/src/components/GirderSlicerTaskButton.tw.vue +208 -0
  48. package/src/components/GirderSlicerTaskCard.tw.vue +184 -0
  49. package/src/components/GirderSlicerTaskMenu.tw.vue +217 -0
  50. package/src/components/GlicerSlicerTaskMenuModal.tw.vue +77 -0
  51. package/src/components/GlicerSlicerTaskMenuModalButton.tw.vue +106 -0
  52. package/src/components/GriderSlicerTasksIntegrated.tw.vue +73 -0
  53. package/src/components/Parameters/BaseParameter.tw.vue +76 -0
  54. package/src/components/Parameters/BooleanParameter.tw.vue +54 -0
  55. package/src/components/Parameters/ColorParameter.tw.vue +58 -0
  56. package/src/components/Parameters/EnumerationParameters.tw.vue +66 -0
  57. package/src/components/Parameters/FileParameter.tw.vue +172 -0
  58. package/src/components/Parameters/RangeParameter.tw.vue +74 -0
@@ -0,0 +1,82 @@
1
+ <script lang='ts'>
2
+ import {defineComponent} from 'vue';
3
+ import { mdiClose } from '@mdi/js';
4
+ import SvgIcon from '@jamescoyle/vue-icon';
5
+
6
+ export default defineComponent({
7
+ components: {
8
+ SvgIcon
9
+ },
10
+ props: {
11
+ isVisible: {
12
+ type: Boolean,
13
+ default: false,
14
+ },
15
+ disabledConfirm: {
16
+ type: Boolean,
17
+ default: true,
18
+ }
19
+ },
20
+ setup(_, { emit}) {
21
+ const cancel = () => {
22
+ emit('cancel')
23
+ }
24
+ const confirm = () => {
25
+ emit('confirm')
26
+ }
27
+ return {
28
+ cancel,
29
+ confirm,
30
+ mdiClose,
31
+
32
+ }
33
+ }
34
+ })
35
+ </script>
36
+
37
+ <template>
38
+ <div v-if="isVisible">
39
+ <div class="fixed inset-0 z-50 flex justify-center items-center ">
40
+ <div class="gsu-card flex flex-col max-w-5xl rounded-lg shadow-lg">
41
+ <!-- Header -->
42
+ <div class="p-5">
43
+ <div class="grid grid-cols-12">
44
+ <span class="col-span-10 text-xl my-2 ml-5">
45
+ <slot name="header">
46
+ <h3 class="text-2xl font-semibold">Modal Header</h3>
47
+ </slot>
48
+ </span>
49
+ <span class="col-start-12 col-span-1 justify-self-end mr-5 my-2 " @click="$emit('cancel')">
50
+ <svg-icon
51
+ type="mdi"
52
+ :path="mdiClose"
53
+ :size="30"
54
+ class="gsu-icon gsu-clickable"
55
+ data-dismiss="modal"
56
+ aria-label="Close"
57
+ >
58
+ <span aria-hidden="true">&times;</span>
59
+ </svg-icon>
60
+ </span>
61
+ </div>
62
+ </div>
63
+
64
+ <slot name="body">
65
+ <div class="p-6">
66
+ <p>This is a modal body content. Let's make this line a bit longer to see the width.</p>
67
+ </div>
68
+ </slot>
69
+
70
+ <div class="p-6 flex justify-end items-center">
71
+ <button class="bg-red-500 border-solid border-2 border-borderColor font-bold py-2 px-4 rounded" @click="cancel">Cancel</button>
72
+ <button
73
+ class=" btn ml-2 font-bold py-2 px-4 rounded"
74
+ :class="{ 'gsu-disabled-button': disabledConfirm, 'gsu-btn-accept': !disabledConfirm }"
75
+ @click="!disabledConfirm && confirm()"
76
+ >Confirm</button>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <div class="opacity-25 fixed inset-0 z-40 bg-black"></div>
81
+ </div>
82
+ </template>
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import { PropType, computed, defineComponent } from 'vue'
3
+ import { GirderModel, GirderModelType} from '../../girderTypes';
4
+ export default defineComponent({
5
+ props: {
6
+ value: {
7
+ type: String,
8
+ required: true,
9
+ },
10
+ home: {
11
+ type: String,
12
+ required: true,
13
+ },
14
+ users: {
15
+ type: Array as PropType<GirderModel[]>,
16
+ required: true,
17
+ },
18
+ collections: {
19
+ type: Array as PropType<GirderModel[]>,
20
+ required: true,
21
+ }
22
+ },
23
+ setup(props, { emit }) {
24
+ const filteredUsers = computed(() => props.users?.filter((item) => item._id !== props.home));
25
+ const update = (e: Event) => {
26
+ let type: GirderModelType = 'user';
27
+ const val = (e.target as HTMLSelectElement).value;
28
+ let name = '';
29
+ if (props.collections && props.users) {
30
+ const findType = props.collections.concat(props.users)
31
+ const found = findType.find((item) => item._id === val);
32
+ if (found) {
33
+ type = found._modelType;
34
+ if (type === 'user') {
35
+ name = `${found.firstName} ${found.lastName}`
36
+ } else {
37
+ name = found.name;
38
+ }
39
+ }
40
+ }
41
+ emit('change', {val, type, name});
42
+ }
43
+ return {
44
+ filteredUsers,
45
+ update,
46
+ }
47
+ }
48
+ });
49
+ </script>
50
+ <template>
51
+ <select
52
+ :value="value"
53
+ class="root-select bg-backgroundColor border-solid border-2 border-bgMutedColor"
54
+ @change="update($event)"
55
+ >
56
+ <option :value="home">
57
+ Home
58
+ </option>
59
+ <optgroup label="Collections">
60
+ <option
61
+ v-for="item in collections"
62
+ :key="item._id"
63
+ :value="item._id"
64
+ >
65
+ {{ item.name }}
66
+ </option>
67
+ </optgroup>
68
+ <optgroup label="Users">
69
+ <option
70
+ v-for="item in filteredUsers"
71
+ :key="item._id"
72
+ :value="item._id"
73
+ >
74
+ {{ item.login }}
75
+ </option>
76
+ </optgroup>
77
+ </select>
78
+ </template>
79
+ <style scoped>
80
+ .root-select {
81
+ height: 34px;
82
+ }
83
+ </style>
@@ -0,0 +1,48 @@
1
+ const sizeFormatter =(size: number, { base = 1024, unit = 'B' } = {}) =>{
2
+ if (size < base) {
3
+ return `${size} ${unit}`;
4
+ }
5
+
6
+ let i;
7
+ let val = size;
8
+ for (i = 0; val >= base && i < 4; i += 1) {
9
+ val /= base;
10
+ }
11
+
12
+ return `${val.toFixed(2)} ${['', 'K', 'M', 'G', 'T'][i]}${unit}`;
13
+ };
14
+
15
+ const countFormatter = (count: number | null) => {
16
+ if (count && count > 1000) {
17
+ return `${(count / 1000).toFixed(2)}k`;
18
+ }
19
+ return count ? count : 0;
20
+ }
21
+
22
+ const convertInputNumber = (e: Event) => {
23
+ const val = (e.target as HTMLInputElement).value;
24
+ return parseInt(val);
25
+ }
26
+
27
+ const convertInputString= (e: Event) => {
28
+ const val = (e.target as HTMLInputElement).value;
29
+ return val as string;
30
+ }
31
+
32
+ function isValidRegex(pattern: string) {
33
+ try {
34
+ new RegExp(pattern);
35
+ return true;
36
+ } catch (error) {
37
+ // The RegExp constructor throws a SyntaxError if the pattern is invalid.
38
+ return false;
39
+ }
40
+ }
41
+
42
+ export {
43
+ sizeFormatter,
44
+ isValidRegex,
45
+ countFormatter,
46
+ convertInputNumber,
47
+ convertInputString,
48
+ }
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ import { PropType, defineComponent, ref } from 'vue'
3
+ import { mdiChevronDown, mdiChevronUp } from '@mdi/js' ;
4
+ import SvgIcon from '@jamescoyle/vue-icon';
5
+ import type { XMLPanel, XMLParameters } from '../parser/parserTypes';
6
+ import GirderSlicerParameter from './GirderSlicerParameter.tw.vue';
7
+ export default defineComponent({
8
+ components: {
9
+ GirderSlicerParameter,
10
+ SvgIcon,
11
+ },
12
+ props: {
13
+ panel: {
14
+ type: Object as PropType<XMLPanel>,
15
+ required: true,
16
+ },
17
+ collapseOverride: {
18
+ type: Boolean,
19
+ required: false,
20
+ }
21
+ },
22
+ setup(props, { emit }) {
23
+ const collapsed = ref(!props.panel.advanced)
24
+ const updateParams = (e: XMLParameters[]) =>{
25
+ emit('change', e);
26
+ };
27
+ return {
28
+ collapsed,
29
+ updateParams,
30
+ mdiChevronDown,
31
+ mdiChevronUp,
32
+ }
33
+ }
34
+ });
35
+ </script>
36
+ <template>
37
+ <div class="gsu-card relative flex flex-col min-w-0 rounded break-words">
38
+ <div
39
+ v-if="panel.groups.length === 1"
40
+ class="mb-3 mb-0"
41
+ >
42
+ <div
43
+ class="flex flex-wrap justify-content-left g-0"
44
+ @click="collapsed = !collapsed"
45
+ >
46
+ <div class=":flex-growauto">
47
+ <svg-icon
48
+ type="mdi"
49
+ :path="collapsed ? mdiChevronUp : mdiChevronDown"
50
+ :size="30"
51
+ class="pb-2 icon"
52
+ />
53
+ </div>
54
+ <div class=":flex-growauto">
55
+ <h5>
56
+ {{ panel.groups[0].label }}
57
+ </h5>
58
+ </div>
59
+ </div>
60
+ <div
61
+ v-if="!collapsed"
62
+ class="flex flex-wrap pad-left"
63
+ >
64
+ <girder-slicer-parameter
65
+ :parameters="panel.groups[0].parameters"
66
+ @change="updateParams($event)"
67
+ @input-selected="$emit('input-selected', $event)"
68
+ />
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </template>
73
+ <style scoped>
74
+ .icon:hover {
75
+ cursor: pointer;
76
+ }
77
+ .collapse-enter-active {
78
+ animation: collapsing reverse 500ms ease;
79
+ }
80
+ .collapse-leave-active {
81
+ animation: collapsing 500ms ease;
82
+ }
83
+ .pad-left {
84
+ padding-left: 15px;
85
+ }
86
+ @keyframes collapsing {
87
+ from {
88
+ max-height: 1000px;
89
+ }
90
+ to {
91
+ max-height: 0px;
92
+ }
93
+ }
94
+ </style>
@@ -0,0 +1,84 @@
1
+ <script lang="ts">
2
+ import { PropType, defineComponent, onMounted, ref, } from 'vue';
3
+ import type { XMLParameters } from '../parser/parserTypes';
4
+ import BaseParameter from './Parameters/BaseParameter.tw.vue';
5
+ import BooleanParameter from './Parameters/BooleanParameter.tw.vue';
6
+ import EnumerationParameters from './Parameters/EnumerationParameters.tw.vue';
7
+ import FileParmaeter from './Parameters/FileParameter.tw.vue';
8
+ import ColorParameter from './Parameters/ColorParameter.tw.vue';
9
+ export default defineComponent({
10
+ components: {
11
+ BaseParameter,
12
+ BooleanParameter,
13
+ EnumerationParameters,
14
+ FileParmaeter,
15
+ ColorParameter
16
+ },
17
+ props: {
18
+ parameters: {
19
+ type: Array as PropType<XMLParameters[]>,
20
+ required: true
21
+ }
22
+ },
23
+ setup(props, { emit }) {
24
+ const baseParmaeters = ['string', 'number', 'vector', 'string-vector', 'number-vector']
25
+ const paramCopy = ref(props.parameters);
26
+ onMounted(() => {
27
+ paramCopy.value = props.parameters;
28
+ })
29
+ const updateParameter = (data: XMLParameters, index: number) => {
30
+ paramCopy.value.splice(index, 1, data);
31
+ emit('change', paramCopy.value);
32
+ }
33
+ return {
34
+ baseParmaeters,
35
+ paramCopy,
36
+ updateParameter,
37
+ }
38
+ }
39
+ });
40
+ </script>
41
+ <template>
42
+ <span>
43
+ <div
44
+ v-for="(parameter, index) in paramCopy"
45
+ :key="`${parameter.title}_${index}`"
46
+ >
47
+ <div class="flex flex-wrap">
48
+ <div class="flex-growauto">
49
+ <base-parameter
50
+ v-if="baseParmaeters.includes(parameter.type)"
51
+ :data="parameter"
52
+ @change="updateParameter($event, index)"
53
+ />
54
+ <boolean-parameter
55
+ v-else-if="parameter.slicerType === 'boolean'"
56
+ :data="parameter"
57
+ @change="updateParameter($event, index)"
58
+ />
59
+ <enumeration-parameters
60
+ v-else-if="parameter.slicerType === 'number-enumeration' || parameter.slicerType === 'string-enumeration'"
61
+ :data="parameter"
62
+ @change="updateParameter($event, index)"
63
+ />
64
+ <file-parmaeter
65
+ v-else-if="['file', 'item', 'directory', 'image', 'multi'].includes(parameter.slicerType)"
66
+ :data="parameter"
67
+ @change="updateParameter($event, index)"
68
+ @input-selected="$emit('input-selected', $event)"
69
+ />
70
+ <color-parameter
71
+ v-else-if="parameter.slicerType === 'color'"
72
+ :data="parameter"
73
+ @change="updateParameter($event, index)"
74
+ />
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </span>
79
+ </template>
80
+ <style scoped>
81
+ a {
82
+ color: #42b983;
83
+ }
84
+ </style>
@@ -0,0 +1,208 @@
1
+ <script lang="ts">
2
+ import { Ref, computed, defineComponent, onMounted, ref } from "vue";
3
+ import { extractImageInfo } from "../utils";
4
+ import RestClient from "../api/girderRest";
5
+ import { useGirderSlicerApi } from "../api/girderSlicerApi";
6
+ import { mdiAlert, mdiChevronDown, mdiChevronRight } from "@mdi/js";
7
+ import SvgIcon from "@jamescoyle/vue-icon";
8
+ import { OnClickOutside } from '@vueuse/components'
9
+
10
+ type TaskHierarchy = Record<
11
+ string,
12
+ {
13
+ tag: string;
14
+ tasks: {
15
+ imageBase: string;
16
+ imageTag: string;
17
+ _id: string;
18
+ name: string;
19
+ description: string;
20
+ }[];
21
+ }[]
22
+ >;
23
+ export default defineComponent({
24
+ components: {
25
+ SvgIcon,
26
+ OnClickOutside,
27
+ },
28
+ props: {
29
+ apiUrl: {
30
+ type: String,
31
+ default: "api/v1",
32
+ },
33
+ filter: {
34
+ type: String,
35
+ default: "",
36
+ },
37
+ },
38
+ setup(props, { emit }) {
39
+ const girderRest = new RestClient({ apiRoot: props.apiUrl });
40
+ const loggedIn = computed(() => girderRest?.token);
41
+ const slicerApi = useGirderSlicerApi(girderRest);
42
+ const results: Ref<TaskHierarchy> = ref({});
43
+ const clicked = ref(false);
44
+ const getData = async () => {
45
+ try {
46
+ const response = await slicerApi.getSlicerList();
47
+ // ground items by their image
48
+ const taskHierarchy: TaskHierarchy = {};
49
+ response.data.forEach((task) => {
50
+ // Lets get the base image name
51
+ const imageInfo = extractImageInfo(task.image);
52
+ if (imageInfo) {
53
+ // We create a root for it
54
+ if (taskHierarchy[imageInfo.baseName] === undefined) {
55
+ taskHierarchy[imageInfo.baseName] = [
56
+ { tag: imageInfo.tag, tasks: [] },
57
+ ];
58
+ }
59
+ const foundIndex = taskHierarchy[imageInfo.baseName].findIndex(
60
+ (item) => item.tag === imageInfo.tag
61
+ );
62
+ if (foundIndex !== -1) {
63
+ taskHierarchy[imageInfo.baseName][foundIndex].tasks.push({
64
+ imageBase: imageInfo.baseName,
65
+ imageTag: imageInfo.tag,
66
+ _id: task._id,
67
+ name: task.name,
68
+ description: task.description,
69
+ });
70
+ } else if (foundIndex === -1) {
71
+ taskHierarchy[imageInfo.baseName].push({
72
+ tag: imageInfo.tag,
73
+ tasks: [
74
+ {
75
+ imageBase: imageInfo.baseName,
76
+ imageTag: imageInfo.tag,
77
+ _id: task._id,
78
+ name: task.name,
79
+ description: task.description,
80
+ },
81
+ ],
82
+ });
83
+ }
84
+ }
85
+ });
86
+ results.value = taskHierarchy;
87
+ } catch (err) {
88
+ console.error("Cannot communicate with server");
89
+ }
90
+ };
91
+ const select = (id: string) => {
92
+ emit("selected", id);
93
+ };
94
+ onMounted(() => {
95
+ getData();
96
+ });
97
+ return {
98
+ results,
99
+ mdiAlert,
100
+ mdiChevronDown,
101
+ mdiChevronRight,
102
+ loggedIn,
103
+ select,
104
+ clicked,
105
+ };
106
+ },
107
+ });
108
+ </script>
109
+ <template>
110
+ <div>
111
+ <on-click-outside @trigger="clicked = false">
112
+ <div v-if="loggedIn" class="dropdown inline-block relative w-56">
113
+ <button
114
+ class="bg-backgroundColor text-textColor font-semibold py-2 px-4 rounded inline-flex items-center
115
+ rounded-sm flex items-center min-w-32"
116
+ @click="clicked = true"
117
+ >
118
+ <span class="pr-1 font-semibold flex-1">Tasks</span>
119
+ <span>
120
+ <svg-icon type="mdi" :path="mdiChevronDown" :size="30" class="pb-1" />
121
+
122
+ </span>
123
+ </button>
124
+ <ul
125
+ v-if="clicked"
126
+ class="absolute text-textColor"
127
+ >
128
+ <li
129
+ class="dropdown w-56"
130
+ v-for="(item, key) in results"
131
+ :key="key"
132
+ >
133
+ <a class="rounded-t bg-backgroundColor hover:bg-bgMutedColor py-2 px-4 block whitespace-no-wrap" href="#">
134
+ <span class="pr-1 flex-1"
135
+ >{{ item.length === 1 ? `${key}:${item[0].tag}` : key }}
136
+ </span>
137
+ <span class="mr-auto">
138
+ </span>
139
+ </a>
140
+ <ul
141
+ v-if="item.length === 1"
142
+ class="dropdown-content absolute hidden text-textColor left-44 top-0"
143
+ >
144
+ <li
145
+ v-for="task in item[0].tasks"
146
+ :key="task._id"
147
+ class="bg-backgroundColor hover:bg-bgMutedColor py-2 px-4 block whitespace-no-wrap"
148
+ @click="select(task._id)"
149
+ >
150
+ {{ task.name }}
151
+ </li>
152
+ </ul>
153
+ <ul
154
+ v-else
155
+ class="dropdown-content absolute hidden text-textColor left-44 top-0"
156
+ >
157
+ <li
158
+ v-for="tag in item"
159
+ :key="tag.tag"
160
+ class="dropdown w-56"
161
+ >
162
+ <a class="bg-backgroundColor hover:bg-bgMutedColor py-2 px-4 block whitespace-no-wrap" href="#">
163
+ <span class="pr-1">{{ tag.tag }}</span>
164
+ <span class="mr-auto">
165
+ <svg-icon type="mdi" :path="mdiChevronRight" :size="30" class="pb-1" style="display:inline" />
166
+
167
+ </span>
168
+ </a>
169
+ <ul
170
+ class="dropdown-content absolute hidden text-textColor left-44 top-0"
171
+ >
172
+ <li
173
+ v-for="task in tag.tasks"
174
+ :key="task._id"
175
+ @click="select(task._id)"
176
+ class="bg-backgroundColor hover:bg-bgMutedColor py-2 px-4 block whitespace-no-wrap"
177
+ >
178
+ <a class="bg-backgroundColor hover:bg-bgMutedColor py-2 px-4 block whitespace-no-wrap" href="#">
179
+ {{ task.name }}
180
+ </a>
181
+ </li>
182
+ </ul>
183
+ </li>
184
+ </ul>
185
+ </li>
186
+ </ul>
187
+ </div>
188
+ <div v-else>
189
+ <button
190
+ type="button"
191
+ class="inline-block align-middle text-center select-none border font-normal whitespace-no-wrap py-2 px-4 rounded text-base leading-normal no-underline text-yellow-dark border-yellow bg-white hover:bg-yellow-light hover:text-yellow-darker"
192
+ data-toggle=""
193
+ data-placement="top"
194
+ title="Not Logged In"
195
+ >
196
+ Error
197
+ <svg-icon type="mdi" :path="mdiAlert" :size="30" class="pb-1" />
198
+ </button>
199
+ </div>
200
+ </on-click-outside>
201
+ </div>
202
+ </template>
203
+ <style scoped lang="scss">
204
+ .dropdown:hover > .dropdown-content {
205
+ display: block;
206
+ }
207
+
208
+ </style>