chore(lite): merge old repo to XO
This commit is contained in:
parent
44ff5d0e4d
commit
b8c9770d43
1
@xen-orchestra/lite/.gitignore
vendored
1
@xen-orchestra/lite/.gitignore
vendored
@ -27,3 +27,4 @@ coverage
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
.env
|
.env
|
||||||
|
.npmrc
|
||||||
|
2
@xen-orchestra/lite/.npmrc.dist
Normal file
2
@xen-orchestra/lite/.npmrc.dist
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@fortawesome:registry=https://npm.fontawesome.com/
|
||||||
|
//npm.fontawesome.com/:_authToken=INSERT_FONT_AWESOME_PRO_TOKEN_HERE
|
@ -89,6 +89,8 @@ const fontSize = ref("2rem");
|
|||||||
|
|
||||||
### Icons
|
### Icons
|
||||||
|
|
||||||
|
This project is using Font Awesome Pro 6.
|
||||||
|
|
||||||
Here is how to use an icon in your template.
|
Here is how to use an icon in your template.
|
||||||
|
|
||||||
Note: `FontAwesomeIcon` is a global component that does not need to be imported.
|
Note: `FontAwesomeIcon` is a global component that does not need to be imported.
|
||||||
@ -105,6 +107,17 @@ import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Font weight <=> Style name
|
||||||
|
|
||||||
|
Here is the equivalent between font weight and style name.
|
||||||
|
|
||||||
|
| Style name | Font weight |
|
||||||
|
|------------|-------------|
|
||||||
|
| Solid | 900 |
|
||||||
|
| Regular | 400 |
|
||||||
|
| Light | 300 |
|
||||||
|
| Thin | 100 |
|
||||||
|
|
||||||
### CSS
|
### CSS
|
||||||
|
|
||||||
Always use `rem` unit (`1rem` = `10px`)
|
Always use `rem` unit (`1rem` = `10px`)
|
||||||
|
@ -11,9 +11,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.1.1",
|
"@fortawesome/pro-light-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/pro-regular-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@fortawesome/pro-solid-svg-icons": "^6.1.2",
|
||||||
|
"@fortawesome/pro-thin-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.1",
|
"@fortawesome/vue-fontawesome": "^3.0.1",
|
||||||
"@novnc/novnc": "^1.3.0",
|
"@novnc/novnc": "^1.3.0",
|
||||||
"@vueuse/core": "^8.7.5",
|
"@vueuse/core": "^8.7.5",
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
font-family: Poppins, sans-serif;
|
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
@ -9,6 +9,7 @@ html {
|
|||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
font-family: Poppins, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
body {
|
:root {
|
||||||
--color-blue-scale-000: #000000;
|
--color-blue-scale-000: #000000;
|
||||||
--color-blue-scale-100: #1A1B38;
|
--color-blue-scale-100: #1A1B38;
|
||||||
--color-blue-scale-200: #595A6F;
|
--color-blue-scale-200: #595A6F;
|
||||||
@ -30,13 +30,13 @@ body {
|
|||||||
--color-orange-world-d40: #554F94;
|
--color-orange-world-d40: #554F94;
|
||||||
--color-orange-world-d60: #383563;
|
--color-orange-world-d60: #383563;
|
||||||
|
|
||||||
--color-red-vates-l60: #D1CEFB;
|
--color-red-vates-l60: #DDA5A7;
|
||||||
--color-red-vates-l40: #BBB5F9;
|
--color-red-vates-l40: #CE787C;
|
||||||
--color-red-vates-l20: #A39DF8;
|
--color-red-vates-l20: #BF4F51;
|
||||||
--color-red-vates-base: #8F84FF;
|
--color-red-vates-base: #BE1621;
|
||||||
--color-red-vates-d20: #716AC6;
|
--color-red-vates-d20: #8E2221;
|
||||||
--color-red-vates-d40: #554F94;
|
--color-red-vates-d40: #6A1919;
|
||||||
--color-red-vates-d60: #383563;
|
--color-red-vates-d60: #471010;
|
||||||
|
|
||||||
--color-grayscale-200: #585757;
|
--color-grayscale-200: #585757;
|
||||||
|
|
||||||
@ -51,13 +51,9 @@ body {
|
|||||||
--shadow-200: 0 0.1rem 0.3rem rgba(20, 20, 30, 0.1), 0 0.2rem 0.1rem rgba(20, 20, 30, 0.06), 0 0.1rem 0.1rem rgba(20, 20, 30, 0.08);
|
--shadow-200: 0 0.1rem 0.3rem rgba(20, 20, 30, 0.1), 0 0.2rem 0.1rem rgba(20, 20, 30, 0.06), 0 0.1rem 0.1rem rgba(20, 20, 30, 0.08);
|
||||||
--shadow-300: 0 0.3rem 0.5rem rgba(20, 20, 30, 0.1), 0 0.1rem 1.8rem rgba(20, 20, 30, 0.06), 0 0.6rem 1.0rem rgba(20, 20, 30, 0.08);
|
--shadow-300: 0 0.3rem 0.5rem rgba(20, 20, 30, 0.1), 0 0.1rem 1.8rem rgba(20, 20, 30, 0.06), 0 0.6rem 1.0rem rgba(20, 20, 30, 0.08);
|
||||||
--shadow-400: 0 1.1rem 1.5rem rgba(20, 20, 30, 0.1), 0 0.9rem 4.6rem rgba(20, 20, 30, 0.06), 0 2.4rem 3.8rem rgba(20, 20, 30, 0.04);
|
--shadow-400: 0 1.1rem 1.5rem rgba(20, 20, 30, 0.1), 0 0.9rem 4.6rem rgba(20, 20, 30, 0.06), 0 2.4rem 3.8rem rgba(20, 20, 30, 0.04);
|
||||||
|
|
||||||
--form-control-disabled: var(--color-blue-scale-200);
|
|
||||||
--form-background: var(--color-blue-scale-500);
|
|
||||||
--form-control-color: var(--color-blue-scale-100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark {
|
:root.dark {
|
||||||
--color-blue-scale-000: #FFFFFF;
|
--color-blue-scale-000: #FFFFFF;
|
||||||
--color-blue-scale-100: #E5E5E7;
|
--color-blue-scale-100: #E5E5E7;
|
||||||
--color-blue-scale-200: #9899A5;
|
--color-blue-scale-200: #9899A5;
|
||||||
|
48
@xen-orchestra/lite/src/components/AccountButton.vue
Normal file
48
@xen-orchestra/lite/src/components/AccountButton.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<button class="account-button">
|
||||||
|
<FontAwesomeIcon class="user-icon" :icon="faCircleUser" />
|
||||||
|
<FontAwesomeIcon class="dropdown-icon" :icon="faAngleDown" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { faAngleDown } from "@fortawesome/pro-light-svg-icons";
|
||||||
|
import { faCircleUser } from "@fortawesome/pro-solid-svg-icons";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.account-button {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-blue-scale-100);
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--color-blue-scale-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover,
|
||||||
|
&:active {
|
||||||
|
background-color: var(--background-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: var(--color-extra-blue-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-icon {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -19,7 +19,7 @@ import { useXenApiStore } from "@/stores/xen-api.store";
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
document.body.classList.toggle("dark");
|
document.documentElement.classList.toggle("dark");
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
|
@ -36,7 +36,7 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.form-container {
|
.form-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -3,118 +3,74 @@
|
|||||||
<UiFilter
|
<UiFilter
|
||||||
v-for="filter in activeFilters"
|
v-for="filter in activeFilters"
|
||||||
:key="filter"
|
:key="filter"
|
||||||
|
@edit="editFilter(filter)"
|
||||||
@remove="emit('removeFilter', filter)"
|
@remove="emit('removeFilter', filter)"
|
||||||
>
|
>
|
||||||
{{ filter }}
|
{{ filter }}
|
||||||
</UiFilter>
|
</UiFilter>
|
||||||
|
|
||||||
<UiButton
|
<UiButton :icon="faPlus" class="add-filter" color="secondary" @click="open">
|
||||||
:icon-left="faPlus"
|
|
||||||
class="add-filter"
|
|
||||||
color="action"
|
|
||||||
@click="open"
|
|
||||||
>
|
|
||||||
Add filter
|
Add filter
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</UiFilterGroup>
|
</UiFilterGroup>
|
||||||
|
|
||||||
<UiModal v-if="isOpen">
|
<UiModal v-if="isOpen">
|
||||||
Add a filter
|
|
||||||
<form @submit.prevent="handleSubmit">
|
<form @submit.prevent="handleSubmit">
|
||||||
<div class="form-row" v-for="(item, index) in items" :key="index">
|
<div class="rows">
|
||||||
<span v-if="items.length > 1" style="width: 2rem">{{
|
<CollectionFilterRow
|
||||||
index > 0 ? "OR" : ""
|
v-for="(newFilter, index) in newFilters"
|
||||||
}}</span>
|
:key="newFilter.id"
|
||||||
<FormWidget
|
v-model="newFilters[index]"
|
||||||
v-if="!isAdvancedModeEnabled"
|
:available-filters="availableFilters"
|
||||||
:before="filterIconForIndex(index)"
|
@remove="removeNewFilter"
|
||||||
>
|
/>
|
||||||
<select v-model="items[index].selectedFilter">
|
</div>
|
||||||
<option v-if="!selectedFilter"></option>
|
|
||||||
<option
|
|
||||||
v-for="availableFilter in availableFilters"
|
|
||||||
:key="availableFilter.property"
|
|
||||||
:value="availableFilter"
|
|
||||||
>
|
|
||||||
{{ availableFilter.label ?? availableFilter.property }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</FormWidget>
|
|
||||||
|
|
||||||
<FormWidget
|
<div
|
||||||
v-if="!isAdvancedModeEnabled && items[index].selectedFilter"
|
v-if="newFilters.some((filter) => filter.isAdvanced)"
|
||||||
>
|
class="available-properties"
|
||||||
<select v-model="items[index].filterPattern">
|
>
|
||||||
<option
|
Available properties for advanced filter:
|
||||||
v-for="comparison in getComparisonsForIndex(index)"
|
<div class="properties">
|
||||||
:key="comparison.pattern"
|
<UiBadge
|
||||||
:value="comparison.pattern"
|
v-for="(filter, property) in availableFilters"
|
||||||
>
|
:key="property"
|
||||||
{{ comparison.label }}
|
:icon="getFilterIcon(filter)"
|
||||||
</option>
|
>
|
||||||
</select>
|
{{ property }}
|
||||||
</FormWidget>
|
</UiBadge>
|
||||||
|
</div>
|
||||||
<FormWidget
|
|
||||||
v-if="isFilterTakingValue(index)"
|
|
||||||
:after="getComparisonForIndex(index)?.after"
|
|
||||||
:before="getComparisonForIndex(index)?.before"
|
|
||||||
>
|
|
||||||
<input v-model="items[index].filterValue" />
|
|
||||||
</FormWidget>
|
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UiButtonGroup>
|
<UiButtonGroup>
|
||||||
<UiButton color="action" @click="addItem">+OR</UiButton>
|
<UiButton color="secondary" @click="addNewFilter">+OR</UiButton>
|
||||||
|
<UiButton :disabled="!isFilterValid" type="submit">
|
||||||
<UiButton
|
{{ editedFilter ? "Update" : "Add" }}
|
||||||
type="submit"
|
|
||||||
:disabled="!generatedFilter || !isGeneratedFilterValid"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</UiButton>
|
|
||||||
|
|
||||||
<UiButton color="action" type="button" @click="handleCancel">
|
|
||||||
Cancel
|
|
||||||
</UiButton>
|
</UiButton>
|
||||||
|
<UiButton color="secondary" @click="handleCancel">Cancel</UiButton>
|
||||||
</UiButtonGroup>
|
</UiButtonGroup>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div style="margin-top: 1rem">
|
|
||||||
<UiButton
|
|
||||||
v-if="!isAdvancedModeEnabled"
|
|
||||||
@click="activateAdvancedMode"
|
|
||||||
color="action"
|
|
||||||
>
|
|
||||||
Switch to advanced mode...
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</UiModal>
|
</UiModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as CM from "complex-matcher";
|
import { Or, parse } from "complex-matcher";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import type {
|
import type { Filters, NewFilter } from "@/types/filter";
|
||||||
AvailableFilter,
|
import { faPlus } from "@fortawesome/pro-solid-svg-icons";
|
||||||
FilterComparisons,
|
import CollectionFilterRow from "@/components/CollectionFilterRow.vue";
|
||||||
FilterType,
|
import UiBadge from "@/components/ui/UiBadge.vue";
|
||||||
} from "@/types/filter";
|
|
||||||
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
|
|
||||||
import { faFont, faHashtag, faPlus } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import FormWidget from "@/components/FormWidget.vue";
|
|
||||||
import UiButton from "@/components/ui/UiButton.vue";
|
import UiButton from "@/components/ui/UiButton.vue";
|
||||||
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
||||||
import UiFilter from "@/components/ui/UiFilter.vue";
|
import UiFilter from "@/components/ui/UiFilter.vue";
|
||||||
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
|
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
|
||||||
import UiModal from "@/components/ui/UiModal.vue";
|
import UiModal from "@/components/ui/UiModal.vue";
|
||||||
import useModal from "@/composables/modal.composable";
|
import useModal from "@/composables/modal.composable";
|
||||||
import { escapeRegExp } from "@/libs/utils";
|
import { getFilterIcon } from "@/libs/utils";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
availableFilters: AvailableFilter[];
|
|
||||||
activeFilters: string[];
|
activeFilters: string[];
|
||||||
|
availableFilters: Filters;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -122,291 +78,113 @@ const emit = defineEmits<{
|
|||||||
(event: "removeFilter", filter: string): void;
|
(event: "removeFilter", filter: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { open, close, isOpen } = useModal();
|
const { isOpen, open, close } = useModal();
|
||||||
|
const newFilters = ref<NewFilter[]>([]);
|
||||||
|
let newFilterId = 0;
|
||||||
|
|
||||||
const getComparisonsForIndex = (index: number) => {
|
const addNewFilter = () =>
|
||||||
const selectedFilter = items.value[index]?.selectedFilter;
|
newFilters.value.push({
|
||||||
|
id: newFilterId++,
|
||||||
if (!selectedFilter) {
|
content: "",
|
||||||
return [];
|
isAdvanced: false,
|
||||||
}
|
builder: { property: "", comparison: "", value: "", negate: false },
|
||||||
|
|
||||||
switch (selectedFilter.type) {
|
|
||||||
case "string":
|
|
||||||
return [
|
|
||||||
{ label: "contains", pattern: "%p:%v", default: true },
|
|
||||||
{ label: "equals", pattern: "%p:/^%v$/i", escape: true },
|
|
||||||
{ label: "starts with", pattern: "%p:/^%v/i", escape: true },
|
|
||||||
{ label: "ends with", pattern: "%p:/%v$/i", escape: true },
|
|
||||||
{
|
|
||||||
label: "matches regex",
|
|
||||||
pattern: "%p:/%v/i",
|
|
||||||
before: "/",
|
|
||||||
after: "/i",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
case "number":
|
|
||||||
return [
|
|
||||||
{ label: "<", pattern: "%p:<%v" },
|
|
||||||
{ label: "<=", pattern: "%p:<=%v" },
|
|
||||||
{ label: "=", pattern: "%p:%v", default: true },
|
|
||||||
{ label: ">=", pattern: "%p:>=%v" },
|
|
||||||
{ label: ">", pattern: "%p:>%v" },
|
|
||||||
];
|
|
||||||
case "boolean":
|
|
||||||
return [
|
|
||||||
{ label: "is true", pattern: "%p?", default: true },
|
|
||||||
{ label: "is false", pattern: "!%p?" },
|
|
||||||
];
|
|
||||||
case "enum":
|
|
||||||
return selectedFilter.choices.map((choice, index) => ({
|
|
||||||
label: choice,
|
|
||||||
pattern: `%p:/^${escapeRegExp(choice)}$/`,
|
|
||||||
default: index === 0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const comparisons = computed<FilterComparisons>(() => ({
|
|
||||||
string: [
|
|
||||||
{ label: "contains", pattern: "%p:%v", default: true },
|
|
||||||
{ label: "equals", pattern: "%p:/^%v$/i", escape: true },
|
|
||||||
{ label: "starts with", pattern: "%p:/^%v/i", escape: true },
|
|
||||||
{ label: "ends with", pattern: "%p:/%v$/i", escape: true },
|
|
||||||
{
|
|
||||||
label: "matches regex",
|
|
||||||
pattern: "%p:/%v/i",
|
|
||||||
before: "/",
|
|
||||||
after: "/i",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
number: [
|
|
||||||
{ label: "<", pattern: "%p:<%v" },
|
|
||||||
{ label: "<=", pattern: "%p:<=%v" },
|
|
||||||
{ label: "=", pattern: "%p:%v", default: true },
|
|
||||||
{ label: ">=", pattern: "%p:>=%v" },
|
|
||||||
{ label: ">", pattern: "%p:>%v" },
|
|
||||||
],
|
|
||||||
boolean: [
|
|
||||||
{ label: "is true", pattern: "%p?", default: true },
|
|
||||||
{ label: "is false", pattern: "!%p?" },
|
|
||||||
],
|
|
||||||
enum:
|
|
||||||
selectedFilter.value?.type !== "enum"
|
|
||||||
? []
|
|
||||||
: selectedFilter.value.choices.map((choice, index) => ({
|
|
||||||
label: choice,
|
|
||||||
pattern: `%p:/^${escapeRegExp(choice)}$/`,
|
|
||||||
default: index === 0,
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const isAdvancedModeEnabled = ref(false);
|
|
||||||
|
|
||||||
const selectedFilter = ref<AvailableFilter>();
|
|
||||||
|
|
||||||
const filterPattern = ref<string>();
|
|
||||||
|
|
||||||
const currentComparison = computed(() => {
|
|
||||||
if (!selectedFilter.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return comparisons.value[selectedFilter.value.type].find(
|
|
||||||
(comparison) => comparison.pattern === filterPattern.value
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const getComparisonForIndex = (index: number) => {
|
|
||||||
const type = items.value[index]?.selectedFilter?.type;
|
|
||||||
const pattern = items.value[index]?.filterPattern;
|
|
||||||
|
|
||||||
if (!type || !pattern) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return comparisons.value[type].find(
|
|
||||||
(comparison) => comparison.pattern === pattern
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDefaultPatternForFilterType = (filterType?: FilterType) => {
|
|
||||||
if (!filterType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return comparisons.value[filterType].find(({ default: def }) => def)?.pattern;
|
|
||||||
};
|
|
||||||
|
|
||||||
const items = ref<
|
|
||||||
{
|
|
||||||
selectedFilter: AvailableFilter | undefined;
|
|
||||||
filterPattern: string;
|
|
||||||
filterValue: string;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const filterIconForIndex = (index: number) => {
|
|
||||||
const selectedFilter = items.value[index]?.selectedFilter;
|
|
||||||
|
|
||||||
if (!selectedFilter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedFilter.icon) {
|
|
||||||
return selectedFilter.icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (selectedFilter.type) {
|
|
||||||
case "string":
|
|
||||||
return faFont;
|
|
||||||
case "number":
|
|
||||||
return faHashtag;
|
|
||||||
case "boolean":
|
|
||||||
return faSquareCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// watch(selectedFilter, (newSelectedFilter) => {
|
|
||||||
// if (newSelectedFilter && !items.value.length) {
|
|
||||||
// items.value.push({
|
|
||||||
// filterPattern:
|
|
||||||
// getDefaultPatternForFilterType(newSelectedFilter.type) || "",
|
|
||||||
// filterValue: "",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// filterPattern.value = getDefaultPatternForFilterType(newSelectedFilter?.type);
|
|
||||||
// });
|
|
||||||
|
|
||||||
const addItem = () => {
|
|
||||||
items.value.push({
|
|
||||||
selectedFilter: undefined,
|
|
||||||
filterPattern: "",
|
|
||||||
filterValue: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const removeNewFilter = (id: number) => {
|
||||||
|
const index = newFilters.value.findIndex((newFilter) => newFilter.id === id);
|
||||||
|
if (index >= 0) {
|
||||||
|
newFilters.value.splice(index, 1);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
addItem();
|
addNewFilter();
|
||||||
|
|
||||||
const isFilterTakingValue = (index: number) => {
|
|
||||||
const comparison = getComparisonForIndex(index);
|
|
||||||
|
|
||||||
return comparison?.pattern.includes("%v") || isAdvancedModeEnabled.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
// const isFilterTakingValue = computed(() => {
|
|
||||||
// return (
|
|
||||||
// currentComparison.value?.pattern.includes("%v") ||
|
|
||||||
// isAdvancedModeEnabled.value
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
|
|
||||||
const filterValue = ref<string>();
|
|
||||||
|
|
||||||
const generatedFilter = computed(() => {
|
const generatedFilter = computed(() => {
|
||||||
if (items.value.length === 0) {
|
const filters = newFilters.value.filter(
|
||||||
return;
|
(newFilter) => newFilter.content !== ""
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filters.length === 0) {
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = items.value
|
if (filters.length === 1) {
|
||||||
.map((item) => {
|
return filters[0].content;
|
||||||
return item.filterPattern
|
|
||||||
.replace("%p", item.selectedFilter?.property)
|
|
||||||
.replace(
|
|
||||||
"%v",
|
|
||||||
currentComparison.value?.escape
|
|
||||||
? escapeRegExp(item.filterValue)
|
|
||||||
: item.filterValue
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
if (items.value.length === 1) {
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return `|(${result})`;
|
return `|(${filters.map((filter) => filter.content).join(" ")})`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// const generatedFilter2 = computed(() => {
|
const isFilterValid = computed(() => generatedFilter.value !== "");
|
||||||
// if (isAdvancedModeEnabled.value) {
|
|
||||||
// return filterValue.value;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (!selectedFilter.value || !filterPattern.value) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (isFilterTakingValue.value) {
|
|
||||||
// if (!filterValue.value) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return filterPattern.value
|
|
||||||
// .replace("%p", selectedFilter.value.property)
|
|
||||||
// .replace(
|
|
||||||
// "%v",
|
|
||||||
// currentComparison.value?.escape
|
|
||||||
// ? escapeRegExp(filterValue.value)
|
|
||||||
// : filterValue.value
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return filterPattern.value.replace("%p", selectedFilter.value.property);
|
|
||||||
// });
|
|
||||||
|
|
||||||
const isGeneratedFilterValid = computed(() => {
|
const editedFilter = ref();
|
||||||
if (!generatedFilter.value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const editFilter = (filter: string) => {
|
||||||
if (generatedFilter.value) {
|
const parsedFilter = parse(filter);
|
||||||
CM.parse(generatedFilter.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
const nodes =
|
||||||
} catch (e) {
|
parsedFilter instanceof Or ? parsedFilter.children : [parsedFilter];
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const activateAdvancedMode = () => {
|
newFilters.value = nodes.map((node) => ({
|
||||||
filterValue.value = generatedFilter.value;
|
id: newFilterId++,
|
||||||
isAdvancedModeEnabled.value = true;
|
content: node.toString(),
|
||||||
|
isAdvanced: true,
|
||||||
|
builder: { property: "", comparison: "", value: "", negate: false },
|
||||||
|
}));
|
||||||
|
editedFilter.value = filter;
|
||||||
|
open();
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetAndClose = () => {
|
const reset = () => {
|
||||||
items.value = [];
|
editedFilter.value = "";
|
||||||
addItem();
|
newFilters.value = [];
|
||||||
selectedFilter.value = undefined;
|
addNewFilter();
|
||||||
filterValue.value = undefined;
|
|
||||||
isAdvancedModeEnabled.value = false;
|
|
||||||
close();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (generatedFilter.value) {
|
if (editedFilter.value) {
|
||||||
emit("addFilter", generatedFilter.value);
|
emit("removeFilter", editedFilter.value);
|
||||||
resetAndClose();
|
|
||||||
}
|
}
|
||||||
|
emit("addFilter", generatedFilter.value);
|
||||||
|
reset();
|
||||||
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
resetAndClose();
|
reset();
|
||||||
|
close();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.add-filter {
|
.add-filter {
|
||||||
height: 3.4rem;
|
height: 3.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row {
|
.properties {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-properties {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.properties {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
margin-top: 0.6rem;
|
||||||
align-items: center;
|
gap: 0.5rem;
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
260
@xen-orchestra/lite/src/components/CollectionFilterRow.vue
Normal file
260
@xen-orchestra/lite/src/components/CollectionFilterRow.vue
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<template>
|
||||||
|
<div class="collection-filter-row">
|
||||||
|
<span class="or">OR</span>
|
||||||
|
<FormWidget v-if="newFilter.isAdvanced" style="flex: 1">
|
||||||
|
<input v-model="newFilter.content" />
|
||||||
|
</FormWidget>
|
||||||
|
<template v-else>
|
||||||
|
<FormWidget :before="currentFilterIcon">
|
||||||
|
<select v-model="newFilter.builder.property">
|
||||||
|
<option v-if="!newFilter.builder.property" value="">
|
||||||
|
- Property -
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="(filter, property) in availableFilters"
|
||||||
|
:key="property"
|
||||||
|
:value="property"
|
||||||
|
>
|
||||||
|
{{ filter.label ?? property }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</FormWidget>
|
||||||
|
<template v-if="hasComparisonSelect">
|
||||||
|
<FormWidget v-if="currentFilter?.type === 'string'">
|
||||||
|
<select v-model="newFilter.builder.negate">
|
||||||
|
<option :value="false">does</option>
|
||||||
|
<option :value="true">does not</option>
|
||||||
|
</select>
|
||||||
|
</FormWidget>
|
||||||
|
<FormWidget v-if="hasComparisonSelect">
|
||||||
|
<select v-model="newFilter.builder.comparison">
|
||||||
|
<option
|
||||||
|
v-for="(label, type) in comparisons"
|
||||||
|
:key="type"
|
||||||
|
:value="type"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</FormWidget>
|
||||||
|
</template>
|
||||||
|
<FormWidget
|
||||||
|
v-if="hasValueInput"
|
||||||
|
:after="valueInputAfter"
|
||||||
|
:before="valueInputBefore"
|
||||||
|
>
|
||||||
|
<input v-model="newFilter.builder.value" />
|
||||||
|
</FormWidget>
|
||||||
|
<template v-else-if="currentFilter?.type === 'enum'">
|
||||||
|
<FormWidget>
|
||||||
|
<select v-model="newFilter.builder.negate">
|
||||||
|
<option :value="false">is</option>
|
||||||
|
<option :value="true">is not</option>
|
||||||
|
</select>
|
||||||
|
</FormWidget>
|
||||||
|
<FormWidget>
|
||||||
|
<select v-model="newFilter.builder.value">
|
||||||
|
<option v-if="!newFilter.builder.value" value="" />
|
||||||
|
<option v-for="choice in enumChoices" :key="choice" :value="choice">
|
||||||
|
{{ choice }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</FormWidget>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<UiButton
|
||||||
|
v-if="!newFilter.isAdvanced"
|
||||||
|
color="secondary"
|
||||||
|
@click="enableAdvancedMode"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faPencil" />
|
||||||
|
</UiButton>
|
||||||
|
<UiButton
|
||||||
|
class="remove"
|
||||||
|
color="secondary"
|
||||||
|
@click="emit('remove', newFilter.id)"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faRemove" class="remove-icon" />
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, watch } from "vue";
|
||||||
|
import type {
|
||||||
|
Filter,
|
||||||
|
FilterComparisonType,
|
||||||
|
FilterComparisons,
|
||||||
|
FilterType,
|
||||||
|
Filters,
|
||||||
|
NewFilter,
|
||||||
|
} from "@/types/filter";
|
||||||
|
import { faPencil, faRemove } from "@fortawesome/pro-solid-svg-icons";
|
||||||
|
import { useVModel } from "@vueuse/core";
|
||||||
|
import FormWidget from "@/components/FormWidget.vue";
|
||||||
|
import UiButton from "@/components/ui/UiButton.vue";
|
||||||
|
import { buildComplexMatcherNode } from "@/libs/complex-matcher.utils";
|
||||||
|
import { getFilterIcon } from "@/libs/utils";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
availableFilters: Filters;
|
||||||
|
modelValue: NewFilter;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:modelValue", value: NewFilter): void;
|
||||||
|
(event: "remove", filterId: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const newFilter = useVModel(props, "modelValue", emit);
|
||||||
|
|
||||||
|
const getDefaultComparisonType = () => {
|
||||||
|
const defaultTypes: { [key in FilterType]: FilterComparisonType } = {
|
||||||
|
string: "stringContains",
|
||||||
|
boolean: "booleanTrue",
|
||||||
|
number: "numberEquals",
|
||||||
|
enum: "stringEquals",
|
||||||
|
};
|
||||||
|
|
||||||
|
return defaultTypes[
|
||||||
|
props.availableFilters[newFilter.value.builder.property].type
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => newFilter.value.builder.property,
|
||||||
|
() => {
|
||||||
|
newFilter.value.builder.comparison = getDefaultComparisonType();
|
||||||
|
newFilter.value.builder.value = "";
|
||||||
|
newFilter.value.builder.negate = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentFilter = computed<Filter>(
|
||||||
|
() => props.availableFilters[newFilter.value.builder.property]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentFilterIcon = computed(() => getFilterIcon(currentFilter.value));
|
||||||
|
|
||||||
|
const hasValueInput = computed(() =>
|
||||||
|
["string", "number"].includes(currentFilter.value?.type)
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasComparisonSelect = computed(
|
||||||
|
() => newFilter.value.builder.property && currentFilter.value?.type !== "enum"
|
||||||
|
);
|
||||||
|
|
||||||
|
const enumChoices = computed(() => {
|
||||||
|
if (!newFilter.value.builder.property) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableFilter =
|
||||||
|
props.availableFilters[newFilter.value.builder.property];
|
||||||
|
|
||||||
|
if (availableFilter.type !== "enum") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableFilter.choices;
|
||||||
|
});
|
||||||
|
|
||||||
|
const generatedFilter = computed(() => {
|
||||||
|
if (newFilter.value.isAdvanced) {
|
||||||
|
return newFilter.value.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newFilter.value.builder.comparison) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const node = buildComplexMatcherNode(
|
||||||
|
newFilter.value.builder.comparison,
|
||||||
|
newFilter.value.builder.property,
|
||||||
|
newFilter.value.builder.value,
|
||||||
|
newFilter.value.builder.negate
|
||||||
|
);
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
return node.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const enableAdvancedMode = () => {
|
||||||
|
newFilter.value.content = generatedFilter.value;
|
||||||
|
newFilter.value.isAdvanced = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(generatedFilter, (value) => {
|
||||||
|
newFilter.value.content = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const comparisons = computed<FilterComparisons>(() => {
|
||||||
|
const comparisonsByType = {
|
||||||
|
string: {
|
||||||
|
stringContains: "contain",
|
||||||
|
stringEquals: "equal",
|
||||||
|
stringStartsWith: "start with",
|
||||||
|
stringEndsWith: "end with",
|
||||||
|
stringMatchesRegex: "match regex",
|
||||||
|
},
|
||||||
|
boolean: {
|
||||||
|
booleanTrue: "is true",
|
||||||
|
booleanFalse: "is false",
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
numberLessThan: "<",
|
||||||
|
numberLessThanOrEquals: "<=",
|
||||||
|
numberEquals: "=",
|
||||||
|
numberGreaterThanOrEquals: ">=",
|
||||||
|
numberGreaterThan: ">",
|
||||||
|
},
|
||||||
|
enum: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return comparisonsByType[currentFilter.value.type];
|
||||||
|
});
|
||||||
|
|
||||||
|
const valueInputBefore = computed(() => {
|
||||||
|
return newFilter.value.builder.comparison === "stringMatchesRegex"
|
||||||
|
? "/"
|
||||||
|
: undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const valueInputAfter = computed(() => {
|
||||||
|
return newFilter.value.builder.comparison === "stringMatchesRegex"
|
||||||
|
? "/i"
|
||||||
|
: undefined;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="postcss" scoped>
|
||||||
|
.collection-filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
border-bottom: 1px solid var(--background-color-secondary);
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
&:only-child {
|
||||||
|
.or,
|
||||||
|
.remove {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child .or {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-icon {
|
||||||
|
color: var(--color-red-vates-base);
|
||||||
|
}
|
||||||
|
</style>
|
104
@xen-orchestra/lite/src/components/CollectionSorter.vue
Normal file
104
@xen-orchestra/lite/src/components/CollectionSorter.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<UiFilterGroup class="collection-sorter">
|
||||||
|
<UiFilter
|
||||||
|
v-for="[property, isAscending] in activeSorts"
|
||||||
|
:key="property"
|
||||||
|
@edit="emit('toggleSortDirection', property)"
|
||||||
|
@remove="emit('removeSort', property)"
|
||||||
|
>
|
||||||
|
<span style="display: inline-flex; align-items: center; gap: 0.7rem">
|
||||||
|
<FontAwesomeIcon :icon="isAscending ? faCaretUp : faCaretDown" />
|
||||||
|
{{ property }}
|
||||||
|
</span>
|
||||||
|
</UiFilter>
|
||||||
|
|
||||||
|
<UiButton :icon="faPlus" class="add-sort" color="secondary" @click="open">
|
||||||
|
Add sort
|
||||||
|
</UiButton>
|
||||||
|
</UiFilterGroup>
|
||||||
|
|
||||||
|
<UiModal v-if="isOpen">
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<div style="display: flex; gap: 1rem">
|
||||||
|
<FormWidget label="Sort by">
|
||||||
|
<select v-model="newSortProperty">
|
||||||
|
<option v-if="!newSortProperty"></option>
|
||||||
|
<option
|
||||||
|
v-for="(sort, property) in availableSorts"
|
||||||
|
:key="property"
|
||||||
|
:value="property"
|
||||||
|
>
|
||||||
|
{{ sort.label ?? property }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</FormWidget>
|
||||||
|
<FormWidget>
|
||||||
|
<select v-model="newSortIsAscending">
|
||||||
|
<option :value="true">ascending</option>
|
||||||
|
<option :value="false">descending</option>
|
||||||
|
</select>
|
||||||
|
</FormWidget>
|
||||||
|
</div>
|
||||||
|
<UiButtonGroup>
|
||||||
|
<UiButton type="submit"> Add</UiButton>
|
||||||
|
<UiButton color="secondary" @click="handleCancel">Cancel</UiButton>
|
||||||
|
</UiButtonGroup>
|
||||||
|
</form>
|
||||||
|
</UiModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import type { ActiveSorts, Sorts } from "@/types/sort";
|
||||||
|
import {
|
||||||
|
faCaretDown,
|
||||||
|
faCaretUp,
|
||||||
|
faPlus,
|
||||||
|
} from "@fortawesome/pro-solid-svg-icons";
|
||||||
|
import FormWidget from "@/components/FormWidget.vue";
|
||||||
|
import UiButton from "@/components/ui/UiButton.vue";
|
||||||
|
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
||||||
|
import UiFilter from "@/components/ui/UiFilter.vue";
|
||||||
|
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
|
||||||
|
import UiModal from "@/components/ui/UiModal.vue";
|
||||||
|
import useModal from "@/composables/modal.composable";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
availableSorts: Sorts;
|
||||||
|
activeSorts: ActiveSorts;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "toggleSortDirection", property: string): void;
|
||||||
|
(event: "addSort", property: string, isAscending: boolean): void;
|
||||||
|
(event: "removeSort", property: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { isOpen, open, close } = useModal();
|
||||||
|
|
||||||
|
const newSortProperty = ref();
|
||||||
|
const newSortIsAscending = ref<boolean>(true);
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
// editedFilter.value = "";
|
||||||
|
// newFilters.value = [];
|
||||||
|
// addNewFilter();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
emit("addSort", newSortProperty.value, newSortIsAscending.value);
|
||||||
|
reset();
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
reset();
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="postcss" scoped>
|
||||||
|
.add-sort {
|
||||||
|
height: 3.4rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,11 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<CollectionFilter
|
<div class="filter-and-sort">
|
||||||
v-if="availableFilters"
|
<CollectionFilter
|
||||||
:active-filters="filters"
|
v-if="availableFilters"
|
||||||
:available-filters="availableFilters"
|
:active-filters="filters"
|
||||||
@add-filter="addFilter"
|
:available-filters="availableFilters"
|
||||||
@remove-filter="removeFilter"
|
@add-filter="addFilter"
|
||||||
/>
|
@remove-filter="removeFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CollectionSorter
|
||||||
|
:active-sorts="sorts"
|
||||||
|
:available-sorts="availableSorts"
|
||||||
|
@add-sort="addSort"
|
||||||
|
@remove-sort="removeSort"
|
||||||
|
@toggle-sort-direction="toggleSortDirection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<UiTable>
|
<UiTable>
|
||||||
<template #header>
|
<template #header>
|
||||||
@ -15,9 +25,13 @@
|
|||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<tr v-for="item in filteredCollection" :key="item.$ref">
|
<tr v-for="item in filteredAndSortedCollection" :key="item[idProperty]">
|
||||||
<td v-if="isSelectable">
|
<td v-if="isSelectable">
|
||||||
<input v-model="selected" :value="item.$ref" type="checkbox" />
|
<input
|
||||||
|
v-model="selected"
|
||||||
|
:value="item[props.idProperty]"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<slot :item="item" name="row" />
|
<slot :item="item" name="row" />
|
||||||
</tr>
|
</tr>
|
||||||
@ -26,18 +40,23 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, toRef, watch } from "vue";
|
import { computed, toRef, watch } from "vue";
|
||||||
import type { AvailableFilter } from "@/types/filter";
|
import type { Filters } from "@/types/filter";
|
||||||
|
import type { Sorts } from "@/types/sort";
|
||||||
import CollectionFilter from "@/components/CollectionFilter.vue";
|
import CollectionFilter from "@/components/CollectionFilter.vue";
|
||||||
|
import CollectionSorter from "@/components/CollectionSorter.vue";
|
||||||
import UiTable from "@/components/ui/UiTable.vue";
|
import UiTable from "@/components/ui/UiTable.vue";
|
||||||
import useCollectionFilter from "@/composables/collection-filter.composable";
|
import useCollectionFilter from "@/composables/collection-filter.composable";
|
||||||
|
import useCollectionSorter from "@/composables/collection-sorter.composable";
|
||||||
import useFilteredCollection from "@/composables/filtered-collection.composable";
|
import useFilteredCollection from "@/composables/filtered-collection.composable";
|
||||||
import useMultiSelect from "@/composables/multi-select.composable";
|
import useMultiSelect from "@/composables/multi-select.composable";
|
||||||
import type { XenApiRecord } from "@/libs/xen-api";
|
import useSortedCollection from "@/composables/sorted-collection.composable";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue?: string[];
|
modelValue?: string[];
|
||||||
availableFilters?: AvailableFilter[];
|
availableFilters?: Filters;
|
||||||
collection: XenApiRecord[];
|
availableSorts?: Sorts;
|
||||||
|
collection: Record<string, any>[];
|
||||||
|
idProperty: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -47,16 +66,25 @@ const emit = defineEmits<{
|
|||||||
const isSelectable = computed(() => props.modelValue !== undefined);
|
const isSelectable = computed(() => props.modelValue !== undefined);
|
||||||
|
|
||||||
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
|
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
|
||||||
|
const { sorts, addSort, removeSort, toggleSortDirection, compareFn } =
|
||||||
|
useCollectionSorter();
|
||||||
|
|
||||||
const filteredCollection = useFilteredCollection(
|
const filteredCollection = useFilteredCollection(
|
||||||
toRef(props, "collection"),
|
toRef(props, "collection"),
|
||||||
predicate
|
predicate
|
||||||
);
|
);
|
||||||
|
|
||||||
const usableRefs = computed(() => props.collection.map((item) => item.$ref));
|
const filteredAndSortedCollection = useSortedCollection(
|
||||||
|
filteredCollection,
|
||||||
|
compareFn
|
||||||
|
);
|
||||||
|
|
||||||
|
const usableRefs = computed(() =>
|
||||||
|
props.collection.map((item) => item[props.idProperty])
|
||||||
|
);
|
||||||
|
|
||||||
const selectableRefs = computed(() =>
|
const selectableRefs = computed(() =>
|
||||||
filteredCollection.value.map((item) => item.$ref)
|
filteredAndSortedCollection.value.map((item) => item[props.idProperty])
|
||||||
);
|
);
|
||||||
|
|
||||||
const { selected, areAllSelected } = useMultiSelect(usableRefs, selectableRefs);
|
const { selected, areAllSelected } = useMultiSelect(usableRefs, selectableRefs);
|
||||||
@ -66,4 +94,8 @@ watch(selected, (selected) => emit("update:modelValue", selected), {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style lang="postcss" scoped>
|
||||||
|
.filter-and-sort {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
37
@xen-orchestra/lite/src/components/ColumnHeader.vue
Normal file
37
@xen-orchestra/lite/src/components/ColumnHeader.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<th>
|
||||||
|
<div class="content">
|
||||||
|
<span class="label">
|
||||||
|
<FontAwesomeIcon v-if="icon" :icon="icon" />
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
icon?: IconDefinition;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="postcss" scoped>
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<label :class="{ inline }" class="form-widget">
|
<label class="form-widget">
|
||||||
<span v-if="label || $slots.label" class="label">
|
<span v-if="label || $slots.label" class="label">
|
||||||
<slot name="label">
|
<slot name="label">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@ -29,8 +29,8 @@
|
|||||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
before?: IconDefinition | string;
|
before?: IconDefinition | string | object; // "object" added as workaround
|
||||||
after?: IconDefinition | string;
|
after?: IconDefinition | string | object; // See https://github.com/vuejs/core/issues/4294
|
||||||
label?: string;
|
label?: string;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
}>();
|
}>();
|
||||||
@ -39,9 +39,9 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
|
|||||||
typeof maybeIcon === "object";
|
typeof maybeIcon === "object";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.form-widget {
|
.form-widget {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
@ -50,12 +50,13 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
|
|||||||
|
|
||||||
.widget {
|
.widget {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
flex: 1;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 0.7rem;
|
padding: 0 0.7rem;
|
||||||
border: 1px solid var(--color-blue-scale-400);
|
border: 1px solid var(--color-blue-scale-400);
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
background-color: var(--form-background);
|
background-color: var(--color-blue-scale-500);
|
||||||
box-shadow: var(--shadow-100);
|
box-shadow: var(--shadow-100);
|
||||||
gap: 0.1rem;
|
gap: 0.1rem;
|
||||||
|
|
||||||
@ -64,29 +65,41 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.form-widget:hover .widget {
|
.form-widget:hover .widget {
|
||||||
border-color: var(--color-extra-blue-l60);
|
border-color: var(--color-extra-blue-l60);
|
||||||
}
|
}
|
||||||
|
|
||||||
.element {
|
.element {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.before,
|
.before,
|
||||||
.after {
|
.after {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 0 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:slotted(input),
|
:slotted(input),
|
||||||
:slotted(select),
|
:slotted(select),
|
||||||
:slotted(textarea) {
|
:slotted(textarea) {
|
||||||
font-family: Poppins, sans-serif;
|
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
color: var(--color-blue-scale-100);
|
color: var(--color-blue-scale-100);
|
||||||
background-color: var(--form-background);
|
background-color: var(--color-blue-scale-500);
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:slotted(input[type="checkbox"]) {
|
:slotted(input[type="checkbox"]) {
|
||||||
@ -121,8 +134,7 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
|
|||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
color: var(--form-control-disabled);
|
color: var(--color-blue-scale-200);
|
||||||
--form-control-color: var(--form-control-disabled);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -10,7 +10,8 @@ import {
|
|||||||
faPlay,
|
faPlay,
|
||||||
faQuestion,
|
faQuestion,
|
||||||
faStop,
|
faStop,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/pro-solid-svg-icons";
|
||||||
|
import FormWidget from "@/components/FormWidget.vue";
|
||||||
import type { PowerState } from "@/libs/xen-api";
|
import type { PowerState } from "@/libs/xen-api";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -18,36 +19,33 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const icon = computed(() => {
|
const icon = computed(() => {
|
||||||
switch (props.state) {
|
const icons = {
|
||||||
case "Running":
|
Running: faPlay,
|
||||||
return faPlay;
|
Paused: faPause,
|
||||||
case "Paused":
|
Suspended: faMoon,
|
||||||
return faPause;
|
Unknown: faQuestion,
|
||||||
case "Suspended":
|
Halted: faStop,
|
||||||
return faMoon;
|
};
|
||||||
case "Unknown":
|
|
||||||
return faQuestion;
|
return icons[props.state];
|
||||||
default:
|
|
||||||
return faStop;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const className = computed(() => props.state.toLocaleLowerCase());
|
const className = computed(() => `state-${props.state.toLocaleLowerCase()}`);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="postcss">
|
<style scoped lang="postcss">
|
||||||
.power-state-icon {
|
.power-state-icon {
|
||||||
color: var(--color-extra-blue-d60);
|
color: var(--color-extra-blue-d60);
|
||||||
|
|
||||||
&.running {
|
&.state-running {
|
||||||
color: var(--color-green-infra-base);
|
color: var(--color-green-infra-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.paused {
|
&.state-paused {
|
||||||
color: var(--color-blue-scale-300);
|
color: var(--color-blue-scale-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.suspended {
|
&.state-suspended {
|
||||||
color: var(--color-extra-blue-d20);
|
color: var(--color-extra-blue-d20);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="infra-action">
|
<div class="infra-action">
|
||||||
<FontAwesomeIcon :icon="icon" fixed-width />
|
<slot>
|
||||||
|
<FontAwesomeIcon :icon="icon" fixed-width />
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -8,7 +10,7 @@
|
|||||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
icon: IconDefinition;
|
icon?: IconDefinition;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -20,11 +20,8 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import {
|
import { faAngleDown, faAngleUp } from "@fortawesome/pro-light-svg-icons";
|
||||||
faAngleDown,
|
import { faServer } from "@fortawesome/pro-regular-svg-icons";
|
||||||
faAngleUp,
|
|
||||||
faServer,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { useToggle } from "@vueuse/core";
|
import { useToggle } from "@vueuse/core";
|
||||||
import InfraAction from "@/components/infra/InfraAction.vue";
|
import InfraAction from "@/components/infra/InfraAction.vue";
|
||||||
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
||||||
|
@ -33,8 +33,6 @@ defineProps<{
|
|||||||
.infra-item-label {
|
.infra-item-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
height: 6rem;
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
color: var(--color-blue-scale-100);
|
color: var(--color-blue-scale-100);
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
background-color: var(--background-color-primary);
|
background-color: var(--background-color-primary);
|
||||||
@ -64,10 +62,12 @@ defineProps<{
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-left: 1.5rem;
|
padding: 1.5rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="infra-pool-list">
|
<ul class="infra-pool-list">
|
||||||
<InfraLoadingItem v-if="!isReady" :icon="faBuilding" />
|
<InfraLoadingItem v-if="!isReady" :icon="faBuildings" />
|
||||||
<li v-else class="infra-pool-item">
|
<li v-else class="infra-pool-item">
|
||||||
<InfraItemLabel
|
<InfraItemLabel
|
||||||
:icon="faBuilding"
|
:icon="faBuildings"
|
||||||
:route="{ name: 'pool.dashboard', params: { uuid: pool.uuid } }"
|
:route="{ name: 'pool.dashboard', params: { uuid: pool.uuid } }"
|
||||||
current
|
current
|
||||||
>
|
>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { faBuilding } from "@fortawesome/free-regular-svg-icons";
|
import { faBuildings } from "@fortawesome/pro-regular-svg-icons";
|
||||||
import InfraHostList from "@/components/infra/InfraHostList.vue";
|
import InfraHostList from "@/components/infra/InfraHostList.vue";
|
||||||
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
||||||
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
|
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
|
||||||
|
@ -7,7 +7,9 @@
|
|||||||
>
|
>
|
||||||
{{ vm.name_label }}
|
{{ vm.name_label }}
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<InfraAction :class="powerStateClass" :icon="powerStateIcon" />
|
<InfraAction>
|
||||||
|
<PowerStateIcon :state="vm?.power_state" />
|
||||||
|
</InfraAction>
|
||||||
</template>
|
</template>
|
||||||
</InfraItemLabel>
|
</InfraItemLabel>
|
||||||
</li>
|
</li>
|
||||||
@ -15,14 +17,9 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import {
|
import { faDisplay } from "@fortawesome/pro-solid-svg-icons";
|
||||||
faDisplay,
|
|
||||||
faMoon,
|
|
||||||
faPause,
|
|
||||||
faPlay,
|
|
||||||
faStop,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { useIntersectionObserver } from "@vueuse/core";
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
|
import PowerStateIcon from "@/components/PowerStateIcon.vue";
|
||||||
import InfraAction from "@/components/infra/InfraAction.vue";
|
import InfraAction from "@/components/infra/InfraAction.vue";
|
||||||
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
||||||
import { useVmStore } from "@/stores/vm.store";
|
import { useVmStore } from "@/stores/vm.store";
|
||||||
@ -44,23 +41,6 @@ const { stop } = useIntersectionObserver(rootElement, ([entry]) => {
|
|||||||
const vmStore = useVmStore();
|
const vmStore = useVmStore();
|
||||||
|
|
||||||
const vm = computed(() => vmStore.getRecord(props.vmOpaqueRef));
|
const vm = computed(() => vmStore.getRecord(props.vmOpaqueRef));
|
||||||
|
|
||||||
const powerStateIcon = computed(() => {
|
|
||||||
switch (vm.value?.power_state) {
|
|
||||||
case "Running":
|
|
||||||
return faPlay;
|
|
||||||
case "Paused":
|
|
||||||
return faPause;
|
|
||||||
case "Suspended":
|
|
||||||
return faMoon;
|
|
||||||
default:
|
|
||||||
return faStop;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const powerStateClass = computed(() =>
|
|
||||||
vm.value?.power_state.toLocaleLowerCase()
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
import { faDisplay } from "@fortawesome/pro-solid-svg-icons";
|
||||||
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
|
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
|
||||||
import InfraVmItem from "@/components/infra/InfraVmItem.vue";
|
import InfraVmItem from "@/components/infra/InfraVmItem.vue";
|
||||||
import { useVmStore } from "@/stores/vm.store";
|
import { useVmStore } from "@/stores/vm.store";
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { faBuilding } from "@fortawesome/free-regular-svg-icons";
|
import { faBuilding } from "@fortawesome/pro-regular-svg-icons";
|
||||||
import TitleBar from "@/components/TitleBar.vue";
|
import TitleBar from "@/components/TitleBar.vue";
|
||||||
import { usePoolStore } from "@/stores/pool.store";
|
import { usePoolStore } from "@/stores/pool.store";
|
||||||
|
|
||||||
|
@ -1,15 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="ui-badge">
|
<span class="ui-badge">
|
||||||
|
<FontAwesomeIcon :icon="icon" />
|
||||||
<slot />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
icon?: IconDefinition;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
.ui-badge {
|
.ui-badge {
|
||||||
font-size: 1.2rem;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 100%;
|
padding: 0 0.8rem;
|
||||||
padding: 0.1rem 0.5rem;
|
height: 2.4rem;
|
||||||
color: var(--color-blue-scale-500);
|
color: var(--color-blue-scale-500);
|
||||||
border-radius: 9.6rem;
|
border-radius: 9.6rem;
|
||||||
background-color: var(--color-blue-scale-300);
|
background-color: var(--color-blue-scale-300);
|
||||||
|
@ -1,70 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
:class="`color-${buttonColor}`"
|
:class="[
|
||||||
|
`color-${buttonColor}`,
|
||||||
|
{ busy: isBusy, disabled: isDisabled, 'has-icon': icon !== undefined },
|
||||||
|
]"
|
||||||
:disabled="isBusy || isDisabled"
|
:disabled="isBusy || isDisabled"
|
||||||
:type="type || 'button'"
|
:type="type || 'button'"
|
||||||
class="ui-button"
|
class="ui-button"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon v-if="isBusy" :icon="faSpinner" spin />
|
<FontAwesomeIcon v-if="isBusy" :icon="faSpinner" class="icon" spin />
|
||||||
<FontAwesomeIcon v-else-if="iconLeft" :icon="iconLeft" />
|
<template v-else>
|
||||||
<slot />
|
<FontAwesomeIcon v-if="icon" :icon="icon" class="icon" />
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, unref } from "vue";
|
import { computed, inject, unref } from "vue";
|
||||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||||
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
|
import { faSpinner } from "@fortawesome/pro-regular-svg-icons";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type?: "button" | "reset" | "submit";
|
type?: "button" | "reset" | "submit";
|
||||||
busy?: boolean;
|
busy?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
iconLeft?: IconDefinition;
|
icon?: IconDefinition;
|
||||||
color?: "default" | "action";
|
color?: "primary" | "secondary";
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isGroupBusy = inject("isButtonGroupBusy", false);
|
const isGroupBusy = inject("isButtonGroupBusy", false);
|
||||||
const isBusy = computed(() => props.busy || unref(isGroupBusy));
|
const isBusy = computed(() => props.busy ?? unref(isGroupBusy));
|
||||||
|
|
||||||
const isGroupDisabled = inject("isButtonGroupDisabled", false);
|
const isGroupDisabled = inject("isButtonGroupDisabled", false);
|
||||||
const isDisabled = computed(() => props.disabled || unref(isGroupDisabled));
|
const isDisabled = computed(() => props.disabled ?? unref(isGroupDisabled));
|
||||||
|
|
||||||
const buttonGroupColor = inject("buttonGroupColor", "default");
|
const buttonGroupColor = inject("buttonGroupColor", "primary");
|
||||||
const buttonColor = computed(() => props.color || unref(buttonGroupColor));
|
const buttonColor = computed(() => props.color ?? unref(buttonGroupColor));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.ui-button {
|
.ui-button {
|
||||||
font-size: 1.6rem;
|
font-size: 2rem;
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 3.8rem;
|
min-width: 10rem;
|
||||||
margin: 0;
|
height: 5rem;
|
||||||
padding: 0 1rem;
|
padding: 0 1.5rem;
|
||||||
color: var(--color-blue-scale-500);
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
background-color: var(--color-extra-blue-base);
|
box-shadow: var(--shadow-100);
|
||||||
gap: 1rem;
|
gap: 1.5rem;
|
||||||
|
|
||||||
&:not([disabled]) {
|
&.has-icon {
|
||||||
cursor: pointer;
|
min-width: 13rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.color-action {
|
&.disabled {
|
||||||
color: var(--color-grayscale-200);
|
color: var(--color-blue-scale-400);
|
||||||
background-color: var(--color-white);
|
background-color: var(--background-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
&:not([disabled]):hover {
|
&:not(.disabled) {
|
||||||
background-color: var(--background-color-secondary);
|
&.color-primary {
|
||||||
|
color: var(--color-blue-scale-500);
|
||||||
|
background-color: var(--color-extra-blue-base);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-extra-blue-d20);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&.busy {
|
||||||
|
background-color: var(--color-extra-blue-d40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-secondary {
|
||||||
|
color: var(--color-extra-blue-base);
|
||||||
|
border: 1px solid var(--color-extra-blue-base);
|
||||||
|
background-color: var(--color-blue-scale-500);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-extra-blue-d20);
|
||||||
|
border-color: var(--color-extra-blue-d20);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&.busy {
|
||||||
|
color: var(--color-extra-blue-d40);
|
||||||
|
border-color: var(--color-extra-blue-d40);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&[disabled] {
|
.icon {
|
||||||
opacity: 0.5;
|
font-size: 1.6rem;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -5,19 +5,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { provide, toRef } from "vue";
|
import { computed, provide } from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
busy?: boolean;
|
busy?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
color?: "default" | "action";
|
color?: "primary" | "secondary";
|
||||||
}>();
|
}>();
|
||||||
provide("isButtonGroupBusy", toRef(props, "busy"));
|
provide(
|
||||||
provide("isButtonGroupDisabled", toRef(props, "disabled"));
|
"isButtonGroupBusy",
|
||||||
provide("buttonGroupColor", toRef(props, "color"));
|
computed(() => props.busy ?? false)
|
||||||
|
);
|
||||||
|
provide(
|
||||||
|
"isButtonGroupDisabled",
|
||||||
|
computed(() => props.disabled ?? false)
|
||||||
|
);
|
||||||
|
provide(
|
||||||
|
"buttonGroupColor",
|
||||||
|
computed(() => props.color ?? "primary")
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.ui-button-group {
|
.ui-button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
|
@ -1,35 +1,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="ui-filter">
|
<span class="ui-filter">
|
||||||
<slot />
|
<span class="label" @click="emit('edit')">
|
||||||
<FontAwesomeIcon :icon="faRemove" class="remove" @click="emit('remove')" />
|
<slot />
|
||||||
|
</span>
|
||||||
|
<span class="remove" @click.stop="emit('remove')">
|
||||||
|
<FontAwesomeIcon :icon="faRemove" class="icon" />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { faRemove } from "@fortawesome/free-solid-svg-icons";
|
import { faRemove } from "@fortawesome/pro-solid-svg-icons";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
(event: "edit"): void;
|
||||||
(event: "remove"): void;
|
(event: "remove"): void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.ui-filter {
|
.ui-filter {
|
||||||
|
overflow: hidden;
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 3.4rem;
|
height: 3.4rem;
|
||||||
padding: 0 1rem;
|
|
||||||
color: var(--color-extra-blue-base);
|
color: var(--color-extra-blue-base);
|
||||||
border: 1px solid var(--color-extra-blue-base);
|
|
||||||
border-radius: 1.7rem;
|
border-radius: 1.7rem;
|
||||||
background-color: var(--background-color-extra-blue);
|
background-color: var(--background-color-extra-blue);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
border: 1px solid var(--color-extra-blue-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label,
|
||||||
|
.remove {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove {
|
.remove {
|
||||||
cursor: pointer;
|
color: white;
|
||||||
color: red;
|
border-radius: 1.4rem;
|
||||||
|
width: 2.8rem;
|
||||||
|
margin: 0.2rem;
|
||||||
|
background-color: var(--color-extra-blue-l40);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-red-vates-l20);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.ui-filter-group {
|
.ui-filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.ui-modal {
|
.ui-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -24,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
background-color: white;
|
background-color: var(--background-color-primary);
|
||||||
min-width: 40rem;
|
min-width: 40rem;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
@ -33,6 +33,6 @@
|
|||||||
|
|
||||||
:slotted(.ui-button-group) {
|
:slotted(.ui-button-group) {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 2rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.ui-table {
|
.ui-table {
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
}
|
}
|
||||||
|
@ -2,32 +2,28 @@
|
|||||||
<UiButtonGroup
|
<UiButtonGroup
|
||||||
:disabled="selectedRefs.length === 0"
|
:disabled="selectedRefs.length === 0"
|
||||||
class="vms-actions-bar"
|
class="vms-actions-bar"
|
||||||
color="action"
|
color="secondary"
|
||||||
>
|
>
|
||||||
<UiButton :icon-left="faPowerOff">Change power state</UiButton>
|
<UiButton :icon="faPowerOff">Change power state</UiButton>
|
||||||
<UiButton :icon-left="faRoute">Migrate</UiButton>
|
<UiButton :icon="faRoute">Migrate</UiButton>
|
||||||
<UiButton :icon-left="faCopy">Copy</UiButton>
|
<UiButton :icon="faCopy">Copy</UiButton>
|
||||||
<UiButton :icon-left="faEdit">Edit config</UiButton>
|
<UiButton :icon="faEdit">Edit config</UiButton>
|
||||||
<UiButton :icon-left="faCamera">Snapshot</UiButton>
|
<UiButton :icon="faCamera">Snapshot</UiButton>
|
||||||
<UiButton :icon-left="faBox">Backup</UiButton>
|
<UiButton :icon="faBox">Backup</UiButton>
|
||||||
<UiButton :icon-left="faTrashCan">Delete</UiButton>
|
<UiButton :icon="faTrashCan">Delete</UiButton>
|
||||||
<UiButton :icon-left="faFileExport">Export</UiButton>
|
<UiButton :icon="faFileExport">Export</UiButton>
|
||||||
</UiButtonGroup>
|
</UiButtonGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import { faCopy, faEdit, faTrashCan } from "@fortawesome/pro-regular-svg-icons";
|
||||||
faCopy,
|
|
||||||
faEdit,
|
|
||||||
faTrashCan,
|
|
||||||
} from "@fortawesome/free-regular-svg-icons";
|
|
||||||
import {
|
import {
|
||||||
faBox,
|
faBox,
|
||||||
faCamera,
|
faCamera,
|
||||||
faFileExport,
|
faFileExport,
|
||||||
faPowerOff,
|
faPowerOff,
|
||||||
faRoute,
|
faRoute,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/pro-solid-svg-icons";
|
||||||
import UiButton from "@/components/ui/UiButton.vue";
|
import UiButton from "@/components/ui/UiButton.vue";
|
||||||
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
|
||||||
|
|
||||||
@ -37,7 +33,7 @@ defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.vms-actions-bar {
|
.vms-actions-bar {
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
# useBusy composable
|
# useBusy composable
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="error" v-if="error">{{ error }}</span>
|
<span class="error" v-if="error">{{ error }}</span>
|
||||||
<button @click="run" :disabled="isBusy">Do something</button>
|
<button @click="run" :disabled="isBusy">Do something</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import useBusy from "@/composables/busy.composable";
|
import useBusy from '@/composables/busy.composable';
|
||||||
|
|
||||||
async function doSomething() {
|
async function doSomething() {
|
||||||
try {
|
try {
|
||||||
// Doing some async work
|
// Doing some async work
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw "Something bad happened";
|
throw "Something bad happened";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const { isBusy, error, run } = useBusy(doSomething);
|
const { isBusy, error, run } = useBusy(doSomething)
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
@ -1,5 +1,35 @@
|
|||||||
# useCollectionFilter composable
|
# useCollectionFilter composable
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
|
||||||
|
|
||||||
|
const filteredCollection = myCollection.filter(predicate);
|
||||||
|
```
|
||||||
|
|
||||||
|
## URL Query String
|
||||||
|
|
||||||
|
By default, when adding/removing filters, the URL will update automatically.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
addFilter('name:/^foo/i'); // Will update the URL with ?filter=name:/^foo/i
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change the URL query string parameter name
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { /* ... */ } = useCollectionFilter({ queryStringParam: 'f' }); // ?f=name:/^foo/i
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable the usage of URL query string
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { /* ... */ } = useCollectionFilter({ queryStringParam: undefined });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example of using the composable with the `CollectionFilter` component
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<CollectionFilter
|
<CollectionFilter
|
||||||
@ -8,32 +38,32 @@
|
|||||||
@add-filter="addFilter"
|
@add-filter="addFilter"
|
||||||
@remove-filter="removeFilter"
|
@remove-filter="removeFilter"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-for="item in filteredCollection">...</div>
|
<div v-for="item in filteredCollection">...</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue";
|
import CollectionFilter from "@/components/CollectionFilter.vue";
|
||||||
import CollectionFilter from "@/components/CollectionFilter.vue";
|
import useCollectionFilter from "@/composables/collection-filter.composable";
|
||||||
import useCollectionFilter from "@/composables/collection-filter.composable";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const collection = [
|
const collection = [
|
||||||
{ name: "Foo", age: 5, registered: true },
|
{ name: "Foo", age: 5, registered: true },
|
||||||
{ name: "Bar", age: 12, registered: false },
|
{ name: "Bar", age: 12, registered: false },
|
||||||
{ name: "Foo Bar", age: 2, registered: true },
|
{ name: "Foo Bar", age: 2, registered: true },
|
||||||
{ name: "Bar Baz", age: 45, registered: false },
|
{ name: "Bar Baz", age: 45, registered: false },
|
||||||
{ name: "Foo Baz", age: 32, registered: false },
|
{ name: "Foo Baz", age: 32, registered: false },
|
||||||
{ name: "Foo Bar Baz", age: 32, registered: true },
|
{ name: "Foo Bar Baz", age: 32, registered: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const availableFilters: AvailableFilter[] = [
|
const availableFilters: AvailableFilter[] = [
|
||||||
{ property: "name", label: "Name", type: "string" },
|
{ property: "name", label: "Name", type: "string" },
|
||||||
{ property: "age", label: "Age", type: "number" },
|
{ property: "age", label: "Age", type: "number" },
|
||||||
{ property: "registered", label: "Registered", type: "boolean", icon: faKey },
|
{ property: "registered", label: "Registered", type: "boolean", icon: faKey },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
|
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
|
||||||
|
|
||||||
const filteredCollection = computed(() => collection.filter(predicate));
|
const filteredCollection = computed(() => collection.filter(predicate));
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
@ -20,9 +20,11 @@ export default function useCollectionFilter(
|
|||||||
|
|
||||||
if (config.queryStringParam) {
|
if (config.queryStringParam) {
|
||||||
const queryStringParam = config.queryStringParam;
|
const queryStringParam = config.queryStringParam;
|
||||||
watch(filters, (value) => {
|
watch(filters, (value) =>
|
||||||
router.replace({ query: { [queryStringParam]: value.join(" ") } });
|
router.replace({
|
||||||
});
|
query: { ...route.query, [queryStringParam]: value.join(" ") },
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const addFilter = (filter: string) => {
|
const addFilter = (filter: string) => {
|
||||||
@ -47,10 +49,16 @@ export default function useCollectionFilter(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryToSet(filter: LocationQueryValue): Set<string> {
|
function queryToSet(query: LocationQueryValue): Set<string> {
|
||||||
if (filter) {
|
if (!query) {
|
||||||
return new Set(filter.split(" "));
|
return new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Set();
|
const rootNode = CM.parse(query);
|
||||||
|
|
||||||
|
if (rootNode instanceof CM.And) {
|
||||||
|
return new Set(rootNode.children.map((child) => child.toString()));
|
||||||
|
} else {
|
||||||
|
return new Set([rootNode.toString()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { type LocationQueryValue, useRoute, useRouter } from "vue-router";
|
||||||
|
import type { ActiveSorts } from "@/types/sort";
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
queryStringParam?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useCollectionSorter(
|
||||||
|
config: Config = { queryStringParam: "sort" }
|
||||||
|
) {
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const sorts = ref<ActiveSorts>(
|
||||||
|
config.queryStringParam
|
||||||
|
? queryToMap(route.query[config.queryStringParam] as LocationQueryValue)
|
||||||
|
: new Map()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (config.queryStringParam) {
|
||||||
|
const queryStringParam = config.queryStringParam;
|
||||||
|
watch(
|
||||||
|
sorts,
|
||||||
|
(value) =>
|
||||||
|
router.replace({
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
[queryStringParam]: Array.from(value)
|
||||||
|
.map(
|
||||||
|
([property, isAscending]) =>
|
||||||
|
`${property}:${isAscending ? "1" : "0"}`
|
||||||
|
)
|
||||||
|
.join(","),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSort = (property: string, isAscending: boolean) => {
|
||||||
|
sorts.value.set(property, isAscending);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSort = (property: string) => {
|
||||||
|
sorts.value.delete(property);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSortDirection = (property: string) => {
|
||||||
|
if (!sorts.value.has(property)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sorts.value.set(property, !sorts.value.get(property));
|
||||||
|
};
|
||||||
|
|
||||||
|
const compareFn = computed(() => {
|
||||||
|
return (record1: any, record2: any) => {
|
||||||
|
for (const [property, isAscending] of sorts.value) {
|
||||||
|
const value1 = record1[property];
|
||||||
|
const value2 = record2[property];
|
||||||
|
|
||||||
|
if (value1 < value2) {
|
||||||
|
return isAscending ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value1 > value2) {
|
||||||
|
return isAscending ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
sorts,
|
||||||
|
addSort,
|
||||||
|
removeSort,
|
||||||
|
toggleSortDirection,
|
||||||
|
compareFn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryToMap(query: LocationQueryValue) {
|
||||||
|
if (!query) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Map(
|
||||||
|
query.split(",").map((sortRaw): [string, boolean] => {
|
||||||
|
const [property, isAscending] = sortRaw.split(":");
|
||||||
|
return [property, isAscending === "1"];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
@ -2,17 +2,14 @@
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import useFilteredCollection from "./filtered-collection.composable";
|
import useFilteredCollection from './filtered-collection.composable';
|
||||||
|
|
||||||
const players = [
|
const players = [
|
||||||
{ name: "Foo", team: "Blue" },
|
{ name: "Foo", team: "Blue" },
|
||||||
{ name: "Bar", team: "Red" },
|
{ name: "Bar", team: "Red" },
|
||||||
{ name: "Baz", team: "Blue" },
|
{ name: "Baz", team: "Blue" },
|
||||||
];
|
]
|
||||||
|
|
||||||
const bluePlayers = useFilteredCollection(
|
const bluePlayers = useFilteredCollection(players, (player) => player.team === "Blue");
|
||||||
players,
|
|
||||||
(player) => player.team === "Blue"
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
@ -5,9 +5,7 @@ export default function useFilteredCollection<T>(
|
|||||||
collection: MaybeRef<T[]>,
|
collection: MaybeRef<T[]>,
|
||||||
predicate: MaybeRef<(value: T) => boolean>
|
predicate: MaybeRef<(value: T) => boolean>
|
||||||
) {
|
) {
|
||||||
const filteredCollection = computed(() => {
|
return computed(() => {
|
||||||
return unref(collection).filter(unref(predicate));
|
return unref(collection).filter(unref(predicate));
|
||||||
});
|
});
|
||||||
|
|
||||||
return filteredCollection;
|
|
||||||
}
|
}
|
||||||
|
@ -5,28 +5,27 @@
|
|||||||
<div v-for="item in items">
|
<div v-for="item in items">
|
||||||
{{ item.name }} <button @click="openRemoveModal(item)">Delete</button>
|
{{ item.name }} <button @click="openRemoveModal(item)">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UiModal v-if="isRemoveModalOpen">
|
<UiModal v-if="isRemoveModalOpen">
|
||||||
Are you sure you want to delete {{ removeModalPayload.name }}
|
Are you sure you want to delete {{ removeModalPayload.name }}
|
||||||
|
|
||||||
<button @click="handleRemove">Yes</button>
|
<button @click="handleRemove">Yes</button> <button @click="closeRemoveModal">No</button>
|
||||||
<button @click="closeRemoveModal">No</button>
|
|
||||||
</UiModal>
|
</UiModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import useModal from "@/composables/modal.composable";
|
import useModal from '@/composables/modal.composable';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
payload: removeModalPayload,
|
payload: removeModalPayload,
|
||||||
isOpen: isRemoveModalOpen,
|
isOpen: isRemoveModalOpen,
|
||||||
open: openRemoveModal,
|
open: openRemoveModal,
|
||||||
close: closeRemoveModal,
|
close: closeRemoveModal,
|
||||||
} = useModal();
|
} = useModal()
|
||||||
|
|
||||||
async function handleRemove() {
|
async function handleRemove() {
|
||||||
await removeItem(removeModalPayload.id);
|
await removeItem(removeModalPayload.id);
|
||||||
closeRemoveModal();
|
closeRemoveModal()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
@ -4,30 +4,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<input type="checkbox" v-model="areAllSelected" />
|
<input type="checkbox" v-model="areAllSelected">
|
||||||
</th>
|
</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="item in items">
|
<tr v-for="item in items">
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" :value="item.id" v-model="selected" />
|
<input type="checkbox" :value="item.id" v-model="selected" />
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.name }}</td>
|
<td>{{ item.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- You can use something else than a "Select All" checkbox -->
|
<!-- You can use something else than a "Select All" checkbox -->
|
||||||
<button @click="areAllSelected = !areAllSelected">Toggle all selected</button>
|
<button @click="areAllSelected = !areAllSelected">Toggle all selected</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import useMultiSelect from "./multi-select.composable";
|
|
||||||
|
|
||||||
const { selected, areAllSelected } = useMultiSelect();
|
<script lang="ts" setup>
|
||||||
|
import useMultiSelect from './multi-select.composable';
|
||||||
|
|
||||||
|
const {
|
||||||
|
selected,
|
||||||
|
areAllSelected,
|
||||||
|
} = useMultiSelect()
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
import { computed, ref, unref } from "vue";
|
|
||||||
import type { MaybeRef } from "@vueuse/core";
|
|
||||||
|
|
||||||
export default function useSortable<T>(data: MaybeRef<T[]>) {
|
|
||||||
const sortProperty = ref<keyof T>();
|
|
||||||
const sortAscending = ref(true);
|
|
||||||
const sortBy = (property: keyof T) => {
|
|
||||||
if (sortProperty.value !== property) {
|
|
||||||
sortProperty.value = property;
|
|
||||||
sortAscending.value = true;
|
|
||||||
} else if (sortAscending.value) {
|
|
||||||
sortAscending.value = !sortAscending.value;
|
|
||||||
} else {
|
|
||||||
sortAscending.value = true;
|
|
||||||
sortProperty.value = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const isSortedBy = (property: keyof T) => {
|
|
||||||
return sortProperty.value === property;
|
|
||||||
};
|
|
||||||
const isAscending = computed(() => sortAscending.value === true);
|
|
||||||
const records = computed(() => {
|
|
||||||
let records = [...unref(data)];
|
|
||||||
|
|
||||||
if (sortProperty.value) {
|
|
||||||
records = records.sort((record1, record2) => {
|
|
||||||
const value1 = record1[sortProperty.value!];
|
|
||||||
const value2 = record2[sortProperty.value!];
|
|
||||||
switch (true) {
|
|
||||||
case value1 < value2:
|
|
||||||
return sortAscending.value ? -1 : 1;
|
|
||||||
case value1 > value2:
|
|
||||||
return sortAscending.value ? 1 : -1;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return records;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
sortBy,
|
|
||||||
records,
|
|
||||||
isAscending,
|
|
||||||
isSortedBy,
|
|
||||||
};
|
|
||||||
}
|
|
@ -0,0 +1,11 @@
|
|||||||
|
import { computed, unref } from "vue";
|
||||||
|
import type { MaybeRef } from "@vueuse/core";
|
||||||
|
|
||||||
|
export default function useSortedCollection<T>(
|
||||||
|
collection: MaybeRef<T[]>,
|
||||||
|
compareFn: MaybeRef<(value1: T, value2: T) => -1 | 1 | 0>
|
||||||
|
) {
|
||||||
|
return computed(() => {
|
||||||
|
return [...unref(collection)].sort(unref(compareFn));
|
||||||
|
});
|
||||||
|
}
|
113
@xen-orchestra/lite/src/libs/complex-matcher.utils.ts
Normal file
113
@xen-orchestra/lite/src/libs/complex-matcher.utils.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import type { ComparisonOperator, ComplexMatcherNode } from "complex-matcher";
|
||||||
|
import * as CM from "complex-matcher";
|
||||||
|
import type { FilterComparisonType } from "@/types/filter";
|
||||||
|
import { escapeRegExp } from "@/libs/utils";
|
||||||
|
|
||||||
|
function buildStringNode(property: string, value: string, negate = false) {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = new CM.Property(property, new CM.StringNode(value));
|
||||||
|
|
||||||
|
if (negate) {
|
||||||
|
return new CM.Not(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRegexNode(
|
||||||
|
property: string,
|
||||||
|
value: string,
|
||||||
|
prefix = "",
|
||||||
|
suffix = "",
|
||||||
|
escape = false,
|
||||||
|
negate = false
|
||||||
|
) {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (escape) {
|
||||||
|
value = escapeRegExp(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = new CM.Property(
|
||||||
|
property,
|
||||||
|
new CM.RegExpNode(`${prefix}${value}${suffix}`, "i")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (negate) {
|
||||||
|
return new CM.Not(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNumberComparisonNode(
|
||||||
|
property: string,
|
||||||
|
value: string | number,
|
||||||
|
comparisonOperator: ComparisonOperator | "="
|
||||||
|
) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
value = parseInt(value, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparisonOperator === "=") {
|
||||||
|
return new CM.Property(property, new CM.Number(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CM.Property(
|
||||||
|
property,
|
||||||
|
new CM.Comparison(comparisonOperator, value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBooleanNode(property: string, value: boolean) {
|
||||||
|
const node = new CM.TruthyProperty(property);
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return new CM.Not(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildComplexMatcherNode(
|
||||||
|
comparisonType: FilterComparisonType,
|
||||||
|
property: string,
|
||||||
|
value: string,
|
||||||
|
negate: boolean
|
||||||
|
): ComplexMatcherNode | undefined {
|
||||||
|
switch (comparisonType) {
|
||||||
|
case "stringContains":
|
||||||
|
return buildStringNode(property, value, negate);
|
||||||
|
case "stringStartsWith":
|
||||||
|
return buildRegexNode(property, value, "^", "", true, negate);
|
||||||
|
case "stringEndsWith":
|
||||||
|
return buildRegexNode(property, value, "", "$", true, negate);
|
||||||
|
case "stringEquals":
|
||||||
|
return buildRegexNode(property, value, "^", "$", true, negate);
|
||||||
|
case "stringMatchesRegex":
|
||||||
|
return buildRegexNode(property, value, "", "", false, negate);
|
||||||
|
case "numberLessThan":
|
||||||
|
return buildNumberComparisonNode(property, value, "<");
|
||||||
|
case "numberLessThanOrEquals":
|
||||||
|
return buildNumberComparisonNode(property, value, "<=");
|
||||||
|
case "numberEquals":
|
||||||
|
return buildNumberComparisonNode(property, value, "=");
|
||||||
|
case "numberGreaterThanOrEquals":
|
||||||
|
return buildNumberComparisonNode(property, value, ">=");
|
||||||
|
case "numberGreaterThan":
|
||||||
|
return buildNumberComparisonNode(property, value, ">");
|
||||||
|
case "booleanTrue":
|
||||||
|
return buildBooleanNode(property, true);
|
||||||
|
case "booleanFalse":
|
||||||
|
return buildBooleanNode(property, false);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,7 @@
|
|||||||
|
import type { Filter } from "@/types/filter";
|
||||||
|
import { faSquareCheck } from "@fortawesome/pro-regular-svg-icons";
|
||||||
|
import { faFont, faHashtag, faList } from "@fortawesome/pro-solid-svg-icons";
|
||||||
|
|
||||||
export function sortRecordsByNameLabel(
|
export function sortRecordsByNameLabel(
|
||||||
record1: { name_label: string },
|
record1: { name_label: string },
|
||||||
record2: { name_label: string }
|
record2: { name_label: string }
|
||||||
@ -18,3 +22,22 @@ export function sortRecordsByNameLabel(
|
|||||||
export function escapeRegExp(string: string) {
|
export function escapeRegExp(string: string) {
|
||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFilterIcon(filter: Filter | undefined) {
|
||||||
|
if (!filter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.icon) {
|
||||||
|
return filter.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconsByType = {
|
||||||
|
string: faFont,
|
||||||
|
number: faHashtag,
|
||||||
|
boolean: faSquareCheck,
|
||||||
|
enum: faList,
|
||||||
|
};
|
||||||
|
|
||||||
|
return iconsByType[filter.type];
|
||||||
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import pool from "@/router/pool";
|
import pool from "@/router/pool";
|
||||||
import DemoView from "@/views/DemoView.vue";
|
|
||||||
import HomeView from "@/views/HomeView.vue";
|
import HomeView from "@/views/HomeView.vue";
|
||||||
import HostDashboardView from "@/views/host/HostDashboardView.vue";
|
import HostDashboardView from "@/views/host/HostDashboardView.vue";
|
||||||
import HostRootView from "@/views/host/HostRootView.vue";
|
import HostRootView from "@/views/host/HostRootView.vue";
|
||||||
@ -15,11 +14,6 @@ const router = createRouter({
|
|||||||
name: "home",
|
name: "home",
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/demo",
|
|
||||||
name: "demo",
|
|
||||||
component: DemoView,
|
|
||||||
},
|
|
||||||
pool,
|
pool,
|
||||||
{
|
{
|
||||||
path: "/host/:uuid",
|
path: "/host/:uuid",
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
declare module "complex-matcher" {
|
declare module "complex-matcher" {
|
||||||
type ComparisonOperator = ">" | ">=" | "<" | "<=";
|
type ComparisonOperator = ">" | ">=" | "<" | "<=";
|
||||||
|
|
||||||
class Node {
|
class ComplexMatcherNode {
|
||||||
createPredicate(): (value) => boolean;
|
createPredicate(): (value) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Null extends Node {
|
class Null extends ComplexMatcherNode {
|
||||||
match(): true;
|
match(): true;
|
||||||
toString(): "";
|
toString(): "";
|
||||||
}
|
}
|
||||||
|
|
||||||
class And extends Node {
|
class And extends ComplexMatcherNode {
|
||||||
constructor(children: Node[]);
|
constructor(children: ComplexMatcherNode[]);
|
||||||
match(value: any): boolean;
|
match(value: any): boolean;
|
||||||
toString(isNested?: boolean): string;
|
toString(isNested?: boolean): string;
|
||||||
children: Node[];
|
children: ComplexMatcherNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class Comparison extends Node {
|
class Comparison extends ComplexMatcherNode {
|
||||||
constructor(operator: ComparisonOperator, value: number);
|
constructor(operator: ComparisonOperator, value: number);
|
||||||
match(value: any): boolean;
|
match(value: any): boolean;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
@ -25,61 +25,65 @@ declare module "complex-matcher" {
|
|||||||
_value: number;
|
_value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Or extends Node {
|
class Or extends ComplexMatcherNode {
|
||||||
constructor(children: Node[]);
|
constructor(children: ComplexMatcherNode[]);
|
||||||
match(value: any): boolean;
|
match(value: any): boolean;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
children: Node[];
|
children: ComplexMatcherNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class Not extends Node {
|
class Not extends ComplexMatcherNode {
|
||||||
constructor(child: Node);
|
constructor(child: ComplexMatcherNode);
|
||||||
match(value: any): boolean;
|
match(value: any): boolean;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
child: Node;
|
child: ComplexMatcherNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Number extends Node {
|
class Number extends ComplexMatcherNode {
|
||||||
constructor(value: number);
|
constructor(value: number);
|
||||||
match(value: any): boolean;
|
match(value: any): boolean;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Property extends Node {
|
class Property extends ComplexMatcherNode {
|
||||||
constructor(name: string, child: Node);
|
constructor(name: string, child: ComplexMatcherNode);
|
||||||
match(value: any): boolean;
|
match(value: any): boolean;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
name: string;
|
name: string;
|
||||||
child: Node;
|
child: ComplexMatcherNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GlobPattern extends Node {
|
class GlobPattern extends ComplexMatcherNode {
|
||||||
constructor(value: string);
|
constructor(value: string);
|
||||||
match(re: RegExp, value: any): boolean;
|
match(re: RegExp, value: any): boolean;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RegExpNode extends Node {
|
class RegExpNode extends ComplexMatcherNode {
|
||||||
constructor(pattern: string, flags: string);
|
constructor(pattern: string, flags: string);
|
||||||
match(value: any): boolean;
|
match(value: any): boolean;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
re: RegExp;
|
re: RegExp;
|
||||||
}
|
}
|
||||||
|
|
||||||
class StringNode extends Node {
|
class StringNode extends ComplexMatcherNode {
|
||||||
constructor(value: string);
|
constructor(value: string);
|
||||||
match(lcValue: string, value: any): boolean;
|
match(lcValue: string, value: any): boolean;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TruthyProperty extends Node {
|
class TruthyProperty extends ComplexMatcherNode {
|
||||||
constructor(name: string);
|
constructor(name: string);
|
||||||
match(value: any): boolean;
|
match(value: any): boolean;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parse(input: string, pos? = 0, end?: input.length): Node;
|
function parse(
|
||||||
|
input: string,
|
||||||
|
pos? = 0,
|
||||||
|
end?: input.length
|
||||||
|
): ComplexMatcherNode;
|
||||||
}
|
}
|
||||||
|
@ -2,30 +2,50 @@ import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
|||||||
|
|
||||||
export type FilterType = "string" | "boolean" | "number" | "enum";
|
export type FilterType = "string" | "boolean" | "number" | "enum";
|
||||||
|
|
||||||
export interface FilterComparison {
|
export type FilterComparisonType =
|
||||||
label: string;
|
| "stringContains"
|
||||||
pattern: string;
|
| "stringEquals"
|
||||||
default?: boolean;
|
| "stringStartsWith"
|
||||||
before?: string | IconDefinition;
|
| "stringEndsWith"
|
||||||
after?: string | IconDefinition;
|
| "stringMatchesRegex"
|
||||||
escape?: boolean;
|
| "numberLessThan"
|
||||||
}
|
| "numberLessThanOrEquals"
|
||||||
|
| "numberEquals"
|
||||||
|
| "numberGreaterThanOrEquals"
|
||||||
|
| "numberGreaterThan"
|
||||||
|
| "booleanTrue"
|
||||||
|
| "booleanFalse";
|
||||||
|
|
||||||
export type FilterComparisons = Record<FilterType, FilterComparison[]>;
|
export type FilterComparisons = {
|
||||||
|
[key in FilterComparisonType]?: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface AvailableFilterCommon {
|
interface FilterCommon {
|
||||||
property: string;
|
|
||||||
label?: string;
|
label?: string;
|
||||||
icon?: IconDefinition;
|
icon?: IconDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvailableFilterEnum extends AvailableFilterCommon {
|
export interface FilterEnum extends FilterCommon {
|
||||||
type: "enum";
|
type: "enum";
|
||||||
choices: string[];
|
choices: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AvailableFilterOther extends AvailableFilterCommon {
|
interface FilterOther extends FilterCommon {
|
||||||
type: Exclude<FilterType, "enum">;
|
type: Exclude<FilterType, "enum">;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AvailableFilter = AvailableFilterEnum | AvailableFilterOther;
|
export type Filter = FilterEnum | FilterOther;
|
||||||
|
|
||||||
|
export type Filters = { [key: string]: Filter };
|
||||||
|
|
||||||
|
export interface NewFilter {
|
||||||
|
id: number;
|
||||||
|
content: string;
|
||||||
|
isAdvanced: boolean;
|
||||||
|
builder: {
|
||||||
|
property: string;
|
||||||
|
comparison: FilterComparisonType | "";
|
||||||
|
value: string;
|
||||||
|
negate: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
10
@xen-orchestra/lite/src/types/sort.ts
Normal file
10
@xen-orchestra/lite/src/types/sort.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||||
|
|
||||||
|
interface Sort {
|
||||||
|
label?: string;
|
||||||
|
icon?: IconDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Sorts = { [key: string]: Sort };
|
||||||
|
|
||||||
|
export type ActiveSorts = Map<string, boolean>;
|
@ -1,22 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="foo">
|
|
||||||
<select>
|
|
||||||
<option></option>
|
|
||||||
<option value="foo">Foo</option>
|
|
||||||
<option value="foo">Foo</option>
|
|
||||||
<option value="foo">Foo</option>
|
|
||||||
<option value="foo">Foo</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="foo">
|
|
||||||
<input placeholder="Bar" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup></script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.foo:has(option[value]:checked) {
|
|
||||||
border: 1px solid blue;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,98 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pool-dashboard-view">
|
<div class="pool-dashboard-view">
|
||||||
<PoolDashboardStatus class="item" />
|
<PoolDashboardStatus class="item" />
|
||||||
|
|
||||||
<UiCard style="min-width: 40rem">
|
<UiCard style="min-width: 40rem">
|
||||||
<UiTitle type="h4">RAM usage</UiTitle>
|
<UiTitle type="h4">Storage usage</UiTitle>
|
||||||
<UsageBar title="Hosts" :data="data.slice(0, 5)">
|
<ProgressBar :value="65" style="margin: 1rem 0" />
|
||||||
<template #header>
|
<ProgressBar :value="50" style="margin: 1rem 0" />
|
||||||
<span>Host</span>
|
<ProgressBar :value="40" style="margin: 1rem 0" />
|
||||||
<span>Top 5</span>
|
<ProgressBar :value="22" style="margin: 1rem 0" />
|
||||||
</template>
|
</UiCard>
|
||||||
</UsageBar>
|
|
||||||
|
<UiCard>
|
||||||
|
<UiBadge>38/64 GB</UiBadge>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import UsageBar from "@/components/UsageBar.vue";
|
import ProgressBar from "@/components/ProgressBar.vue";
|
||||||
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
|
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
|
||||||
|
import UiBadge from "@/components/ui/UiBadge.vue";
|
||||||
import UiCard from "@/components/ui/UiCard.vue";
|
import UiCard from "@/components/ui/UiCard.vue";
|
||||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||||
|
|
||||||
const data = [
|
|
||||||
{
|
|
||||||
value: 61,
|
|
||||||
maxValue: 128,
|
|
||||||
label: "R620-L2",
|
|
||||||
badgeLabel: "61/128 GB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 38,
|
|
||||||
maxValue: 64,
|
|
||||||
label: "R620-L1",
|
|
||||||
badgeLabel: "38/64 GB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 118,
|
|
||||||
maxValue: 512,
|
|
||||||
label: "R620-L3",
|
|
||||||
badgeLabel: "118/512 GB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 60,
|
|
||||||
maxValue: 1000,
|
|
||||||
label: "R620-L5",
|
|
||||||
badgeLabel: "60/1000 GB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 4,
|
|
||||||
maxValue: 32,
|
|
||||||
label: "R620-L4",
|
|
||||||
badgeLabel: "4/32 GB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 60,
|
|
||||||
maxValue: 1000,
|
|
||||||
label: "R620-L5",
|
|
||||||
badgeLabel: "60/1000 GB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 4,
|
|
||||||
maxValue: 32,
|
|
||||||
label: "R620-L4",
|
|
||||||
badgeLabel: "4/32 GB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 60,
|
|
||||||
maxValue: 1000,
|
|
||||||
label: "R620-L5",
|
|
||||||
badgeLabel: "60/1000 GB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 4,
|
|
||||||
maxValue: 32,
|
|
||||||
label: "R620-L4",
|
|
||||||
badgeLabel: "4/32 GB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 60,
|
|
||||||
maxValue: 1000,
|
|
||||||
label: "R620-L5",
|
|
||||||
badgeLabel: "60/1000 GB",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 4,
|
|
||||||
maxValue: 32,
|
|
||||||
label: "R620-L4",
|
|
||||||
badgeLabel: "4/32 GB",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
.pool-dashboard-view {
|
.pool-dashboard-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 2rem;
|
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import PoolHeader from "@/components/pool/PoolHeader.vue";
|
|||||||
import PoolTabBar from "@/components/pool/PoolTabBar.vue";
|
import PoolTabBar from "@/components/pool/PoolTabBar.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="postcss" scoped>
|
||||||
.view {
|
.view {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,23 @@
|
|||||||
<UiCard class="pool-vms-view">
|
<UiCard class="pool-vms-view">
|
||||||
<VmsActionsBar :selected-refs="selectedVmsRefs" />
|
<VmsActionsBar :selected-refs="selectedVmsRefs" />
|
||||||
<CollectionTable
|
<CollectionTable
|
||||||
:available-filters="filters"
|
|
||||||
:collection="vms"
|
|
||||||
v-model="selectedVmsRefs"
|
v-model="selectedVmsRefs"
|
||||||
|
:available-filters="filters"
|
||||||
|
:available-sorts="filters"
|
||||||
|
:collection="vms"
|
||||||
|
id-property="$ref"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<th>
|
<ColumnHeader :icon="faPowerOff" />
|
||||||
<FontAwesomeIcon :icon="faPowerOff" />
|
<ColumnHeader>Name</ColumnHeader>
|
||||||
</th>
|
<ColumnHeader>Description</ColumnHeader>
|
||||||
<th>Name</th>
|
|
||||||
</template>
|
</template>
|
||||||
<template #row="{ item: vm }">
|
<template #row="{ item: vm }">
|
||||||
<td>
|
<td>
|
||||||
<PowerStateIcon :state="vm.power_state" />
|
<PowerStateIcon :state="vm.power_state" />
|
||||||
</td>
|
</td>
|
||||||
<td>{{ vm.name_label }}</td>
|
<td>{{ vm.name_label }}</td>
|
||||||
|
<td>{{ vm.name_description }}</td>
|
||||||
</template>
|
</template>
|
||||||
</CollectionTable>
|
</CollectionTable>
|
||||||
</UiCard>
|
</UiCard>
|
||||||
@ -25,9 +27,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { AvailableFilter } from "@/types/filter";
|
import type { Filters } from "@/types/filter";
|
||||||
import { faPowerOff } from "@fortawesome/free-solid-svg-icons";
|
import { faPowerOff } from "@fortawesome/pro-solid-svg-icons";
|
||||||
import CollectionTable from "@/components/CollectionTable.vue";
|
import CollectionTable from "@/components/CollectionTable.vue";
|
||||||
|
import ColumnHeader from "@/components/ColumnHeader.vue";
|
||||||
import PowerStateIcon from "@/components/PowerStateIcon.vue";
|
import PowerStateIcon from "@/components/PowerStateIcon.vue";
|
||||||
import UiCard from "@/components/ui/UiCard.vue";
|
import UiCard from "@/components/ui/UiCard.vue";
|
||||||
import VmsActionsBar from "@/components/vm/VmsActionsBar.vue";
|
import VmsActionsBar from "@/components/vm/VmsActionsBar.vue";
|
||||||
@ -36,18 +39,18 @@ import { useVmStore } from "@/stores/vm.store";
|
|||||||
const vmStore = useVmStore();
|
const vmStore = useVmStore();
|
||||||
const { allRecords: vms } = storeToRefs(vmStore);
|
const { allRecords: vms } = storeToRefs(vmStore);
|
||||||
|
|
||||||
const filters: AvailableFilter[] = [
|
const filters: Filters = {
|
||||||
{ property: "name_label", label: "VM Name", type: "string" },
|
name_label: { label: "VM Name", type: "string" },
|
||||||
{
|
name_description: { label: "VM Description", type: "string" },
|
||||||
property: "power_state",
|
power_state: {
|
||||||
label: "VM State",
|
label: "VM State",
|
||||||
icon: faPowerOff,
|
icon: faPowerOff,
|
||||||
type: "enum",
|
type: "enum",
|
||||||
choices: ["Running", "Halted", "Paused"],
|
choices: ["Running", "Halted", "Paused"],
|
||||||
},
|
},
|
||||||
];
|
};
|
||||||
|
|
||||||
const selectedVmsRefs = ref([]);
|
const selectedVmsRefs = ref([]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style lang="postcss" scoped></style>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ES2017"],
|
"lib": ["ES2019"],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
|
Loading…
Reference in New Issue
Block a user