Initial commit
This commit is contained in:
102
proxy-ui-app/src/components/1_atoms/DoubleSwitch.vue
Normal file
102
proxy-ui-app/src/components/1_atoms/DoubleSwitch.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'DoubleSwitch',
|
||||
components: {
|
||||
// Button,
|
||||
},
|
||||
props: {
|
||||
machine: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
firstTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
secondTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
firstColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
secondColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isCheck: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
position: {
|
||||
type: String,
|
||||
default: 'row'
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
switchClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
emits: ['switched'],
|
||||
data() {
|
||||
return {
|
||||
isChecked: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
watch: {
|
||||
isCheck: function () {
|
||||
this.isChecked = !this.isChecked
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
|
||||
},
|
||||
methods: {
|
||||
buttonClass: function(value) {
|
||||
return `border border-slate-400 flex items-center justify-center cursor-pointer rounded transition-all text-xs text-center py-[5px] px-2 ${value}`
|
||||
},
|
||||
setChecked(e) {
|
||||
this.$emit('switched', e.target.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label :class="`${position === 'row' ? 'flex-row' : 'flex-col'} ${labelClass} relative flex items-center cursor-pointer`">
|
||||
<input
|
||||
v-bind="$attrs"
|
||||
class="input sr-only peer/checkbox"
|
||||
type="checkbox"
|
||||
:checked="isCheck"
|
||||
@change="setChecked"
|
||||
>
|
||||
<span
|
||||
v-if="position === 'col'"
|
||||
class="block peer-checked/checkbox:hidden ml-0 text-xs font-medium text-gray-900 dark:text-gray-300"
|
||||
> {{ firstTitle }}</span>
|
||||
<span
|
||||
v-if="position === 'col'"
|
||||
class="hidden peer-checked/checkbox:block ml-0 text-xs font-medium text-gray-900 dark:text-gray-300"
|
||||
> {{ secondTitle }} </span>
|
||||
<div
|
||||
:class="`${switchClass} relative w-11 h-6 rounded-full peer peer-focus:ring-4 peer-focus:ring-${secondColor}-300 dark:peer-focus:ring-${secondColor}-800 dark:bg-${firstColor}-700 peer-checked/checkbox:after:translate-x-full peer-checked/checkbox:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-${firstColor}-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-${firstColor}-600`"
|
||||
:style="{background: !isCheck ? firstColor : secondColor}"
|
||||
/>
|
||||
<span
|
||||
v-if="position === 'row'"
|
||||
class="block peer-checked/checkbox:hidden ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
> {{ firstTitle }}</span>
|
||||
<span
|
||||
v-if="position === 'row'"
|
||||
class="hidden peer-checked/checkbox:block ml-2 text-sm font-medium text-gray-900 dark:text-gray-300"
|
||||
> {{ secondTitle }} </span>
|
||||
</label>
|
||||
</template>
|
||||
59
proxy-ui-app/src/components/1_atoms/Input.vue
Normal file
59
proxy-ui-app/src/components/1_atoms/Input.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Input',
|
||||
props: {
|
||||
id: {
|
||||
default: 'input',
|
||||
type: String
|
||||
},
|
||||
type: {
|
||||
default: 'text',
|
||||
type: String
|
||||
},
|
||||
name: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
inputClass: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
placeholder: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
required: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
},
|
||||
value: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
onChange: {
|
||||
default: () => {},
|
||||
type: Function
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
setValue: function (e) {
|
||||
this.onChange({key: this.name, value: e.target.value})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
:id="id"
|
||||
:type="type"
|
||||
:name="name"
|
||||
:class="`flex w-full rounded-lg border-zinc-300 py-1 px-2 text-zinc-900 focus:outline-none focus:ring-4 text-sm sm:leading-6 border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5 ${inputClass}`"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
:value="value"
|
||||
:on:change="setValue"
|
||||
>
|
||||
</template>
|
||||
36
proxy-ui-app/src/components/1_atoms/PageHeader.vue
Normal file
36
proxy-ui-app/src/components/1_atoms/PageHeader.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import {mapMutations, mapGetters} from 'vuex'
|
||||
export default {
|
||||
name: 'PageHeader',
|
||||
computed: {
|
||||
...mapGetters('proxy', ['selectedSite']),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('proxy', ['setIsSaveData', 'setIsDeleteData']),
|
||||
saveData: function () {
|
||||
console.log('saveData')
|
||||
return this.setIsSaveData(true)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex">
|
||||
<a
|
||||
href="/#"
|
||||
class="text-white block w-fit bg-slate-700 hover:bg-blue-600 focus:outline-none focus:ring-4 focus:ring-blue-300 font-medium rounded-full text-sm text-center px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
>
|
||||
<i class="ri-reply-line" />
|
||||
На главную
|
||||
</a>
|
||||
<button
|
||||
:class="`w-fit text-white font-medium rounded-full text-sm px-5 py-2.5 text-center me-2 mx-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 ${!selectedSite ? 'bg-blue-200' : 'bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300'}`"
|
||||
:disabled="!selectedSite"
|
||||
@click="saveData"
|
||||
>
|
||||
<i class="ri-save-line cursor-pointer" />
|
||||
Сохранить изменения
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
69
proxy-ui-app/src/components/1_atoms/Textarea.vue
Normal file
69
proxy-ui-app/src/components/1_atoms/Textarea.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Textarea',
|
||||
props: {
|
||||
id: {
|
||||
default: 'input',
|
||||
type: String
|
||||
},
|
||||
type: {
|
||||
default: 'text',
|
||||
type: String
|
||||
},
|
||||
name: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
textareaClass: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
placeholder: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
required: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
},
|
||||
value: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
onChange: {
|
||||
default: () => {},
|
||||
type: Function
|
||||
},
|
||||
focus: {
|
||||
default: () => {},
|
||||
type: Function
|
||||
},
|
||||
blur: {
|
||||
default: () => {},
|
||||
type: Function
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
setValue: function (e) {
|
||||
this.onChange({key: this.name, value: e.target.value})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea
|
||||
:id="id"
|
||||
:type="type"
|
||||
:name="name"
|
||||
:class="`border p-2.5 w-full text-sm bg-gray-50 rounded-lg border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 ${textareaClass}`"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
:value="value"
|
||||
:on:change="setValue"
|
||||
@focus="focus"
|
||||
@blur="blur"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script>
|
||||
import {mapActions} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Control',
|
||||
props: { },
|
||||
methods: {
|
||||
...mapActions('proxy', ["breakSavingSite", "addNewRouteLayout"]),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-4 flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
class="w-fit text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 font-medium rounded-full text-sm px-5 py-2.5 text-center me-2 mr-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
@click="addNewRouteLayout"
|
||||
>
|
||||
Добавить роут
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-fit text-white bg-slate-700 hover:bg-slate-800 focus:outline-none focus:ring-4 focus:ring-slate-300 font-medium rounded-full text-sm px-5 py-2.5 text-center me-2 dark:bg-slate-600 dark:hover:bg-slate-700 dark:focus:ring-slate-800"
|
||||
@click="breakSavingSite"
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,306 @@
|
||||
<script>
|
||||
import {mapActions, mapGetters} from 'vuex'
|
||||
import { FwbSpinner } from 'flowbite-vue'
|
||||
import Input from '@atoms/Input.vue'
|
||||
import DoubleSwitch from '@atoms/DoubleSwitch.vue'
|
||||
import Textarea from '@atoms/Textarea.vue'
|
||||
|
||||
export default {
|
||||
name: 'RouterRow',
|
||||
components: {Input, Textarea, DoubleSwitch, FwbSpinner},
|
||||
props: {
|
||||
id: {
|
||||
default: -1,
|
||||
type: Number
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timer: null,
|
||||
state: "active",
|
||||
textAreaRows: 1,
|
||||
isRemoving: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('proxy', ["selectedSite", "routes", 'routeOptions', 'routesLib']),
|
||||
path() {
|
||||
return this.routesLib[this.id]?.path || ""
|
||||
},
|
||||
role() {
|
||||
return this.routesLib[this.id]?.role || ""
|
||||
},
|
||||
description() {
|
||||
return this.routesLib[this.id]?.description || ""
|
||||
},
|
||||
errorPercent() {
|
||||
if (!this.routesLib[this.id]?.cb_error_threshold_percentage && this.routesLib[this.id]?.cb_error_threshold_percentage !== 0) return ""
|
||||
return this.routesLib[this.id]?.cb_error_threshold_percentage
|
||||
},
|
||||
intervalDuration() {
|
||||
if (!this.routesLib[this.id]?.cb_interval_duration && this.routesLib[this.id]?.cb_interval_duration !== 0) return ""
|
||||
return this.routesLib[this.id]?.cb_interval_duration
|
||||
},
|
||||
minRequests() {
|
||||
if (!this.routesLib[this.id]?.cb_min_requests && this.routesLib[this.id]?.cb_min_requests !== 0) return ""
|
||||
return this.routesLib[this.id]?.cb_min_requests
|
||||
},
|
||||
openStateTimeout() {
|
||||
if (!this.routesLib[this.id]?.cb_open_state_timeout && this.routesLib[this.id]?.cb_open_state_timeout !== 0) return ""
|
||||
return this.routesLib[this.id]?.cb_open_state_timeout
|
||||
},
|
||||
requestLimit() {
|
||||
if (!this.routesLib[this.id]?.cb_request_limit && this.routesLib[this.id]?.cb_request_limit !== 0) return ""
|
||||
return this.routesLib[this.id]?.cb_request_limit
|
||||
},
|
||||
deepness() {
|
||||
if (!this.routesLib[this.id]?.deepness && this.routesLib[this.id]?.deepness !== 0) return ""
|
||||
return this.routesLib[this.id]?.deepness
|
||||
},
|
||||
order() {
|
||||
if (!this.routesLib[this.id]?.order && this.routesLib[this.id]?.order !== 0) return ""
|
||||
return this.routesLib[this.id]?.order
|
||||
},
|
||||
isCbOn() {
|
||||
return this.routesLib[this.id]?.is_cb_on
|
||||
},
|
||||
isOnline() {
|
||||
return this.routesLib[this.id]?.is_online
|
||||
},
|
||||
|
||||
},
|
||||
methods: {
|
||||
...mapActions('proxy', ["updateRouteRow", "removeRoute", "breateAddingRoute"]),
|
||||
toggle() {
|
||||
this.open = !this.open
|
||||
this.onToggle({isOpen: this.open})
|
||||
},
|
||||
setValue(key, value) {
|
||||
this.updateRouteRow({
|
||||
...this.routesLib[this.id],
|
||||
[key]: value
|
||||
})
|
||||
},
|
||||
setInputValue(params) {
|
||||
const value = params.isNumber ? parseFloat(params.value) : params.value
|
||||
this.updateRouteRow({
|
||||
...this.routesLib[this.id],
|
||||
[params.key]: value
|
||||
})
|
||||
},
|
||||
removeCurrentRoute() {
|
||||
this.removeRoute({id: this.id})
|
||||
},
|
||||
expandTextArea() {
|
||||
this.textAreaRows = 8
|
||||
},
|
||||
decreaseTextArea() {
|
||||
this.textAreaRows = 1
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex flex-row gap-2 mb-2 items-end">
|
||||
<div class="w-fit">
|
||||
<DoubleSwitch
|
||||
name="isCbOn"
|
||||
firstColor="#8b5cf6"
|
||||
secondColor="#2563eb"
|
||||
firstTitle="Без защиты"
|
||||
secondTitle="Защита"
|
||||
:isCheck="isCbOn"
|
||||
position="col"
|
||||
labelClass="pb-2"
|
||||
switchClass="mt-2.5"
|
||||
@switched="(v) => setValue('is_cb_on', v)"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-fit">
|
||||
<DoubleSwitch
|
||||
name="isOnline"
|
||||
firstColor="#8b5cf6"
|
||||
secondColor="#2563eb"
|
||||
firstTitle="Офлайн"
|
||||
secondTitle="Онлайн"
|
||||
:isCheck="isOnline"
|
||||
position="col"
|
||||
labelClass="pb-2"
|
||||
switchClass="mt-2.5"
|
||||
@switched="(v) => setValue('is_online', v)"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="mr-2 mb-1 min-w-[80px] text-xs">
|
||||
Путь
|
||||
</div>
|
||||
<Input
|
||||
name="path"
|
||||
:value="path"
|
||||
inputClass="!w-[100%] py-1.5"
|
||||
placeholder="Указать путь"
|
||||
:onChange="setInputValue"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-fit min-w-[70px]">
|
||||
<div class="mr-2 mb-1 text-xs">
|
||||
Роль
|
||||
</div>
|
||||
<select
|
||||
:value="role"
|
||||
class="w-full rounded-lg border-gray-300 py-1.5 px-2"
|
||||
@change="(e) => setValue('role', parseFloat(e.target.value))"
|
||||
>
|
||||
<option
|
||||
v-for="(el, idx) in routeOptions"
|
||||
:key="{idx}"
|
||||
:value="el.value"
|
||||
>
|
||||
{{ el.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="w-full translate-y-1.5">
|
||||
<div class="mr-2 mb-1 max-w-[80px] text-xs">
|
||||
Описание
|
||||
</div>
|
||||
<Textarea
|
||||
name="description"
|
||||
:value="description"
|
||||
textareaClass="py-2 resize-none "
|
||||
placeholder="Описание..."
|
||||
:rows="textAreaRows"
|
||||
:focus="expandTextArea"
|
||||
:blur="decreaseTextArea"
|
||||
:onChange="setInputValue"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-fit min-w-[80px]">
|
||||
<div class="mr-2 mb-1 text-xs">
|
||||
Ошибка, %
|
||||
</div>
|
||||
<Input
|
||||
name="cb_error_threshold_percentage"
|
||||
:value="`${errorPercent}`"
|
||||
inputClass="!w-[100%] py-1.5"
|
||||
placeholder="Процент ошибки"
|
||||
:onChange="(v) => setInputValue({...v, isNumber: true})"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-fit min-w-[80px]">
|
||||
<div class="mr-2 mb-1 text-xs">
|
||||
Интервал
|
||||
</div>
|
||||
<Input
|
||||
name="cb_interval_duration"
|
||||
:value="`${intervalDuration}`"
|
||||
inputClass="!w-[100%] py-1.5"
|
||||
placeholder="Интервал"
|
||||
:onChange="(v) => setInputValue({...v, isNumber: true})"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-fit min-w-[80px]">
|
||||
<div class="mr-2 mb-1 text-xs">
|
||||
Кол-во запросов
|
||||
</div>
|
||||
<Input
|
||||
name="cb_min_requests"
|
||||
:value="`${minRequests}`"
|
||||
inputClass="!w-[100%] py-1.5"
|
||||
placeholder="Кол-во запросов"
|
||||
:onChange="(v) => setInputValue({...v, isNumber: true})"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-fit min-w-[80px]">
|
||||
<div class="mr-2 mb-1 text-xs">
|
||||
Тайм-аут состояния
|
||||
</div>
|
||||
<Input
|
||||
name="cb_open_state_timeout"
|
||||
:value="`${openStateTimeout}`"
|
||||
inputClass="!w-[100%] py-1.5"
|
||||
placeholder="Тайм-аут состояния"
|
||||
:onChange="(v) => setInputValue({...v, isNumber: true})"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-fit min-w-[80px]">
|
||||
<div class="mr-2 mb-1 text-xs">
|
||||
Лимит запросов
|
||||
</div>
|
||||
<Input
|
||||
name="cb_request_limit"
|
||||
:value="`${requestLimit}`"
|
||||
inputClass="!w-[100%] py-1.5"
|
||||
placeholder="Лимит запросов"
|
||||
:onChange="(v) => setInputValue({...v, isNumber: true})"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-fit min-w-[80px]">
|
||||
<div class="mr-2 mb-1 text-xs">
|
||||
Глубина запросов
|
||||
</div>
|
||||
<Input
|
||||
name="deepness"
|
||||
:value="`${deepness}`"
|
||||
inputClass="!w-[100%] py-1.5"
|
||||
placeholder="Глубина запросов"
|
||||
:onChange="(v) => setInputValue({...v, isNumber: true})"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-fit min-w-[80px]">
|
||||
<div class="mr-2 mb-1 text-xs">
|
||||
Порядок сортировки
|
||||
</div>
|
||||
<Input
|
||||
name="order"
|
||||
:value="`${order}`"
|
||||
inputClass="!w-[100%] py-1.5"
|
||||
placeholder="Порядок сортировки"
|
||||
:onChange="(v) => setInputValue({...v, isNumber: true})"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center col-span-1">
|
||||
<div
|
||||
v-if="!isRemoving"
|
||||
class=" transition-all h-9 w-9 mr-4 rounded-md bg-red-700 hover:bg-red-600 cursor-pointer text-white flex items-center justify-center"
|
||||
@click="() => isRemoving = true"
|
||||
>
|
||||
<i class="ri-delete-bin-line" />
|
||||
</div>
|
||||
<div
|
||||
v-if="isRemoving"
|
||||
class=" transition-all h-9 w-24 mr-4 rounded-md bg-gray-700 hover:bg-gray-600 cursor-pointer text-white flex items-center justify-center"
|
||||
@click="() => isRemoving = false"
|
||||
>
|
||||
Отменить
|
||||
</div>
|
||||
<div
|
||||
v-if="isRemoving"
|
||||
class=" transition-all h-9 w-24 mr-4 rounded-md bg-red-700 hover:bg-red-600 cursor-pointer text-white flex items-center justify-center"
|
||||
@click="removeCurrentRoute({id})"
|
||||
>
|
||||
Удалить
|
||||
</div>
|
||||
<div
|
||||
v-if="timer"
|
||||
class="flex items-center opacity-75"
|
||||
>
|
||||
<span class="mr-2 font-w-700 text-slate-900">Сохранение</span>
|
||||
<fwb-spinner
|
||||
v-if="timer"
|
||||
size="6"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="state === 'success'"
|
||||
class="flex items-center "
|
||||
>
|
||||
<span class="mr-2 text-slate-700 font-w-700">Сохранено</span>
|
||||
<i
|
||||
class="ri-check-line text-green-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script>
|
||||
import { FwbSpinner } from 'flowbite-vue'
|
||||
import { mapGetters} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'RoutesLoader',
|
||||
components: {FwbSpinner},
|
||||
computed: {
|
||||
...mapGetters('proxy', ["routesState"]),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="routesState === 'loading'"
|
||||
class="w-full flex justify-center items-center"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<fwb-spinner
|
||||
size="8"
|
||||
class="mr-2"
|
||||
/> Загрузка...
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script>
|
||||
import {mapGetters} from 'vuex'
|
||||
import Control from './Controll.vue'
|
||||
import RoutesLoader from './RoutesLoader.vue'
|
||||
|
||||
export default {
|
||||
name: 'RoutesList',
|
||||
components: {RoutesLoader, Control},
|
||||
props: {
|
||||
name: {
|
||||
default: "",
|
||||
type: String
|
||||
},
|
||||
port: {
|
||||
default: "",
|
||||
type: String
|
||||
},
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
open: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('proxy', ['routesState']),
|
||||
},
|
||||
watch: {
|
||||
routesState: function (newVal) {
|
||||
this.open = newVal === 'active' ? true : false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggle () {
|
||||
this.open = !this.open
|
||||
this.onToggle({isOpen: this.open})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="open"
|
||||
class="flex flex-col w-full"
|
||||
>
|
||||
<Control />
|
||||
<slot />
|
||||
</div>
|
||||
<RoutesLoader />
|
||||
</template>
|
||||
108
proxy-ui-app/src/components/3_organisms/SitesEditor/EditCard.vue
Normal file
108
proxy-ui-app/src/components/3_organisms/SitesEditor/EditCard.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<!-- eslint-disable vue/prop-name-casing -->
|
||||
<script>
|
||||
import {mapActions, mapGetters} from 'vuex'
|
||||
import Input from '@atoms/Input.vue'
|
||||
import Textarea from '@atoms/Textarea.vue'
|
||||
import DoubleSwitch from '@atoms/DoubleSwitch.vue'
|
||||
|
||||
export default {
|
||||
name: 'EditCard',
|
||||
components: {Input, Textarea, DoubleSwitch},
|
||||
props: {
|
||||
id: {
|
||||
default: -1,
|
||||
type: Number
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('proxy', ["selectedSite"]),
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
...mapActions('proxy', ["editSelectedSite", "breakSavingSite", "breakeAddingSite"]),
|
||||
editData (params) {
|
||||
this.editSelectedSite(params)
|
||||
},
|
||||
breakSavingSiteDate() {
|
||||
if (this.id == -1) {
|
||||
this.breakeAddingSite()
|
||||
} else {
|
||||
this.editable_name = this.name
|
||||
this.editable_port = this.port
|
||||
this.breakSavingSite()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="w-fit mr-2">
|
||||
<DoubleSwitch
|
||||
name="isCbOn"
|
||||
firstColor="#8b5cf6"
|
||||
secondColor="#2563eb"
|
||||
firstTitle="Офлайн"
|
||||
secondTitle="Онлайн"
|
||||
:isCheck="selectedSite.is_online"
|
||||
position="col"
|
||||
labelClass="items-start pb-2"
|
||||
switchClass="mt-1"
|
||||
@switched="(e) => editData({key: 'is_online', value: e})"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
name="name"
|
||||
:value="selectedSite.name"
|
||||
inputClass="!w-[90%] py-2"
|
||||
placeholder="Указать путь"
|
||||
:onChange="editData"
|
||||
/>
|
||||
<div>
|
||||
<i
|
||||
v-tippy="{ content: 'закрыть сервис' }"
|
||||
class="ri-close-line ml-2 cursor-pointer"
|
||||
@click="breakSavingSiteDate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center mb-2 w-full">
|
||||
<span class="mr-2 min-w-[80px]">Порт:</span>
|
||||
<Input
|
||||
name="port"
|
||||
:value="`${selectedSite.port}`"
|
||||
inputClass="py-2"
|
||||
placeholder="Порт"
|
||||
:onChange="editData"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center mb-2 w-full">
|
||||
<span class="mr-2 min-w-[80px]">IP proxy:</span>
|
||||
<Input
|
||||
name="proxy_ip"
|
||||
:value="selectedSite.proxy_ip"
|
||||
inputClass="py-2"
|
||||
placeholder="IP proxy"
|
||||
:onChange="editData"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center w-full mb-2">
|
||||
<span class="mr-2 min-w-[80px]">IP устр-ва:</span>
|
||||
<Input
|
||||
name="device_ip"
|
||||
:value="selectedSite.device_ip"
|
||||
inputClass="py-2"
|
||||
placeholder="IP устройства"
|
||||
:onChange="editData"
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
name="description"
|
||||
:value="selectedSite.description"
|
||||
textareaClass="py-2"
|
||||
placeholder="Описание..."
|
||||
:onChange="editData"
|
||||
/>
|
||||
</template>
|
||||
165
proxy-ui-app/src/components/3_organisms/SitesEditor/SiteCard.vue
Normal file
165
proxy-ui-app/src/components/3_organisms/SitesEditor/SiteCard.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<!-- eslint-disable vue/prop-name-casing -->
|
||||
<script>
|
||||
import {mapActions, mapGetters} from 'vuex'
|
||||
import EditCard from './EditCard.vue'
|
||||
|
||||
export default {
|
||||
name: 'SiteCard',
|
||||
components: {EditCard},
|
||||
props: {
|
||||
id: {
|
||||
default: -1,
|
||||
type: Number
|
||||
},
|
||||
name: {
|
||||
default: "",
|
||||
type: String
|
||||
},
|
||||
port: {
|
||||
default: "",
|
||||
type: String
|
||||
},
|
||||
device_ip: {
|
||||
default: "",
|
||||
type: String
|
||||
},
|
||||
proxy_ip: {
|
||||
default: "",
|
||||
type: String
|
||||
},
|
||||
status: {
|
||||
default: "disable",
|
||||
type: String
|
||||
},
|
||||
description: {
|
||||
default: "disable",
|
||||
type: String
|
||||
},
|
||||
site: {
|
||||
default: () => ({}),
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isDelete: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('proxy', ["selectedSite", "routes", 'isSaveData']),
|
||||
__btnClass__() {
|
||||
if (this.forcedBtnClass) return this.forcedBtnClass
|
||||
return `${this.btnClass} cursor-pointer w-full bg-transparent hover:bg-primary text-primary font-semibold hover:text-white py-1 text-ssm px-4 border border-primary hover:border-transparent rounded-lg text-center transition_up`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isSaveData: function (newVal) {
|
||||
console.log('newVal', newVal)
|
||||
if (newVal) {
|
||||
this.saveSiteData()
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
...mapActions('proxy', ["uploadSiteRoutes", "saveSite", "createNewSite", 'removeSite', 'updateRoutesWithApi']),
|
||||
toggle () {
|
||||
this.open = !this.open
|
||||
this.onToggle({isOpen: this.open})
|
||||
},
|
||||
saveSiteData () {
|
||||
if (this.selectedSite.id == -1) {
|
||||
const data = {
|
||||
name: this.selectedSite.name,
|
||||
port: this.selectedSite.port,
|
||||
device_ip: this.selectedSite.device_ip,
|
||||
proxy_ip: this.selectedSite.proxy_ip,
|
||||
description: this.selectedSite.description
|
||||
}
|
||||
this.createNewSite(data)
|
||||
} else {
|
||||
this.updateRoutesWithApi(this.selectedSite)
|
||||
this.saveSite(this.selectedSite)
|
||||
}
|
||||
},
|
||||
deleteSite (v) {
|
||||
this.isDelete = v
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="!selectedSite || selectedSite.id !== id"
|
||||
href="#"
|
||||
class="block w-full p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<h5
|
||||
v-tippy="{ content: name }"
|
||||
class="max-w-[200px] text-xl font-bold tracking-tight text-slate-600 dark:text-white truncate"
|
||||
>
|
||||
{{ name }}
|
||||
</h5>
|
||||
<div class="relative">
|
||||
<i
|
||||
v-tippy="{ content: 'редактировать сервис' }"
|
||||
class="ri-pencil-line cursor-pointer"
|
||||
@click="uploadSiteRoutes(site)"
|
||||
/>
|
||||
<i
|
||||
v-tippy="{ content: 'удалить сервис' }"
|
||||
class="ri-delete-bin-line cursor-pointer ml-2"
|
||||
@click="deleteSite(true)"
|
||||
/>
|
||||
<div
|
||||
v-if="isDelete"
|
||||
class="absolute flex flex-row top-0 right-0 p-3 bg-white rounded-lg shadow-lg z-10"
|
||||
>
|
||||
<button
|
||||
class="flex items-center text-xs text-white bg-blue-700 p-1 mr-2 rounded-lg shadow"
|
||||
@click="deleteSite(false)"
|
||||
>
|
||||
Отменить
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center p-1 text-xs text-white bg-slate-700 rounded-lg shadow"
|
||||
@click="removeSite(id)"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-[1px] bg-slate-200 mb-4" />
|
||||
<div class="font-normal text-sm text-gray-700 dark:text-gray-400 mb-4 flex -translate-x-2">
|
||||
<span class="flex border-slate-300 mr-4 bg-slate-100 rounded-full py-1 px-2 items-center w-full max-w-[50%]">
|
||||
<span class="inline-block flex mr-2 font-w-700"> статус:</span> <span class="mr-2">{{ status }}</span> <div class="min-h-2 min-w-2 bg-green-700 rounded-full" />
|
||||
</span>
|
||||
<span class="flex border-slate-300 bg-slate-100 rounded-full py-1 px-2 grow">
|
||||
<span class="inline-block flex mr-2 font-w-700"> Порт:</span> <span>{{ port }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class=" font-normal text-sm text-gray-700 dark:text-gray-400 mb-2 flex">
|
||||
<span class="min-w-[80px] font-w-700 inline-block flex"> IP proxy:</span> {{ proxy_ip }}
|
||||
</p>
|
||||
<p class=" font-normal text-sm text-gray-700 dark:text-gray-400 mb-2 flex">
|
||||
<span class="min-w-[80px] font-w-700 inline-block flex"> IP устр-ва:</span> {{ device_ip }}
|
||||
</p>
|
||||
<p class=" font-normal text-sm text-gray-700 dark:text-gray-400 mb-2 flex ">
|
||||
<span class="min-w-[80px] font-w-700 inline-block flex"> Описание:</span> <span
|
||||
v-tippy="{ content: description }"
|
||||
class="truncate"
|
||||
>{{ description }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedSite && selectedSite.id === id"
|
||||
href="#"
|
||||
class="block w-full p-6 bg-white border-4 border-blue-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
|
||||
>
|
||||
<EditCard :id="id" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script>
|
||||
// import {ref} from 'vue'
|
||||
import {mapActions, mapGetters} from 'vuex'
|
||||
import { FwbSpinner } from 'flowbite-vue'
|
||||
import SiteCard from "./SiteCard.vue"
|
||||
import NewSiteButton from "../../NewSiteButton.vue"
|
||||
|
||||
export default {
|
||||
name: 'SiteList',
|
||||
components: {FwbSpinner, SiteCard, NewSiteButton},
|
||||
data () {
|
||||
return {
|
||||
dynamicHeight: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('proxy', ["sites", "routes", "sitesState", "newSite", 'isDeleteData']),
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(function () {
|
||||
this.maxHeight()
|
||||
})
|
||||
},
|
||||
updated () {
|
||||
this.$nextTick(function () {
|
||||
this.maxHeight()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions('proxy', ["uploadSites"]),
|
||||
maxHeight () {
|
||||
setTimeout(() => {
|
||||
const containerHeight = this.$refs.sitesList?.offsetHeight
|
||||
const clientHeight = document.documentElement.clientHeight
|
||||
const delHeight = 400
|
||||
const currHeight = containerHeight + delHeight
|
||||
if ((currHeight) > clientHeight) {
|
||||
this.dynamicHeight = clientHeight - delHeight
|
||||
}
|
||||
}, 100)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
{{
|
||||
|
||||
}}
|
||||
<div
|
||||
v-if="sitesState === 'active'"
|
||||
ref="sitesList"
|
||||
:class="`${dynamicHeight ? 'shadow-lg p-3' : ''} grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 3xl:grid-cols-6 gap-5 overflow-y-auto mb-14`"
|
||||
:style="{maxHeight: `${dynamicHeight}px`}"
|
||||
>
|
||||
<NewSiteButton />
|
||||
<SiteCard
|
||||
v-for="site in sites"
|
||||
:id="site.id"
|
||||
:key="site.name"
|
||||
:site="site"
|
||||
:name="site.name"
|
||||
:port="`${site.port}`"
|
||||
:device_ip="site.device_ip"
|
||||
:proxy_ip="site.proxy_ip"
|
||||
:status="site.status"
|
||||
:description="site.description"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="sitesState === 'loading'"
|
||||
class="flex w-full justify-center"
|
||||
>
|
||||
<fwb-spinner
|
||||
size="8"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span class="text-2xl text-slate-700">
|
||||
Загрузка сайтов...
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
0
proxy-ui-app/src/components/Loader.vue
Normal file
0
proxy-ui-app/src/components/Loader.vue
Normal file
33
proxy-ui-app/src/components/NewSiteButton.vue
Normal file
33
proxy-ui-app/src/components/NewSiteButton.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import {mapActions} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'NewSiteButton',
|
||||
props: {
|
||||
name: {
|
||||
default: "",
|
||||
type: String
|
||||
},
|
||||
port: {
|
||||
default: "",
|
||||
type: String
|
||||
},
|
||||
site: {
|
||||
default: () => ({}),
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('proxy', ['addNewSiteLayout'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="cursor-pointer flex items-center justify-center w-full p-6 bg-white border border-gray-200 rounded-lg shadow hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 text-2xl font-w-700 text-slate-700"
|
||||
@click="addNewSiteLayout"
|
||||
>
|
||||
Добавить сайт
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user