Skip to content

Commit 5a2fedb

Browse files
committed
feat: vpn deploy view, detail view wip
1 parent 62b9a31 commit 5a2fedb

7 files changed

Lines changed: 567 additions & 3 deletions

File tree

src/layout/EditableTitle.vue

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<script setup>
2+
import LoadingSpinner from '@/components/icons/LoadingSpinner.vue'
3+
import ValidationError from '@/components/ValidationError.vue'
4+
import { required } from '@vuelidate/validators'
5+
import useVuelidate from '@vuelidate/core'
6+
import { CheckIcon, PencilIcon, XIcon } from '@heroicons/vue/outline'
7+
import { computed, defineEmits, defineProps, reactive, ref, watchEffect } from 'vue'
8+
9+
const emit = defineEmits(['change'])
10+
11+
const props = defineProps({
12+
busy: Boolean,
13+
disabled: Boolean,
14+
placeholder: String,
15+
validation: [Function, Object],
16+
value: String
17+
})
18+
19+
const formState = reactive({
20+
newValue: ''
21+
})
22+
23+
const v$ = useVuelidate({ newValue: props.validation || { required } }, formState)
24+
25+
const canSave = computed(() => !props.disabled && !v$.value.$invalid)
26+
const editing = ref(false)
27+
const inputRef = ref(null)
28+
29+
function cancelEdit() {
30+
editing.value = false
31+
reset()
32+
}
33+
34+
function reset() {
35+
v$.value.newValue.$model = props.value || ''
36+
v$.value.$reset()
37+
}
38+
39+
function startEdit() {
40+
reset()
41+
editing.value = true
42+
}
43+
44+
async function stopEdit() {
45+
if (!await v$.value.$validate()) {
46+
console.log(v$.value.$errors)
47+
return
48+
}
49+
50+
emit('change', v$.value.newValue.$model)
51+
52+
editing.value = false
53+
}
54+
55+
watchEffect(() => {
56+
if (inputRef.value) {
57+
inputRef.value.focus()
58+
}
59+
})
60+
</script>
61+
62+
<template>
63+
<div class="editable-title__container flex">
64+
<div v-if="editing">
65+
<input
66+
v-model="v$.newValue.$model"
67+
@keypress.enter="stopEdit"
68+
class="editable-title__value"
69+
:placeholder="placeholder"
70+
ref="inputRef"
71+
type="text"
72+
/>
73+
<ValidationError :errors="v$.newValue.$errors" />
74+
</div>
75+
<h1 v-else class="w-max mb-0">{{ busy ? v$.newValue.$model : value }}</h1>
76+
77+
<div class="mt-3">
78+
<button v-if="busy" class="ml-2" disabled>
79+
<LoadingSpinner />
80+
</button>
81+
<button v-else-if="editing" class="ml-2" @click="stopEdit" :disabled="!canSave">
82+
<CheckIcon class="button__icon text-green hover:text-green-600" />
83+
</button>
84+
<button v-else class="ml-2" @click="startEdit" :disabled="disabled">
85+
<PencilIcon class="button__icon text-gray-400 hover:text-green" />
86+
</button>
87+
88+
<button v-if="!busy && editing" @click="cancelEdit" class="ml-2">
89+
<XIcon class="button__icon text-red hover:text-red-600" />
90+
</button>
91+
</div>
92+
</div>
93+
</template>
94+
95+
<style>
96+
.editable-title__value {
97+
@apply bg-transparent text-3xl text-gray-600 border-b border-gray-400 w-full;
98+
@apply focus:outline-none focus:border-green focus:text-green;
99+
}
100+
101+
.editable-title__container .button__icon {
102+
@apply w-5 ml-1;
103+
}
104+
105+
.editable-title__container button:disabled,
106+
.editable-title__container button:disabled .button__icon {
107+
@apply text-gray-400 hover:no-underline;
108+
}
109+
</style>

src/layout/Slider.vue

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<script setup>
2+
import 'vue-slider-component/theme/antd.css'
3+
import VueSlider from 'vue-slider-component'
4+
import { defineEmits, defineModel, defineProps } from 'vue'
5+
6+
defineProps({
7+
disabled: Boolean,
8+
formatter: Function,
9+
marks: Object,
10+
max: Number,
11+
min: Number,
12+
title: String,
13+
tooltip: String
14+
})
15+
16+
const emit = defineEmits(['change'])
17+
18+
const value = defineModel()
19+
</script>
20+
21+
<template>
22+
<div class="slider__container" :class="{ disabled }">
23+
<span v-if="title" class="slider__title">{{ title }}</span>
24+
<vue-slider
25+
:contained="true"
26+
:disabled="disabled"
27+
:dot-style="{ background: '#4ecd5f', boxShadow: '0 0 2px 1px #eee', border: 'none' }"
28+
:dotSize="20"
29+
:label-style="{ color: '#999', fontSize: '12px' }"
30+
:marks="marks"
31+
:max="max"
32+
:min="min"
33+
:process-style="{ background: '#4ecd5f' }"
34+
:step-active-style="{ background: '#fff', opacity: '1', border: 'none', boxShadow: 'rgba(0, 0, 0, 0.24) 0px 3px 8px' }"
35+
:tooltip="tooltip || 'hover'"
36+
:tooltip-formatter="formatter"
37+
:tooltip-style="{ background: '#4ecd5f', borderColor: '#4ecd5f' }"
38+
adsorb
39+
tooltipPlacement="top"
40+
v-model=value
41+
width="100%"
42+
@change="val => emit('change', val)"
43+
/>
44+
</div>
45+
</template>
46+
47+
<style>
48+
.slider__container {
49+
@apply relative flex space-x-3 items-start justify-center pr-5 pl-2 pt-14 pb-8 border border-gray-300 rounded-md;
50+
}
51+
52+
.slider__container.disabled {
53+
@apply cursor-not-allowed opacity-50;
54+
}
55+
56+
.slider__title {
57+
@apply absolute top-0 inline-block px-3 text-gray-500 transform -translate-y-1/2 bg-white;
58+
}
59+
60+
</style>

0 commit comments

Comments
 (0)