feat(lite/stories): needed components for incoming Component Stories (#6611)
This commit is contained in:
committed by
GitHub
parent
b42127f083
commit
bc0afb589e
@@ -24,12 +24,14 @@
|
||||
"d3-time-format": "^4.1.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"echarts": "^5.3.3",
|
||||
"highlight.js": "^11.6.0",
|
||||
"human-format": "^1.0.0",
|
||||
"json-rpc-2.0": "^1.3.0",
|
||||
"json5": "^2.2.1",
|
||||
"limit-concurrency-decorator": "^0.5.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"make-error": "^1.3.6",
|
||||
"markdown-it": "^13.0.1",
|
||||
"pinia": "^2.0.14",
|
||||
"placement.js": "^1.0.0-beta.5",
|
||||
"vue": "^3.2.37",
|
||||
|
||||
@@ -16,8 +16,8 @@ a {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
code, code * {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.card-view {
|
||||
|
||||
114
@xen-orchestra/lite/src/components/AppMarkdown.vue
Normal file
114
@xen-orchestra/lite/src/components/AppMarkdown.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div ref="rootElement" class="app-markdown" v-html="html" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type Ref, computed, ref } from "vue";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import { markdown } from "@/libs/markdown";
|
||||
|
||||
const rootElement = ref() as Ref<HTMLElement>;
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
}>();
|
||||
|
||||
const html = computed(() => markdown.render(props.content ?? ""));
|
||||
|
||||
useEventListener(
|
||||
rootElement,
|
||||
"click",
|
||||
(event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (!target.classList.contains("copy-button")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const copyable =
|
||||
target.parentElement!.querySelector<HTMLElement>(".copyable");
|
||||
|
||||
if (copyable !== null) {
|
||||
navigator.clipboard.writeText(copyable.innerText);
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.app-markdown {
|
||||
font-size: 1.6rem;
|
||||
|
||||
:deep() {
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1.4rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
code:not(.hljs-code) {
|
||||
background-color: var(--background-color-extra-blue);
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: revert;
|
||||
padding-left: 2rem;
|
||||
list-style-type: revert;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
thead th {
|
||||
border-bottom: 2px solid var(--color-blue-scale-400);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
}
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 1rem;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-extra-blue-d20);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
41
@xen-orchestra/lite/src/components/CodeHighlight.vue
Normal file
41
@xen-orchestra/lite/src/components/CodeHighlight.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<pre class="code-highlight hljs"><code v-html="codeAsHtml"></code></pre>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import HLJS from "highlight.js";
|
||||
import { computed } from "vue";
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
code?: any;
|
||||
lang?: string;
|
||||
}>(),
|
||||
{ lang: "typescript" }
|
||||
);
|
||||
|
||||
const codeAsText = computed(() => {
|
||||
switch (typeof props.code) {
|
||||
case "string":
|
||||
return props.code;
|
||||
case "function":
|
||||
return String(props.code);
|
||||
default:
|
||||
return JSON.stringify(props.code, undefined, 2);
|
||||
}
|
||||
});
|
||||
|
||||
const codeAsHtml = computed(
|
||||
() => HLJS.highlight(codeAsText.value, { language: props.lang }).value
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.code-highlight {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.6rem;
|
||||
text-align: left;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
</style>
|
||||
29
@xen-orchestra/lite/src/components/RouterTab.vue
Normal file
29
@xen-orchestra/lite/src/components/RouterTab.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<RouterLink
|
||||
v-slot="{ isActive, href }"
|
||||
:to="disabled || isTabBarDisabled ? '' : to"
|
||||
custom
|
||||
>
|
||||
<UiTab :active="isActive" :disabled="disabled" :href="href" tag="a">
|
||||
<slot />
|
||||
</UiTab>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, computed, inject } from "vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import UiTab from "@/components/ui/UiTab.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
to: RouteLocationRaw;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const isTabBarDisabled = inject<ComputedRef<boolean>>(
|
||||
"isTabBarDisabled",
|
||||
computed(() => false)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
@@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<span v-if="disabled" class="tab-bar-item disabled">
|
||||
<slot />
|
||||
</span>
|
||||
<RouterLink v-else class="tab-bar-item" v-bind="$props">
|
||||
<slot />
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RouterLinkProps } from "vue-router";
|
||||
|
||||
// https://vuejs.org/api/sfc-script-setup.html#type-only-props-emit-declarations
|
||||
interface Props extends RouterLinkProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.tab-bar-item {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1.2em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-blue-scale-100);
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--color-blue-scale-400);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@
|
||||
>
|
||||
<input
|
||||
v-model="value"
|
||||
:class="{ indeterminate: type === 'checkbox' && value === undefined }"
|
||||
:disabled="isLabelDisabled || disabled"
|
||||
:type="type === 'radio' ? 'radio' : 'checkbox'"
|
||||
class="input"
|
||||
@@ -32,7 +33,7 @@ import {
|
||||
inject,
|
||||
ref,
|
||||
} from "vue";
|
||||
import { faCheck, faCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheck, faCircle, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
@@ -53,7 +54,17 @@ const value = useVModel(props, "modelValue", emit);
|
||||
const type = inject<"checkbox" | "radio" | "toggle">("inputType", "checkbox");
|
||||
const hasLabel = inject("hasLabel", false);
|
||||
const isLabelDisabled = inject("isLabelDisabled", ref(false));
|
||||
const icon = computed(() => (type === "checkbox" ? faCheck : faCircle));
|
||||
const icon = computed(() => {
|
||||
if (type !== "checkbox") {
|
||||
return faCircle;
|
||||
}
|
||||
|
||||
if (value.value === undefined) {
|
||||
return faMinus;
|
||||
}
|
||||
|
||||
return faCheck;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
@@ -74,6 +85,11 @@ const icon = computed(() => (type === "checkbox" ? faCheck : faCircle));
|
||||
.form-checkbox {
|
||||
--checkbox-border-radius: 0.25em;
|
||||
--checkbox-icon-size: 1em;
|
||||
|
||||
.input.indeterminate + .fake-checkbox > .icon {
|
||||
opacity: 1;
|
||||
color: var(--color-blue-scale-300);
|
||||
}
|
||||
}
|
||||
|
||||
.form-checkbox,
|
||||
@@ -109,8 +125,8 @@ const icon = computed(() => (type === "checkbox" ? faCheck : faCircle));
|
||||
}
|
||||
|
||||
.icon {
|
||||
transform: translateX(-0.7em);
|
||||
transition: transform 0.125s ease-in-out;
|
||||
transform: translateX(-0.7em);
|
||||
}
|
||||
|
||||
.input:checked + .fake-checkbox > .icon {
|
||||
@@ -140,12 +156,12 @@ const icon = computed(() => (type === "checkbox" ? faCheck : faCircle));
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1.25em;
|
||||
transition: background-color 0.125s ease-in-out,
|
||||
border-color 0.125s ease-in-out;
|
||||
border: var(--checkbox-border-width) solid var(--border-color);
|
||||
border-radius: var(--checkbox-border-radius);
|
||||
background-color: var(--background-color);
|
||||
box-shadow: var(--shadow-100);
|
||||
transition: background-color 0.125s ease-in-out,
|
||||
border-color 0.125s ease-in-out;
|
||||
|
||||
--border-color: var(--color-blue-scale-400);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
<template>
|
||||
<span :class="wrapperClass" v-bind="wrapperAttrs">
|
||||
<input
|
||||
v-if="!isSelect"
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
:disabled="disabled || isLabelDisabled"
|
||||
class="input"
|
||||
ref="inputElement"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<template v-else>
|
||||
<template v-if="inputType === 'select'">
|
||||
<select
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
@@ -24,6 +15,24 @@
|
||||
<UiIcon :fixed-width="false" :icon="faAngleDown" />
|
||||
</span>
|
||||
</template>
|
||||
<textarea
|
||||
v-else-if="inputType === 'textarea'"
|
||||
ref="textarea"
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
:disabled="disabled || isLabelDisabled"
|
||||
class="textarea"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
v-model="value"
|
||||
:class="inputClass"
|
||||
:disabled="disabled || isLabelDisabled"
|
||||
class="input"
|
||||
ref="inputElement"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<span v-if="before !== undefined" class="before">
|
||||
<template v-if="typeof before === 'string'">{{ before }}</template>
|
||||
<UiIcon v-else :icon="before" class="before" />
|
||||
@@ -43,18 +52,19 @@ export default {
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isEmpty } from "lodash-es";
|
||||
import {
|
||||
type HTMLAttributes,
|
||||
type InputHTMLAttributes,
|
||||
computed,
|
||||
inject,
|
||||
nextTick,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import type { Color } from "@/types";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { useTextareaAutosize, useVModel } from "@vueuse/core";
|
||||
import UiIcon from "@/components/ui/UiIcon.vue";
|
||||
|
||||
// Temporary workaround for https://github.com/vuejs/core/issues/4294
|
||||
@@ -79,8 +89,10 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
const empty = computed(() => isEmpty(props.modelValue));
|
||||
const isSelect = inject("isSelect", false);
|
||||
const isEmpty = computed(
|
||||
() => props.modelValue == null || String(props.modelValue).trim() === ""
|
||||
);
|
||||
const inputType = inject("inputType", "input");
|
||||
const isLabelDisabled = inject("isLabelDisabled", ref(false));
|
||||
const color = inject(
|
||||
"color",
|
||||
@@ -88,10 +100,10 @@ const color = inject(
|
||||
);
|
||||
|
||||
const wrapperClass = computed(() => [
|
||||
isSelect ? "form-select" : "form-input",
|
||||
`form-${inputType}`,
|
||||
{
|
||||
disabled: props.disabled || isLabelDisabled.value,
|
||||
empty: empty.value,
|
||||
empty: isEmpty.value,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -104,6 +116,12 @@ const inputClass = computed(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const { textarea, triggerResize } = useTextareaAutosize();
|
||||
|
||||
watch(value, () => nextTick(() => triggerResize()), {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
const focus = () => inputElement.value.focus();
|
||||
|
||||
defineExpose({
|
||||
@@ -113,12 +131,13 @@ defineExpose({
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.form-input,
|
||||
.form-select {
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
display: inline-grid;
|
||||
align-items: stretch;
|
||||
|
||||
--before-width: v-bind('beforeWidth ?? "1.75em"');
|
||||
--after-width: v-bind('afterWidth ?? "1.625em"');
|
||||
--before-width: v-bind('beforeWidth || "1.75em"');
|
||||
--after-width: v-bind('afterWidth || "1.625em"');
|
||||
--caret-width: 1.5em;
|
||||
|
||||
--text-color: var(--color-blue-scale-100);
|
||||
@@ -132,7 +151,8 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.form-input {
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
grid-template-columns: var(--before-width) auto var(--after-width);
|
||||
}
|
||||
|
||||
@@ -145,8 +165,10 @@ defineExpose({
|
||||
}
|
||||
|
||||
.input,
|
||||
.textarea,
|
||||
.select {
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
@@ -235,8 +257,19 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
.textarea {
|
||||
height: auto;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 0 0.625em 0 0.625em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input,
|
||||
.textarea {
|
||||
padding-right: 0.625em;
|
||||
padding-left: 0.625em;
|
||||
|
||||
&.has-before {
|
||||
padding-left: calc(var(--before-width) + 0.25em);
|
||||
|
||||
93
@xen-orchestra/lite/src/components/form/FormJson.vue
Normal file
93
@xen-orchestra/lite/src/components/form/FormJson.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<UiModal
|
||||
@submit.prevent="saveJson"
|
||||
:color="isJsonValid ? 'success' : 'error'"
|
||||
v-if="isCodeModalOpen"
|
||||
:icon="faCode"
|
||||
@close="closeCodeModal"
|
||||
>
|
||||
<FormTextarea class="modal-textarea" v-model="editedJson" />
|
||||
<template #buttons>
|
||||
<UiButton transparent @click="formatJson">{{ $t("reformat") }}</UiButton>
|
||||
<UiButton outlined @click="closeCodeModal">{{ $t("cancel") }}</UiButton>
|
||||
<UiButton :disabled="!isJsonValid" type="submit"
|
||||
>{{ $t("save") }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiModal>
|
||||
<FormInput
|
||||
@click="openCodeModal"
|
||||
:model-value="jsonValue"
|
||||
:before="faCode"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormTextarea from "@/components/form/FormTextarea.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiModal from "@/components/ui/UiModal.vue";
|
||||
import useModal from "@/composables/modal.composable";
|
||||
import { faCode } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useVModel, whenever } from "@vueuse/core";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: any): void;
|
||||
}>();
|
||||
|
||||
const model = useVModel(props, "modelValue", emit);
|
||||
|
||||
const {
|
||||
open: openCodeModal,
|
||||
close: closeCodeModal,
|
||||
isOpen: isCodeModalOpen,
|
||||
} = useModal();
|
||||
|
||||
const jsonValue = computed(() => JSON.stringify(model.value, undefined, 2));
|
||||
|
||||
const isJsonValid = computed(() => {
|
||||
try {
|
||||
JSON.parse(editedJson.value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const formatJson = () => {
|
||||
if (!isJsonValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
editedJson.value = JSON.stringify(JSON.parse(editedJson.value), undefined, 2);
|
||||
};
|
||||
|
||||
const saveJson = () => {
|
||||
if (!isJsonValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
formatJson();
|
||||
|
||||
model.value = JSON.parse(editedJson.value);
|
||||
|
||||
closeCodeModal();
|
||||
};
|
||||
|
||||
whenever(isCodeModalOpen, () => (editedJson.value = jsonValue.value));
|
||||
|
||||
const editedJson = ref();
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
:deep(.modal-textarea) {
|
||||
min-width: 50rem;
|
||||
min-height: 20rem;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
import { provide } from "vue";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
|
||||
provide("isSelect", true);
|
||||
provide("inputType", "select");
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
||||
12
@xen-orchestra/lite/src/components/form/FormTextarea.vue
Normal file
12
@xen-orchestra/lite/src/components/form/FormTextarea.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<FormInput />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { provide } from "vue";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
|
||||
provide("inputType", "textarea");
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
@@ -1,67 +1,40 @@
|
||||
<template>
|
||||
<TabBar>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.dashboard', params: { uuid: poolUuid } }"
|
||||
>
|
||||
<UiTabBar :disabled="!isReady">
|
||||
<RouterTab :to="{ name: 'pool.dashboard', params: { uuid: poolUuid } }">
|
||||
{{ $t("dashboard") }}
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.alarms', params: { uuid: poolUuid } }"
|
||||
>
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.alarms', params: { uuid: poolUuid } }">
|
||||
{{ $t("alarms") }}
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.stats', params: { uuid: poolUuid } }"
|
||||
>
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.stats', params: { uuid: poolUuid } }">
|
||||
{{ $t("stats") }}
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.system', params: { uuid: poolUuid } }"
|
||||
>
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.system', params: { uuid: poolUuid } }">
|
||||
{{ $t("system") }}
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.network', params: { uuid: poolUuid } }"
|
||||
>
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.network', params: { uuid: poolUuid } }">
|
||||
{{ $t("network") }}
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.storage', params: { uuid: poolUuid } }"
|
||||
>
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.storage', params: { uuid: poolUuid } }">
|
||||
{{ $t("storage") }}
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.tasks', params: { uuid: poolUuid } }"
|
||||
>
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.tasks', params: { uuid: poolUuid } }">
|
||||
{{ $t("tasks") }}
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.hosts', params: { uuid: poolUuid } }"
|
||||
>
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.hosts', params: { uuid: poolUuid } }">
|
||||
{{ $t("hosts") }}
|
||||
</TabBarItem>
|
||||
<TabBarItem
|
||||
:disabled="!isReady"
|
||||
:to="{ name: 'pool.vms', params: { uuid: poolUuid } }"
|
||||
>
|
||||
</RouterTab>
|
||||
<RouterTab :to="{ name: 'pool.vms', params: { uuid: poolUuid } }">
|
||||
{{ $t("vms") }}
|
||||
</TabBarItem>
|
||||
</TabBar>
|
||||
</RouterTab>
|
||||
</UiTabBar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed } from "vue";
|
||||
import TabBar from "@/components/TabBar.vue";
|
||||
import TabBarItem from "@/components/TabBarItem.vue";
|
||||
import RouterTab from "@/components/RouterTab.vue";
|
||||
import UiTabBar from "@/components/ui/UiTabBar.vue";
|
||||
import { usePoolStore } from "@/stores/pool.store";
|
||||
|
||||
const poolStore = usePoolStore();
|
||||
|
||||
68
@xen-orchestra/lite/src/components/ui/UiTab.vue
Normal file
68
@xen-orchestra/lite/src/components/ui/UiTab.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<component
|
||||
:is="tag"
|
||||
:class="{ active, disabled: disabled || isTabBarDisabled }"
|
||||
class="ui-tab"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ComputedRef, computed, inject } from "vue";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
tag?: string;
|
||||
}>(),
|
||||
{ tag: "span" }
|
||||
);
|
||||
|
||||
const isTabBarDisabled = inject<ComputedRef<boolean>>(
|
||||
"isTabBarDisabled",
|
||||
computed(() => false)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-tab {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1.2em;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-blue-scale-100);
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--color-blue-scale-400);
|
||||
}
|
||||
|
||||
&:not(.disabled) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-extra-blue-base);
|
||||
border-bottom-color: var(--color-extra-blue-base);
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +1,29 @@
|
||||
<template>
|
||||
<div class="tab-bar">
|
||||
<div class="ui-tab-bar">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
provide(
|
||||
"isTabBarDisabled",
|
||||
computed(() => props.disabled)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.tab-bar {
|
||||
.ui-tab-bar {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 6.5rem;
|
||||
background-color: var(--background-color-primary);
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
background-color: var(--background-color-primary);
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
64
@xen-orchestra/lite/src/libs/markdown.ts
Normal file
64
@xen-orchestra/lite/src/libs/markdown.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import HLJS from "highlight.js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
export const markdown = new MarkdownIt();
|
||||
|
||||
markdown.set({
|
||||
highlight: (str: string, lang: string) => {
|
||||
const code = highlight(str, lang);
|
||||
return `<pre class="hljs"><button class="copy-button" type="button">Copy</button><code class="hljs-code">${code}</code></pre>`;
|
||||
},
|
||||
});
|
||||
|
||||
function highlight(str: string, lang: string) {
|
||||
switch (lang) {
|
||||
case "vue-template": {
|
||||
const indented = str
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((s) => ` ${s}`)
|
||||
.join("\n");
|
||||
return wrap(indented, "template");
|
||||
}
|
||||
case "vue-script":
|
||||
return wrap(str.trim(), "script");
|
||||
case "vue-style":
|
||||
return wrap(str.trim(), "style");
|
||||
default: {
|
||||
if (HLJS.getLanguage(lang) !== undefined) {
|
||||
return copyable(HLJS.highlight(str, { language: lang }).value);
|
||||
}
|
||||
|
||||
return copyable(markdown.utils.escapeHtml(str));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wrap(str: string, tag: "template" | "script" | "style") {
|
||||
let openTag;
|
||||
let code;
|
||||
|
||||
switch (tag) {
|
||||
case "template":
|
||||
openTag = "<template>";
|
||||
code = HLJS.highlight(str, { language: "xml" }).value;
|
||||
break;
|
||||
case "script":
|
||||
openTag = '<script lang="ts" setup>';
|
||||
code = HLJS.highlight(str, { language: "typescript" }).value;
|
||||
break;
|
||||
case "style":
|
||||
openTag = '<style lang="postcss" scoped>';
|
||||
code = HLJS.highlight(str, { language: "scss" }).value;
|
||||
break;
|
||||
}
|
||||
|
||||
const openTagHtml = HLJS.highlight(openTag, { language: "xml" }).value;
|
||||
const closeTagHtml = HLJS.highlight(`</${tag}>`, { language: "xml" }).value;
|
||||
|
||||
return `${openTagHtml}${copyable(code)}${closeTagHtml}`;
|
||||
}
|
||||
|
||||
function copyable(code: string) {
|
||||
return `<div class="copyable">${code}</div>`;
|
||||
}
|
||||
@@ -55,6 +55,7 @@
|
||||
"property": "Property",
|
||||
"ram-usage": "RAM usage",
|
||||
"reboot": "Reboot",
|
||||
"reformat": "Reformat",
|
||||
"relative-time": {
|
||||
"day": "1 day | {n} days",
|
||||
"future": "In {str}",
|
||||
@@ -67,6 +68,7 @@
|
||||
"year": "1 year | {n} years"
|
||||
},
|
||||
"resume": "Resume",
|
||||
"save": "Save",
|
||||
"send-us-feedback": "Send us feedback",
|
||||
"settings": "Settings",
|
||||
"shutdown": "Shutdown",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"property": "Propriété",
|
||||
"ram-usage": "Utilisation de la RAM",
|
||||
"reboot": "Redémarrer",
|
||||
"reformat": "Reformater",
|
||||
"relative-time": {
|
||||
"day": "1 jour | {n} jours",
|
||||
"future": "Dans {str}",
|
||||
@@ -67,6 +68,7 @@
|
||||
"year": "1 an | {n} ans"
|
||||
},
|
||||
"resume": "Reprendre",
|
||||
"save": "Enregistrer",
|
||||
"send-us-feedback": "Envoyez-nous vos commentaires",
|
||||
"settings": "Paramètres",
|
||||
"shutdown": "Arrêter",
|
||||
|
||||
28
yarn.lock
28
yarn.lock
@@ -7926,6 +7926,11 @@ entities@~1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
|
||||
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
|
||||
|
||||
entities@~3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
|
||||
integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
|
||||
|
||||
envify@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/envify/-/envify-4.1.0.tgz#f39ad3db9d6801b4e6b478b61028d3f0b6819f7e"
|
||||
@@ -10507,6 +10512,11 @@ highland@^2.11.1:
|
||||
dependencies:
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
highlight.js@^11.6.0:
|
||||
version "11.6.0"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.6.0.tgz#a50e9da05763f1bb0c1322c8f4f755242cff3f5a"
|
||||
integrity sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw==
|
||||
|
||||
highlight.js@^9.7.0:
|
||||
version "9.18.5"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825"
|
||||
@@ -12857,6 +12867,13 @@ linkify-it@^2.0.0:
|
||||
dependencies:
|
||||
uc.micro "^1.0.1"
|
||||
|
||||
linkify-it@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec"
|
||||
integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==
|
||||
dependencies:
|
||||
uc.micro "^1.0.1"
|
||||
|
||||
lint-staged@^13.0.3:
|
||||
version "13.1.0"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.1.0.tgz#d4c61aec939e789e489fa51987ec5207b50fd37e"
|
||||
@@ -13388,6 +13405,17 @@ markdown-it-table-of-contents@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz#3dc7ce8b8fc17e5981c77cc398d1782319f37fbc"
|
||||
integrity sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==
|
||||
|
||||
markdown-it@^13.0.1:
|
||||
version "13.0.1"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430"
|
||||
integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
entities "~3.0.1"
|
||||
linkify-it "^4.0.1"
|
||||
mdurl "^1.0.1"
|
||||
uc.micro "^1.0.5"
|
||||
|
||||
markdown-it@^8.4.1:
|
||||
version "8.4.2"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"
|
||||
|
||||
Reference in New Issue
Block a user