Initial commit

This commit is contained in:
vbuglov 2024-03-05 11:36:21 +03:00
commit bf2f060b94
212 changed files with 100448 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
proxy-ui-app/yarn.lock

59
Makefile Normal file
View File

@ -0,0 +1,59 @@
HOOK_NAME=pre-push
HOOK_PATH=.git/hooks/$(HOOK_NAME)
restart:
docker build -t proxy-ui-vue .
docker run -d --name proxy-ui-vue --rm -p 5000:80 proxy-ui-vue
stoprestart:
docker stop proxy-ui-vue
docker build -t proxy-ui-vue .
docker run -d --name proxy-ui-vue --rm -p 5000:80 proxy-ui-vue
rmrestart:
docker image rm proxy-ui-vue
docker build -t proxy-ui-vue .
docker stop proxy-ui-vue
docker run -d --name proxy-ui-vue --rm -p 5000:80 proxy-ui-vue
pre_proxy_hook:
@echo "Setting up pre-push hook..."
@rm -f $(HOOK_PATH)
@echo '#!/bin/sh' >> $(HOOK_PATH)
@echo 'echo "Running lint for proxy-ui"' >> $(HOOK_PATH)
@echo 'cd ./proxy-ui-app' >> $(HOOK_PATH)
@echo 'yarn lint' >> $(HOOK_PATH)
@echo 'if [ $$? -ne 0 ]; then' >> $(HOOK_PATH)
@echo ' echo "Tests failed, push aborted."' >> $(HOOK_PATH)
@echo ' exit 1' >> $(HOOK_PATH)
@echo 'fi' >> $(HOOK_PATH)
@echo 'echo "Running tests for proxy-ui"' >> $(HOOK_PATH)
@echo 'yarn test_ones' >> $(HOOK_PATH)
@echo 'if [ $$? -ne 0 ]; then' >> $(HOOK_PATH)
@echo ' echo "Tests failed, push aborted."' >> $(HOOK_PATH)
@echo ' exit 1' >> $(HOOK_PATH)
@echo 'fi' >> $(HOOK_PATH)
@echo 'exit 0' >> $(HOOK_PATH)
@chmod +x $(HOOK_PATH)
@echo "Pre-push hook set successfully."
pre_users_hook:
@echo "Setting up pre-push hook..."
@rm -f $(HOOK_PATH)
@echo '#!/bin/sh' >> $(HOOK_PATH)
@echo 'echo "Running lint for proxy-ui"' >> $(HOOK_PATH)
@echo 'cd ./users-manage' >> $(HOOK_PATH)
@echo 'yarn lint' >> $(HOOK_PATH)
@echo 'if [ $$? -ne 0 ]; then' >> $(HOOK_PATH)
@echo ' echo "Tests failed, push aborted."' >> $(HOOK_PATH)
@echo ' exit 1' >> $(HOOK_PATH)
@echo 'fi' >> $(HOOK_PATH)
@echo 'echo "Running tests for proxy-ui"' >> $(HOOK_PATH)
@echo 'yarn test_ones' >> $(HOOK_PATH)
@echo 'if [ $$? -ne 0 ]; then' >> $(HOOK_PATH)
@echo ' echo "Tests failed, push aborted."' >> $(HOOK_PATH)
@echo ' exit 1' >> $(HOOK_PATH)
@echo 'fi' >> $(HOOK_PATH)
@echo 'exit 0' >> $(HOOK_PATH)
@chmod +x $(HOOK_PATH)
@echo "Pre-push hook set successfully."

1
README.md Normal file
View File

@ -0,0 +1 @@
### Пуш приложений

1
proxy-ui-app/.env Normal file
View File

@ -0,0 +1 @@
VITE_API_ADDR="http://172.25.78.64:8087"

View File

@ -0,0 +1,36 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended",
],
rules: {
"array-callback-return": "warn",
"for-direction": "error",
"getter-return": "warn",
"no-async-promise-executor": "warn",
"no-class-assign": "warn",
"no-multiple-empty-lines": ["error", {max: 2, "maxBOF": 0}],
"indent": ["error", 2],
"no-mixed-spaces-and-tabs": "error",
"vue/multi-word-component-names": "off",
"vue/attribute-hyphenation": "off",
"vue/html-self-closing": ["error", {
"html": {
"void": "never",
"normal": "always",
"component": "always"
},
"svg": "always",
"math": "always"
}],
"vue/no-reserved-component-names": "off",
"vue/no-use-v-if-with-v-for": "off",
"vue/html-indent": ["error", 2, {
"attribute": 1,
"baseIndent": 1,
"closeBracket": 0,
"alignAttributesVertically": true,
"ignores": []
}]
}
}

24
proxy-ui-app/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
proxy-ui-app/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

13
proxy-ui-app/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
# этап сборки (build stage)
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# этап production (production-stage)
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

47
proxy-ui-app/Makefile Normal file
View File

@ -0,0 +1,47 @@
HOOK_NAME=pre-push
HOOK_PATH=../.git/hooks/$(HOOK_NAME)
restart:
docker build -t proxy-ui-vue .
docker run -d --name proxy-ui-vue --rm -p 5000:80 proxy-ui-vue
stoprestart:
docker stop proxy-ui-vue
docker build -t proxy-ui-vue .
docker run -d --name proxy-ui-vue --rm -p 5000:80 proxy-ui-vue
rmrestart:
docker image rm proxy-ui-vue
docker build -t proxy-ui-vue .
docker stop proxy-ui-vue
docker run -d --name proxy-ui-vue --rm -p 5000:80 proxy-ui-vue
prehook:
@echo "Setting up pre-push hook..."
@rm -f $(HOOK_PATH)
@echo '#!/bin/sh' >> $(HOOK_PATH)
@echo 'echo "Running lint for proxy-ui-app"' >> $(HOOK_PATH)
@echo 'cd ./proxy-ui-app' >> $(HOOK_PATH)
@echo 'yarn lint' >> $(HOOK_PATH)
@echo 'if [ $$? -ne 0 ]; then' >> $(HOOK_PATH)
@echo ' echo "Tests failed, push aborted."' >> $(HOOK_PATH)
@echo ' exit 1' >> $(HOOK_PATH)
@echo 'fi' >> $(HOOK_PATH)
@echo 'echo "Running tests for proxy-ui"' >> $(HOOK_PATH)
@echo 'yarn test_ones' >> $(HOOK_PATH)
@echo 'if [ $$? -ne 0 ]; then' >> $(HOOK_PATH)
@echo ' echo "Tests failed, push aborted."' >> $(HOOK_PATH)
@echo ' exit 1' >> $(HOOK_PATH)
@echo 'fi' >> $(HOOK_PATH)
@echo 'exit 0' >> $(HOOK_PATH)
@chmod +x $(HOOK_PATH)
@echo "Pre-push hook set successfully."
push:
ifeq ($(commit),)
$(error mn is not set)
endif
make prehook
git add . && git commit -m "feat($(commit)):" && git push

7
proxy-ui-app/README.md Normal file
View File

@ -0,0 +1,7 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).

16
proxy-ui-app/index.html Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="stylesheet" href="/css/remixicons/remixicon.css">
<link rel="stylesheet" href="/css/fonts.css">
<link rel="stylesheet" href="/css/global.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

51
proxy-ui-app/package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "proxy-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test_dom": "vitest --dom",
"test_dom_ones": "vitest run --dom",
"test": "vitest",
"test_ones": "vitest run",
"lint": "yarn eslint './**/*.{js,vue}'",
"lint_fix": "yarn eslint './**/*.{js,vue}' --fix"
},
"dependencies": {
"axios": "^1.6.7",
"flowbite": "^2.2.1",
"flowbite-vue": "^0.1.2",
"postcss-import": "^16.0.0",
"ramda": "^0.29.1",
"ramda-vue": "^0.0.9",
"tippy.js": "^6.3.7",
"vue": "^3.3.11",
"vue-multiselect": "^3.0.0-beta.3",
"vue-tippy": "^6.4.1",
"vuex": "^4.1.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.17",
"eslint": "8.30.0",
"eslint-plugin-vue": "^9.22.0",
"happy-dom": "^13.6.2",
"postcss": "^8.4.34",
"postcss-advanced-variables": "^3.0.1",
"postcss-apply": "^0.12.0",
"postcss-nesting": "^12.0.2",
"tailwindcss": "^3.4.1",
"vite": "^5.0.8",
"vitest": "^1.2.2"
},
"resolutions": {
"strip-ansi": "6.0.1",
"string-width": "4.2.2",
"wrap-ansi": "7.0.0"
}
}

View File

@ -0,0 +1,9 @@
export default {
plugins: {
"postcss-import": {},
"postcss-advanced-variables": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,4 @@
@font-face {
font-family: Nunito;
src: url(../fonts/Nunito.ttf);
}

View File

@ -0,0 +1,119 @@
body {
background-color: transparent;
}
html body {
overflow-x: hidden;
font-family: Nunito;
font-size: 0.875rem;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 0 !important;
}
.text-ssm {
font-size: 0.675rem;
line-height: 1.1rem;
}
input[phx-hook='ToggleInput'] {
border-color: transparent;
}
.fontw-100 {
font-weight: 100
}
.fontw-200 {
font-weight: 200;
}
.fontw-300 {
font-weight: 300;
}
.fontw-400 {
font-weight: 400;
}
.fontw-500 {
font-weight: 500;
}
.fontw-600 {
font-weight: 600;
}
.fontw-700 {
font-weight: 700;
}
.fontw-800 {
font-weight: 800;
}
.fontw-900 {
font-weight: 900;
}
.font-w-100 {
font-weight: 100
}
.font-w-200 {
font-weight: 200;
}
.font-w-300 {
font-weight: 300;
}
.font-w-400 {
font-weight: 400;
}
.font-w-500 {
font-weight: 500;
}
.font-w-600 {
font-weight: 600;
}
.font-w-700 {
font-weight: 700;
}
.font-w-800 {
font-weight: 800;
}
.font-w-900 {
font-weight: 900;
}
.transition-all {
transition: all .3s;
}
.transition-3 {
transition: all .3s;
}
.transition-4 {
transition: all .4s;
}
.transition-5 {
transition: all .5s;
}
.transition-6 {
transition: all .6s;
}
.transition-7 {
transition: all .7s;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.3 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,93 @@
Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,79 @@
Nunito Variable Font
====================
This download contains Nunito as both variable fonts and static fonts.
Nunito is a variable font with this axis:
wght
This means all the styles are contained in these files:
Nunito-VariableFont_wght.ttf
Nunito-Italic-VariableFont_wght.ttf
If your app fully supports variable fonts, you can now pick intermediate styles
that arent available as static fonts. Not all apps support variable fonts, and
in those cases you can use the static font files for Nunito:
static/Nunito-ExtraLight.ttf
static/Nunito-Light.ttf
static/Nunito-Regular.ttf
static/Nunito-Medium.ttf
static/Nunito-SemiBold.ttf
static/Nunito-Bold.ttf
static/Nunito-ExtraBold.ttf
static/Nunito-Black.ttf
static/Nunito-ExtraLightItalic.ttf
static/Nunito-LightItalic.ttf
static/Nunito-Italic.ttf
static/Nunito-MediumItalic.ttf
static/Nunito-SemiBoldItalic.ttf
static/Nunito-BoldItalic.ttf
static/Nunito-ExtraBoldItalic.ttf
static/Nunito-BlackItalic.ttf
Get started
-----------
1. Install the font files you want to use
2. Use your app's font picker to view the font family and all the
available styles
Learn more about variable fonts
-------------------------------
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
https://variablefonts.typenetwork.com
https://medium.com/variable-fonts
In desktop apps
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
Online
https://developers.google.com/fonts/docs/getting_started
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
Installing fonts
MacOS: https://support.apple.com/en-us/HT201749
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
Android Apps
https://developers.google.com/fonts/docs/android
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
License
-------
Please read the full license text (OFL.txt) to understand the permissions,
restrictions and requirements for usage, redistribution, and modification.
You can use them in your products & projects print or digital,
commercial or otherwise.
This isn't legal advice, please consider consulting a lawyer and see the full
license for all details.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

41
proxy-ui-app/src/App.vue Normal file
View File

@ -0,0 +1,41 @@
<script>
import RoutesList from "@organisms/RoutersEditor/index.vue"
import PageHeader from "@atoms/PageHeader.vue"
import RouterRow from "@organisms/RoutersEditor/RouterRow.vue"
import SiteList from "@organisms/SitesEditor/SiteList.vue"
import {mapActions, mapGetters} from 'vuex'
export default {
name: 'App',
components: {RoutesList, SiteList, RouterRow, PageHeader},
computed: {
...mapGetters('proxy', ["sites", "routes"]),
},
mounted() {
this.uploadSites()
// const routes = new Users("http://172.25.78.64:8087")
// routes.test().then((testRes) => {
// console.log(testRes)
// })
},
methods: {
...mapActions('proxy', ["uploadSites"])
}
}
</script>
<template>
<div class="p-6">
<PageHeader class="me-2 mb-6" />
<SiteList />
<RoutesList>
<RouterRow
v-for="route in routes"
:id="route.id"
:key="`key-${route.id}`"
/>
</RoutesList>
</div>
</template>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

View 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>

View File

@ -0,0 +1,160 @@
import {get, post, put, remove} from './apiHelpers.js'
import {convertObject, convertList} from './adapter/adapter.js'
import { config} from '@store/modules/proxy/StaticData.js';
const servConfig = {
id: "id",
created_at: "created_at",
updated_at: "updated_at",
deleted_at: "deleted_at",
name: "name",
port: "port",
proxy_ip: "proxy_ip",
internet_uri: "internet_uri",
description: "description",
is_online: "is_online",
device_ip: "site_ip",
}
class Services {
/**
*
* @param {String} apiAddr - path to service
* @param {Object} config - oldKey: newKey
*/
constructor(apiAddr, config) {
this.apiAddr = apiAddr
this.config = config
}
/**
*
* @returns {Array<Object>} res.data - all services
*/
async getServices() {
const res = await get(`${this.apiAddr}/servers`)
return convertList(res.data, {config: this.config})
}
/**
*
* @param {Object} payload - add new service
* @returns {Object} newService - added new service
*/
async createService(payload) {
let newService = []
const updatedPort = parseFloat(payload.port)
const updatedService = {...convertObject(payload, {config: servConfig}), port: updatedPort}
await post(`${this.apiAddr}/servers`, updatedService).then(res => {
newService = convertObject(res.value, {config: this.config})
}).catch(err => {
console.log('err', err)
})
return newService
}
/**
*
* @param {Object} payload - edit params in selected service
* @returns {Object} resService - updated service with edited params
*/
async updateService(payload) {
let resService = []
const updatedPort = parseFloat(payload.port)
const updatedService = {...convertObject(payload, {config: servConfig}), port: updatedPort}
if (payload.id) {
await put(`${this.apiAddr}/servers`, updatedService, payload.id).then(res => {
resService = convertObject(res.value, {config: this.config})
}).catch(err => {
console.log('err', err)
})
}
return resService
}
/**
*
* @param {Number} id - id selected service for remove
* @returns {Number} deletedServiceId - id removed service
*/
async deleteService(id) {
let deletedServiceId = null
await remove(`${this.apiAddr}/servers`, id).then((res) => {
deletedServiceId = res.id
}).catch(err => {
console.log('err', err)
})
return deletedServiceId
}
}
export default Services
/**
* Testing class Services
*/
// eslint-disable-next-line no-unused-vars
const apiTest = async () => {
const services = new Services(import.meta.env.VITE_API_ADDR, config)
// Testing get services
console.log('Uploaded services, method getServices - start')
const uploadedServices = await services.getServices()
console.log('Uploaded services, method getServices - result:', uploadedServices)
// Testing create service
console.log('Added new service, method createService - start')
const newService = await services.createService({
name: "Test site for testing createService",
port: 7777,
site_ip: "172.25.78.151",
proxy_ip: "172.25.78.151",
description: 'Testing createService method',
})
let updatedServices = [...uploadedServices, newService]
console.log('Added new service, method createService - result:', newService)
// Testing update service
console.log('Updated services, method updateService - start')
const serviceWithNewParams = {...newService, name: 'Test site for testing updateService', description: 'Testing updateService method', port: 9999}
const updatedService = await services.updateService(serviceWithNewParams)
updatedServices = [...updatedServices.filter(service => service.id !== newService.id), updatedService]
console.log('Updated services, method updateService - result:', updatedService)
// Testing delete service
console.log('Deleted service, method deleteService - start')
const deletedServiceId = await services.deleteService(newService.id)
console.log('Deleted service, method deleteService, get id from deleted service - result:', deletedServiceId)
// Testing get services after delete
const withoutDeletedService = updatedServices.filter(service => service.id !== newService.id)
const updatedServicesAfterDelete = await services.getServices()
console.log('Services after all tests:', updatedServicesAfterDelete)
// Equal results
const reg = new RegExp (`${updatedServicesAfterDelete.length}`, 'g')
const lengthCurrServices = withoutDeletedService.length.toString()
console.log('reg', reg)
const isEqual = reg.test(lengthCurrServices)
console.log('isCountServicesEqual', isEqual)
}
// Запуск теста api - расскомментировать функцию apiTest
// apiTest()
// тест api end
/**
* Testing class Services end
*/

View File

@ -0,0 +1,42 @@
/**
* Каллбек изменнеия, ожидает что вернется измененный объект
*
* @callback adapterCallback
* @param {Object} modifiedObject - новый объект
* @param {Object} originalObject - изначальный объект
* @returns {Object} измененный коллбэком объект
*/
/**
*
* Если у newKey значение отстутсвует - null, '', false, 0, то oldKey удаляется из объекта
*
* @param {Object} targetObject
* @param {Object} params
* @param {Object} params.config - oldKey: null // delete oldKey
* @param {adapterCallback | undefined} params.callback
* @returns {Object}
*/
const convertObject = (targetObject, {config, callback = (v) => v}) => {
let newObject = {}
for (const key in config) {
newObject[config[key]] = targetObject[key]
}
return callback(newObject, targetObject)
}
/**
*
* Если у newKey значение отстутсвует - null, '', false, 0, то oldKey удаляется из объекта
*
* @param {Array} targetList
* @param {Object} options - oldKey: null // delete oldKey
* @param {Object} options.config - oldKey: null // delete oldKey
* @param {adapterCallback | undefined} options.callback
* @returns {Object}
*/
const convertList = (targetList, {config, callback = (v) => v}) => {
return targetList.map((targetObject) => convertObject(targetObject, {config, callback}))
}
export {convertList, convertObject}

View File

@ -0,0 +1,39 @@
import axios from "axios";
const post = async (path, data, onError) => {
return await axios.post(path, data)
.then(r => r.data)
.catch(error => {
onError && onError(error)
console.error('Error post request:', error)
})
}
const get = async (path) => {
return await axios.get(path)
.catch(error => {
console.error('Error get request:', error)
})
}
const put = async (path, data, id, onError) => {
return await axios.put(`${path}/${id}`, data)
.then(r => r.data)
.catch(error => {
onError && onError(error)
console.error('Error put request:', error)
})
}
const remove = async (path, id, onError) => {
return await axios.delete(`${path}/${id}`)
.then(r => r.data)
.catch(error => {
onError && onError(error)
console.error('Error delete request:', error)
})
}
export {get, post, put, remove}

View File

@ -0,0 +1,232 @@
import {get, post, put, remove} from './apiHelpers.js'
import {convertList} from './adapter/adapter.js'
import {equals, cond} from "ramda";
/**
* Интерфейс роутера
* @typedef {Object} Router
* @param {Number} id
* @param {Number} server_id
* @param {String} path
* @param {Number} role
* @param {String} description
* @param {Number} deepness
* @param {Number} order
* @param {Boolean} is_cb_on
* @param {Number} cb_request_limit
* @param {Number} cb_min_requests
* @param {Number} cb_error_threshold_percentage
* @param {Number} cb_interval_duration
* @param {Number} cb_open_state_timeout
*/
/**
* Интерфейс роутера
* @typedef {Object} Action
* @property {'remove' | 'create' | 'update' | 'delete'} action
*
* @typedef {Router & Action} ActionRouter
*/
class Routes {
/**
* Класс управления роутерами
* @param {String} apiAddr - path to service
* @param {Object | undefined} adapter_config - oldKey: newKey
*/
constructor(apiAddr, adapter_config = {}) {
this.apiAddr = apiAddr
this.config = adapter_config
}
/**
*
* @returns {Promise<*[]>}
*/
async getRoutes() {
let res = await get(`${this.apiAddr}/routers`)
let updatedRoutes = convertList(res.data, {config: this.config})
return updatedRoutes
}
/**
*
* @param {Number} id - Сервис id, если id не указан, отображается список всех роутеров
* @returns {Promise<*[]>}
*/
async getRouterById(id) {
let res = await get(`${this.apiAddr}/routers/by_server/${id}`)
let updatedRoutes = convertList(res.data, {config: this.config})
return updatedRoutes
}
/**
*
* @param {Router} routerData
* @returns {Promise<void>}
*/
async createRoute(routerData) {
const newRoute = await post(`${this.apiAddr}/routers`, routerData)
return newRoute
}
/**
*
* @param {Router} routerData
* @returns {Promise<void>}
*/
async updateRoute(routerData) {
const updatedRouterData = {...routerData}
delete updatedRouterData.id
const newRoute = await put(`${this.apiAddr}/routers/${routerData.id}`, updatedRouterData)
return newRoute
}
/**
*
* @param {Number} routerId - Сервис id, если id не указан, отображается список всех роутеров
* @returns {Promise<*[]>}
*/
async removeRoute(routerId) {
const removedRoute = await remove(`${this.apiAddr}/routers/${routerId}`)
return removedRoute
}
/**
*
* @param {Array<ActionRouter>} routesActions - Группа роутеров для обновления
* @returns {Promise<Array<Router>>}
*/
async updateGroupRoutes(routesActions) {
const responses = routesActions.map(async (mutation) => {
return cond([
[equals('create'), () => this.createRoute(mutation)],
[equals('delete'), () => this.removeRoute(mutation.id)],
[equals('remove'), () => this.removeRoute(mutation.id)],
[equals('update'), () => this.updateRoute(mutation)],
])(mutation.action)
})
return Promise.all(responses)
}
/**
* Функция запускает список запросов к апишке и логает ответы
* @returns {Promise<String>}
*/
async test() {
console.log("_______START TEST_______")
const allRoutes = await this.getRoutes()
console.log("allRoutes", allRoutes)
const serverRouter = await this.getRouterById(1)
console.log("getRouterById 1", serverRouter)
const newRoute = await this.createRoute({
"server_id": 1,
"path": "/",
"role": 1,
"description": "tests swagger",
"deepness": 6,
"order": 0,
"is_cb_on": true,
"cb_request_limit": 100,
"cb_min_requests": 100,
"cb_error_threshold_percentage": 0.35,
"cb_interval_duration": 2000000,
"cb_open_state_timeout": 1000000
})
console.log("newRoute", newRoute)
const updatedRoute = await this.updateRoute({
"path": "/updated_path/",
"description": "updated_description",
"id": newRoute.id
})
console.log("updatedRoute", updatedRoute)
const removedRoute = await this.removeRoute(newRoute.id)
console.log("removedRoute", removedRoute)
const newRoute1 = await this.createRoute({
"server_id": 1,
"path": "/",
"role": 1,
"description": "tests swagger",
"deepness": 6,
"order": 0,
"is_cb_on": true,
"cb_request_limit": 100,
"cb_min_requests": 100,
"cb_error_threshold_percentage": 0.35,
"cb_interval_duration": 2000000,
"cb_open_state_timeout": 1000000
})
const newRoute2 = await this.createRoute({
"server_id": 1,
"path": "/",
"role": 1,
"description": "tests swagger",
"deepness": 6,
"order": 0,
"is_cb_on": true,
"cb_request_limit": 100,
"cb_min_requests": 100,
"cb_error_threshold_percentage": 0.35,
"cb_interval_duration": 2000000,
"cb_open_state_timeout": 1000000
})
const newRoute3 = await this.createRoute({
"server_id": 1,
"path": "/",
"role": 1,
"description": "tests swagger",
"deepness": 6,
"order": 0,
"is_cb_on": true,
"cb_request_limit": 100,
"cb_min_requests": 100,
"cb_error_threshold_percentage": 0.35,
"cb_interval_duration": 2000000,
"cb_open_state_timeout": 1000000
})
const actions = [
{
...newRoute1,
action: "update",
"path": "/updated_path2/",
"description": "updated_description2",
}, {
...newRoute2,
action: "update",
"path": "/updated_path3/",
"description": "updated_description3",
}, {
...newRoute3,
action: "remove"
},
{
action: "create",
"server_id": 1,
"path": "/lalalala/lalalal",
"role": 1,
"description": "new_route_created",
"deepness": 6,
"order": 0,
"is_cb_on": true,
"cb_request_limit": 100,
"cb_min_requests": 100,
"cb_error_threshold_percentage": 0.35,
"cb_interval_duration": 2000000,
"cb_open_state_timeout": 1000000
}
]
const mutationsList = await this.updateGroupRoutes(actions)
console.log("mutationsList", mutationsList)
console.log("________END TEST________")
return "ok"
}
}
export default Routes

View File

@ -0,0 +1,43 @@
/**
* Каллбек изменнеия, ожидает что вернется измененный объект
*
* @callback adapterCallback
* @param {Object} modifiedObject - новый объект
* @param {Object} originalObject - изначальный объект
* @returns {Object} измененный коллбэком объект
*/
/**
*
* Если у newKey значение отстутсвует - null, '', false, 0, то oldKey удаляется из объекта
*
* @param {Object} targetObject
* @param {Object} params
* @param {Object} params.config - oldKey: null // delete oldKey
* @param {adapterCallback | undefined} params.callback
* @returns {Object}
*/
const convertObject = (targetObject, {config, callback = (v) => v}) => {
let newObject = {}
for (const key in config) {
newObject[config[key]] = targetObject[key]
}
return callback(newObject, targetObject)
}
/**
*
* Если у newKey значение отстутсвует - null, '', false, 0, то oldKey удаляется из объекта
*
* @param {Array} targetList
* @param {Object} options - oldKey: null // delete oldKey
* @param {Object} options.config - oldKey: null // delete oldKey
* @param {adapterCallback | undefined} options.callback
* @returns {Object}
*/
const convertList = (targetList, {config, callback = (v) => v}) => {
return targetList.map((targetObject) => convertObject(targetObject, {config, callback}))
}
export {convertList, convertObject}

View File

@ -0,0 +1,38 @@
import axios from "axios";
const post = async (path, data, onError) => {
return await axios.post(path, data)
.then(r => r.data)
.catch(error => {
onError && onError(error)
console.error('Error post request:', error)
})
}
const get = async (path) => {
return await axios.get(path)
.catch(error => {
console.error('Error get request:', error)
})
}
const put = async (path, data, onError) => {
return await axios.put(`${path}`, data)
.then(r => r.data)
.catch(error => {
onError && onError(error)
console.error('Error put request:', error)
})
}
const remove = async (path, onError) => {
return await axios.delete(`${path}`)
.then(r => r.data)
.catch(error => {
onError && onError(error)
console.error('Error delete request:', error)
})
}
export {get, post, put, remove}

14
proxy-ui-app/src/main.js Normal file
View File

@ -0,0 +1,14 @@
import './styles/main.css'
import 'tippy.js/dist/tippy.css'
import { createApp } from 'vue'
import App from './App.vue'
import ramdaVue from "./ramda-vue.js";
import { store } from '@/store'
import VueTippy from 'vue-tippy'
createApp(App)
.use(ramdaVue)
.use(store)
.use(VueTippy)
.mount('#app')

View File

@ -0,0 +1,18 @@
import * as R from 'ramda'
const RamdaVue = {
install: (app) => {
app.$R = R;
app.config.globalProperties.$R = R;
R.ifElse(
R.and(R.compose(R.not, R.isNil), R.has("Vue")),
(win) => {
win.Vue.use(RamdaVue);
},
() => {}
)(window);
},
};
export default RamdaVue;

View File

@ -0,0 +1,19 @@
import {createStore, createLogger} from 'vuex';
import {store as proxy} from '@/store/modules/proxy';
// eslint-disable-next-line no-undef
let debug = process.env.NODE_ENV !== 'production';
debug = false;
const plugins = debug ? [createLogger({})] : [];
export const store = createStore({
plugins,
modules: {
proxy
},
});
export function useStore() {
return store;
}

View File

@ -0,0 +1,65 @@
const config = {
id: "id",
created_at: "created_at",
updated_at: "updated_at",
deleted_at: "deleted_at",
name: "name",
port: "port",
proxy_ip: "proxy_ip",
internet_uri: "internet_uri",
description: "description",
is_online: "is_online",
site_ip: "device_ip",
}
const configRoutes = {
id: "id",
created_at: "created_at",
updated_at: "updated_at",
deleted_at: "deleted_at",
server_id: "server_id",
path: "path",
role: "role",
description: "description",
order: "order",
deepness: "deepness",
is_cb_on: "is_cb_on",
is_online: "is_online",
cb_request_limit: "cb_request_limit",
cb_min_requests: "cb_min_requests",
cb_error_threshold_percentage: "cb_error_threshold_percentage",
cb_interval_duration: "cb_interval_duration",
cb_open_state_timeout: "cb_open_state_timeout",
site_ip: "device_ip",
}
const routes = [
{"path": "/socket.io", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/socket.io", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/socket.io/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/api", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/api/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/api/*/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/api/*/*/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/api/*/*/*/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/swagger", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/swagger/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/swagger/*/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/swagger/*/*/*","backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"path": "/", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"},
{"path": "/*", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"},
{"path": "/*/*", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"},
{"path": "/*/*/*", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"},
{"path": "/*/*/*/*", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"},
{"path": "/*/*/*/*/*", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"}
]
const sites = [
{"name": "cdrp"},
{"name": "live_monitore"},
{"name": "buk"},
{"name": "mtso"},
{"name": "as_dmps"}
]
export {routes, sites, config, configRoutes}

View File

@ -0,0 +1,67 @@
const addedSite = (addedSite, sites) => {
return [addedSite,...sites]
}
const updatedSite = (updatedSite, sites) => {
if (updatedSite.id) {
const editIdx = sites.findIndex(service => service.id === updatedSite.id)
const beforeEdit = sites.slice(0, editIdx)
const afterEdit = sites.slice(editIdx + 1)
const withEdit = [...beforeEdit, updatedSite]
return [...withEdit, ...afterEdit]
}
}
const removedNewSite = (sites) => {
if (sites.length > 0) {
const firstSite = sites[0]
const isNewSite = firstSite.id === -1
sites = isNewSite ? sites.slice(1) : sites
return sites
}
}
const deletedSite = (deletedSiteId, sites) => {
if (deletedSiteId) {
const deleteIdx = sites.findIndex(service => service.id === deletedSiteId)
return sites.slice(0, deleteIdx).concat(sites.slice(deleteIdx + 1))
}
}
const addedNewRoute = (selectedSite, routes) => {
let newRoute = routes.find((route) => route.action === 'create')
if (newRoute) {
newRoute = {...newRoute, server_id: selectedSite.id}
const withoutNewRoute = routes.filter(route => route.action !== 'create')
return [newRoute, ...withoutNewRoute]
}
}
const updatedRoute = (updatedRoute, routes) => {
return routes.reduce((acc, cur) => {
if (cur.id === updatedRoute.id) {
return [...acc, {
...updatedRoute,
action: updatedRoute.action === "create" ? "create" : "update"
}]
}
return [...acc, cur]
}, [])
}
const deletedRoute = (deletedRouteId, routes) => {
return routes.reduce((acc, cur) => {
if (cur.id !== deletedRouteId) return [...acc, cur]
if (cur.action === "create") return acc
return [...acc, {
...cur,
action: "remove"
}]
}, [])
}
const sortedRoutes = (routes) => {
return routes.sort(({updated_at: a}, {updated_at: b}) => new Date(b).valueOf() - new Date(a).valueOf())
}
export { addedSite, updatedSite, removedNewSite, deletedSite, addedNewRoute, updatedRoute, sortedRoutes, deletedRoute }

View File

@ -0,0 +1,153 @@
import { config, configRoutes } from './StaticData.js';
import routeOptions from './routeOptions.json'
import Services from '@helpers/Services/Services.js';
import {addedSite, updatedSite, removedNewSite, deletedSite, addedNewRoute, updatedRoute, sortedRoutes, deletedRoute} from './helpers.js';
import {isEmpty, dissoc} from 'ramda';
import RoutesClass from "@helpers/server-routes/Routes.js";
const routerManager = new RoutesClass(import.meta.env.VITE_API_ADDR, configRoutes)
const services = new Services(import.meta.env.VITE_API_ADDR, config)
const initState = {
sites: [],
sitesState: "loading",
isSaveData: false,
selectedSite: null,
routes: [],
routesState: "await",
routesLib: {},
};
const state = {
...initState
};
const getters = {
isSaveData: (state) => state.isSaveData,
sites: (state) => state.sites,
routes: (state) => state.routes.filter(({action}) => !["remove", "delete"].includes(action)),
routesLib: (state) => state.routes.reduce((acc, cur) => ({
...acc,
[cur.id]: cur
}), {}),
routesState: (state) => state.routesState,
sitesState: (state) => state.sitesState,
routeOptions: () => routeOptions,
selectedSite: (state) => state.selectedSite,
};
const mutations = {
setIsSaveData: (state, payload) => state.isSaveData = payload,
setSites: (state, payload) => state.sites = payload,
setSitesState: (state, payload) => state.sitesState = payload,
setSelectedSite: (state, payload) => state.selectedSite = payload,
setRoutes: (state, payload) => state.routes = payload,
setRoutesState: (state, payload) => state.routesState = payload,
};
const actions = {
addNewSiteLayout: ({commit, getters}) => {
const newSite = {"port": "", "name": "", id: -1}
commit('setSites', [newSite,...getters.sites])
commit('setSelectedSite', newSite)
commit('setRoutes', [])
commit('setRoutesState', "active")
},
createNewSite: async ({dispatch, commit, getters, state}, payload) => {
commit('setIsSaveData', false)
commit('setSelectedSite', null)
const newSite = await services.createService(payload)
const updatedSites = addedSite(newSite, getters.sites)
if (!isEmpty(updatedSites)) {
const newSite = updatedSites[0]
const updatedRoutes = addedNewRoute(newSite, state.routes)
commit('setRoutes', updatedRoutes)
dispatch('updateRoutesWithApi', newSite)
return commit('setSites', updatedSites)
}
},
breakeAddingSite: ({commit}) => {
commit('setSelectedSite', null)
},
uploadSites: async ({commit}) => {
const sites = await services.getServices()
commit('setSites', sites)
commit('setSitesState', 'active')
},
editSelectedSite: async ({commit, getters}, payload) => {
const selectedSite = getters.selectedSite
selectedSite[payload.key] = payload.value
commit('setSelectedSite', selectedSite)
},
saveSite: async ({commit, getters}, payload) => {
commit('setIsSaveData', false)
commit('setSelectedSite', null)
const editedSite = await services.updateService(payload)
const updatedSites = !isEmpty(editedSite) ? updatedSite(editedSite, getters.sites) : getters.sites
commit('setSites', updatedSites)
},
breakSavingSite: ({commit}) => {
commit('setSelectedSite', null)
commit('setRoutesState', "await")
},
removeSite: async ({commit, getters}, id) => {
const deletedSiteId = await services.deleteService(id)
const updatedSites = deletedSite(deletedSiteId, getters.sites)
if (!isEmpty(updatedSites)) return commit('setSites', updatedSites)
},
uploadSiteRoutes: async ({commit, getters}, siteProps) => {
const sites = removedNewSite(getters.sites)
commit('setSites', sites)
commit('setSelectedSite', siteProps)
commit('setRoutesState', "loading")
let routes = await routerManager.getRouterById(siteProps.id)
routes = sortedRoutes(routes)
commit('setRoutes', routes)
commit('setRoutesState', "active")
},
addNewRouteLayout: ({commit, getters}) => {
const newRoute = {
"path": null,
"role": null,
id: Math.random().toString(36).slice(4),
action: "create",
server_id: getters.selectedSite.id
}
commit('setRoutes', [newRoute, ...state.routes])
},
updateRouteRow: ({commit, state}, editRoute) => {
const updatedRoutes = updatedRoute(editRoute, state.routes)
commit('setRoutes', updatedRoutes)
},
updateRoutesWithApi: async ({commit, state}, payload) => {
commit('setRoutesState', "loading")
let updatedRoutes = state.routes.filter(({action}) => action)
updatedRoutes = updatedRoutes.map((el) => {
if (el.action === "create") return dissoc('id', el)
return el
})
await routerManager.updateGroupRoutes(updatedRoutes)
let routes = await routerManager.getRouterById(payload.id)
routes = sortedRoutes(routes)
commit('setRoutes', routes)
commit('setRoutesState', "await")
},
removeRoute: ({commit, state}, {id}) => {
const updatedRoutes = deletedRoute(id, state.routes)
commit('setRoutes', updatedRoutes)
},
resetStore: ({state}) => {
Object.entries(initState).forEach(([k,v]) => {
state[k] = v
})
},
};
export const store = {
namespaced: true,
state,
getters,
mutations,
actions,
};

View File

@ -0,0 +1,5 @@
[
{ "name": 1, "value": 1 },
{ "name": 2, "value": 2 },
{ "name": 3, "value": 3 }
]

View File

@ -0,0 +1,19 @@
[
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 1,"path": "/socket.io", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 2,"path": "/socket.io/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 3,"path": "/api", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 4,"path": "/api/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 5,"path": "/api/*/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 6,"path": "/api/*/*/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 7,"path": "/api/*/*/*/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 8,"path": "/swagger", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 9,"path": "/swagger/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 10,"path": "/swagger/*/*", "backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 11,"path": "/swagger/*/*/*","backend": "http://backend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 12,"path": "/", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 13,"path": "/*", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 14,"path": "/*/*", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 15,"path": "/*/*/*", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 16,"path": "/*/*/*/*", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "id": 17,"path": "/*/*/*/*/*", "backend": "http://frontend:8000", "role": "*", "site": "cdrp"}
]

View File

@ -0,0 +1,7 @@
[
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "device_ip": "172.25.78.36", "proxy_ip": "172.25.78.36", "status": "active", "id": 1,"port": "4000", "name": "cdrp"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "device_ip": "172.25.78.36", "proxy_ip": "172.25.78.36", "status": "active", "id": 2,"port": "4000", "name": "live_monitore"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "device_ip": "172.25.78.36", "proxy_ip": "172.25.78.36", "status": "active", "id": 3,"port": "4000", "name": "buk"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "device_ip": "172.25.78.36", "proxy_ip": "172.25.78.36", "status": "active", "id": 4,"port": "4000", "name": "mtso"},
{"description": "The item is sized according to its width and height properties. It shrinks to its minimum size to fit the container, but does not grow to absorb any extra free space in the flex container. This is equivalent to setting", "device_ip": "172.25.78.36", "proxy_ip": "172.25.78.36", "status": "active", "id": 5,"port": "4000", "name": "as_dmps"}
]

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,22 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
'node_modules/flowbite-vue/**/*.{js,jsx,ts,tsx,vue}',
'node_modules/flowbite/**/*.{js,jsx,ts,tsx}'
],
darkMode: "class", // or 'media' or 'class'
// eslint-disable-next-line no-undef
plugins: [require("@tailwindcss/forms"), require('flowbite/plugin')],
theme: {
screens: {
'sm': '420px',
'md': '768px',
'lg': '1024px',
'xl': '1366px',
'2xl': '1536px', //local
'3xl': '2048px', //local
},
}
}

View File

@ -0,0 +1,204 @@
import { expect, test } from 'vitest'
import { convertObject, convertList } from '@helpers/server-routes/adapter/adapter.js'
test('parse object from api', () => {
const targetObject = {
"id": 1,
"created_at": "2024-02-15T17:24:52.755254148+03:00",
"updated_at": "2024-02-15T17:24:52.755254148+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "172.25.78.36",
"internet_uri": "localhost",
"description": "localhost",
"is_online": true
}
const expectedObject = {
id: 1,
name2: "jsonplaceholder.typicode.com",
port2: 9965,
proxy_ip: "172.25.78.153",
site_ip: "172.25.78.36",
internet_uri: "localhost",
description: "localhost",
is_online: true,
}
const config = {
id: "id",
name: "name2",
port: "port2",
proxy_ip: "proxy_ip",
site_ip: "site_ip",
internet_uri: "internet_uri",
description: "description",
is_online: "is_online",
}
expect(convertObject(targetObject, {config})).toEqual(expectedObject)
})
test('adapt list objects without callback', () => {
const targetObject1 = {
"id": 1,
"created_at": "2024-02-15T17:24:52.755254148+03:00",
"updated_at": "2024-02-15T17:24:52.755254148+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "172.25.78.36",
"internet_uri": "localhost",
"description": "localhost",
"is_online": true
}
const targetObject2 = {
"id": 1,
"created_at": "2024-02-15T17:24:52.755254148+03:00",
"updated_at": "2024-02-15T17:24:52.755254148+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "172.25.78.36",
"internet_uri": "localhost",
"description": "localhost",
"is_online": true
}
const targetObject3 = {
"id": 1,
"created_at": "2024-02-15T17:24:52.755254148+03:00",
"updated_at": "2024-02-15T17:24:52.755254148+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "172.25.78.36",
"internet_uri": "localhost",
"description": "localhost",
"is_online": true
}
const targetList = [targetObject1, targetObject2, targetObject3]
const selfObject1 = {
id: 1,
name2: "jsonplaceholder.typicode.com",
port2: 9965,
proxy_ip: "172.25.78.153",
site_ip: "172.25.78.36",
internet_uri: "localhost",
description: "localhost",
is_online: true,
}
const selfObject2 = {
id: 1,
name2: "jsonplaceholder.typicode.com",
port2: 9965,
proxy_ip: "172.25.78.153",
site_ip: "172.25.78.36",
internet_uri: "localhost",
description: "localhost",
is_online: true,
}
const selfObject3 = {
id: 1,
name2: "jsonplaceholder.typicode.com",
port2: 9965,
proxy_ip: "172.25.78.153",
site_ip: "172.25.78.36",
internet_uri: "localhost",
description: "localhost",
is_online: true,
}
const selfList = [selfObject1, selfObject2, selfObject3]
const config = {
id: "id",
name: "name2",
port: "port2",
proxy_ip: "proxy_ip",
site_ip: "site_ip",
internet_uri: "internet_uri",
description: "description",
is_online: "is_online",
}
expect(convertList(targetList, {config})).toEqual(selfList)
})
test('adapt list objects with callback', () => {
const targetObject1 = {
"id": 1,
"created_at": "2024-02-15T17:24:52.755254148+03:00",
"updated_at": "2024-02-15T17:24:52.755254148+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "172.25.78.36",
"internet_uri": "localhost",
"description": "localhost",
"is_online": true
}
const targetObject2 = {
"id": 1,
"created_at": "2024-02-15T17:24:52.755254148+03:00",
"updated_at": "2024-02-15T17:24:52.755254148+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "172.25.78.36",
"internet_uri": "localhost",
"description": "localhost",
"is_online": false
}
const targetList = [targetObject1, targetObject2]
const expectedObject1 = {
id: 1,
name2: "jsonplaceholder.typicode.com",
port2: 9965,
proxy_ip: "172.25.78.153",
site_ip: "172.25.78.36",
internet_uri: "localhost",
description: "localhost",
state: "active",
}
const expectedObject2 = {
id: 1,
name2: "jsonplaceholder.typicode.com",
port2: 9965,
proxy_ip: "172.25.78.153",
site_ip: "172.25.78.36",
internet_uri: "localhost",
description: "localhost",
state: "disabled",
}
const expectedList = [expectedObject1, expectedObject2]
const config = {
id: "id",
name: "name2",
port: "port2",
proxy_ip: "proxy_ip",
site_ip: "site_ip",
internet_uri: "internet_uri",
description: "description",
}
const callback = (el, {is_online}) => ({...el, state: is_online ? "active": "disabled"})
expect(convertList(targetList, {config, callback})).toEqual(expectedList)
})

View File

@ -0,0 +1,310 @@
import {describe, expect, beforeEach} from 'vitest'
import {createStore} from 'vuex'
import {store} from '@store/modules/proxy/index.js'
import {addedNewRoute, updatedRoute, sortedRoutes, deletedRoute} from '@store/modules/proxy/helpers.js'
const selectedSite = store.state.selectedSite
const routes = store.state.routes
const routesState = store.state.routesState
const setSelectedSite = store.mutations.setSelectedSite
const setRoutes = store.mutations.setRoutes
const setRoutesState = store.mutations.setRoutesState
const addNewRouteLayout = store.actions.addNewRouteLayout
const uploadSiteRoutes = store.actions.uploadSiteRoutes
const updateRouteRow = store.actions.updateRouteRow
const updateRoutesWithApi = store.actions.updateRoutesWithApi
const removeRoute = store.actions.removeRoute
const resetStore = store.actions.resetStore
const defaultRoutes = [{
"id": 7,
"created_at": "2024-02-27T12:02:34.666042252+03:00",
"updated_at": "2024-02-27T12:02:34.666042252+03:00",
"deleted_at": null,
"server_id": -1,
"path": "/route",
"role": 0,
"description": "with route",
"order": 0,
"deepness": 0,
"is_cb_on": true,
"cb_request_limit": 0,
"cb_min_requests": 7,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 0,
"cb_open_state_timeout": 0,
"is_online": false
},
{
"id": 8,
"created_at": "2024-02-27T12:05:45.268921795+03:00",
"updated_at": "2024-02-27T12:05:45.268921795+03:00",
"deleted_at": null,
"server_id": 7,
"path": "new/3",
"role": 0,
"description": "",
"order": 0,
"deepness": 0,
"is_cb_on": false,
"cb_request_limit": 3,
"cb_min_requests": 5,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 0,
"cb_open_state_timeout": 0,
"is_online": true
},
{
"id": 9,
"created_at": "2024-02-27T12:11:09.940033992+03:00",
"updated_at": "2024-02-27T12:11:09.940033992+03:00",
"deleted_at": null,
"server_id": -1,
"path": "new/1",
"role": 0,
"description": "",
"order": 0,
"deepness": 9,
"is_cb_on": true,
"cb_request_limit": 0,
"cb_min_requests": 0,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 0,
"cb_open_state_timeout": 5,
"is_online": false
}]
const mockStore = createStore({
state: {
selectedSite: selectedSite,
routes: routes,
routesState: routesState,
},
getters: {
selectedSite: (state) => state.selectedSite,
},
mutations: {
setSelectedSite: setSelectedSite,
setRoutes: setRoutes,
setRoutesState: setRoutesState
},
actions: {
addNewRouteLayout: addNewRouteLayout,
uploadSiteRoutes: uploadSiteRoutes,
updateRouteRow: updateRouteRow,
updateRoutesWithApi: updateRoutesWithApi,
removeRoute: removeRoute,
resetStore
},
})
describe('actions', () => {
//eslint-disable-next-line no-undef
it('action addNewRouteLayout - added fields for new Route', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
mockStore.commit('setSelectedSite', {id: 9}) // added id for selected site, need to this action for bind server_id
mockStore.dispatch('addNewRouteLayout')
expect(mockStore.state.routes[0]).toMatchObject({action: "create", server_id: 9})
})
//eslint-disable-next-line no-undef
it('action updateRouteRow - edited fields values, in selected row', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const editedRoute = {
"id": 8,
"created_at": "2024-02-27T12:05:45.268921795+03:00",
"updated_at": "2024-02-27T12:05:45.268921795+03:00",
"deleted_at": null,
"server_id": 7,
"path": "new/3",
"role": 0,
"description": "Testing edited route",
"order": 0,
"deepness": 0,
"is_cb_on": true,
"cb_request_limit": 3,
"cb_min_requests": 5,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 7,
"cb_open_state_timeout": 0,
"is_online": true
}
mockStore.commit('setRoutes', defaultRoutes)
const currentRoute = mockStore.state.routes.find(route => route.id === 8)
expect(currentRoute).toMatchObject({"description": "", "is_cb_on": false, "cb_interval_duration": 0}) // check default values fields in selected route
mockStore.dispatch('updateRouteRow', editedRoute)
const updatedRoute = mockStore.state.routes.find(route => route.id === 8)
expect(updatedRoute).toMatchObject({"description": "Testing edited route", "is_cb_on": true, "cb_interval_duration": 7})
})
//eslint-disable-next-line no-undef
it('action updateRouteRow - edited fields, empty values, in selected row', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const editedRoute = {
"id": 8,
"created_at": "2024-02-27T12:05:45.268921795+03:00",
"updated_at": "2024-02-27T12:05:45.268921795+03:00",
"deleted_at": null,
"server_id": 7,
"path": "",
"role": 0,
"description": null,
"order": 0,
"deepness": 0,
"is_cb_on": true,
"cb_request_limit": 0,
"cb_min_requests": 5,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 7,
"cb_open_state_timeout": 0,
"is_online": true
}
mockStore.commit('setRoutes', defaultRoutes)
const currentRoute = mockStore.state.routes.find(route => route.id === 8)
expect(currentRoute).toMatchObject({"cb_request_limit": 3, "path": "new/3", "description": ""}) // check default values fields in selected route
mockStore.dispatch('updateRouteRow', editedRoute)
const updatedRoute = mockStore.state.routes.find(route => route.id === 8)
expect(updatedRoute).toMatchObject({"cb_request_limit": 0, "path": "", "description": null})
})
})
describe('helpers', () => {
//eslint-disable-next-line no-undef
it('added new route to site', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const editedSite = {
"id": 3,
"created_at": "2024-02-26T17:50:07.191225008+03:00",
"updated_at": "2024-02-28T09:41:49.274089436+03:00",
"deleted_at": null,
"name": "new",
"port": 3645,
"proxy_ip": "172.25.78.151",
"site_ip": "172.25.78.151",
"internet_uri": "",
"description": "new updated...",
"is_online": false
}
const newRoute = {
'action': "create",
"path": "",
"role": 0,
"description": "9",
"order": 0,
"deepness": 7,
"is_cb_on": false,
"cb_request_limit": 0,
"cb_min_requests": 0,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 3,
"cb_open_state_timeout": 0,
"is_online": true
}
mockStore.commit('setRoutes', [newRoute, ...defaultRoutes])
const updatedRoutes = addedNewRoute(editedSite, mockStore.state.routes)
mockStore.commit('setRoutes', updatedRoutes)
const addedNewRouteToStore = mockStore.state.routes.find(route => route.action === 'create')
expect(addedNewRouteToStore).not.toBe(undefined)
expect(editedSite.id).toEqual(addedNewRouteToStore.server_id) // if true then added new route successfully
})
//eslint-disable-next-line no-undef
it('updated route', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const editedRoute = {
"id": 8,
"created_at": "2024-02-27T12:05:45.268921795+03:00",
"updated_at": "2024-02-27T12:05:45.268921795+03:00",
"deleted_at": null,
"server_id": 7,
"path": "new/3",
"role": 0,
"description": "",
"order": 0,
"deepness": 9,
"is_cb_on": true,
"cb_request_limit": 3,
"cb_min_requests": 5,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 7,
"cb_open_state_timeout": 0,
"is_online": true
}
mockStore.commit('setRoutes', defaultRoutes)
const currentRouteBeforeEdit = mockStore.state.routes.find(route => route.id === 8)
expect(currentRouteBeforeEdit).toMatchObject({"deepness": 0, is_cb_on: false, "cb_interval_duration": 0}) // default params route
const updatedRoutes = updatedRoute(editedRoute, mockStore.state.routes)
mockStore.commit('setRoutes', updatedRoutes)
const updatedRouteToStore = mockStore.state.routes.find(route => route.id === 8)
expect(updatedRouteToStore).not.toBe(undefined)
expect(updatedRouteToStore).toMatchObject({"deepness": 9, is_cb_on: true, "cb_interval_duration": 7}) // check new params updated route
})
//eslint-disable-next-line no-undef
it('deleted route', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const deleteRouteId = 9
mockStore.commit('setRoutes', defaultRoutes)
const currentRouteBeforeDelete = mockStore.state.routes.find(route => route.id === 9)
expect(currentRouteBeforeDelete).toMatchObject({id: 9}) // exists route before deleteting
const updatedRoutes = deletedRoute(deleteRouteId, mockStore.state.routes)
mockStore.commit('setRoutes', updatedRoutes)
const deletedRouteToStore = mockStore.state.routes.find(route => route.id === 9)
expect(deletedRouteToStore).toMatchObject({action: 'remove'}) // to deleted route to be added prop action with value 'remove'
})
//eslint-disable-next-line no-undef
it('sorted routes - sort in descending order, sorted prop: updated_at', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const newRoute = {
"id": 10,
"created_at": "2024-02-27T12:12:53.295785444+03:00",
"updated_at": "2024-02-27T12:12:53.295785444+03:00",
"deleted_at": null,
"server_id": -1,
"path": "new/12",
"role": 0,
"description": "",
"order": 1,
"deepness": 0,
"is_cb_on": false,
"cb_request_limit": 3,
"cb_min_requests": 5,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 0,
"cb_open_state_timeout": 0,
"is_online": true
}
mockStore.commit('setRoutes', [...defaultRoutes, newRoute])
const newRouteIdxBeforeSorting = mockStore.state.routes.findIndex(route => route.id === 10)
expect(newRouteIdxBeforeSorting).toBe(3) // default last index this new route in test
const updatedRoutes = sortedRoutes(mockStore.state.routes)
mockStore.commit('setRoutes', updatedRoutes)
const newRouteIdxAfterSorting = mockStore.state.routes.findIndex(route => route.id === 10)
expect(newRouteIdxAfterSorting).toBe(0) // check route with id 10 moved to first index after sorting, because new route have updated_at === created_at for nre routes
})
})

View File

@ -0,0 +1,345 @@
import {describe, expect, beforeEach} from 'vitest'
import {createStore} from 'vuex'
import {store} from '@store/modules/proxy/index.js'
import {addedSite, updatedSite, removedNewSite, deletedSite} from '@store/modules/proxy/helpers.js'
const sites = store.state.sites
const selectedSite = store.state.selectedSite
const isSaveData = store.state.isSaveData
const routes = store.state.routes
const routesState = store.state.routesState
const setSites = store.mutations.setSites
const setSelectedSite = store.mutations.setSelectedSite
const setIsSaveData = store.mutations.setIsSaveData
const setRoutes = store.mutations.setRoutes
const setRoutesState = store.mutations.setRoutesState
const addNewSiteLayout = store.actions.addNewSiteLayout
const createNewSite = store.actions.createNewSite
const breakeAddingSite = store.actions.breakeAddingSite
const editSelectedSite = store.actions.editSelectedSite
const saveSite = store.actions.saveSite
const breakSavingSite = store.actions.breakSavingSite
const removeSite = store.actions.removeSite
const resetStore = store.actions.resetStore
const defaultSites = [ {
"id": 1,
"created_at": "2024-02-22T17:08:37.715772388+03:00",
"updated_at": "2024-02-26T14:11:38.64094899+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "172.25.78.153",
"internet_uri": "localhost",
"description": "localhost",
"is_online": true
},
{
"id": 2,
"created_at": "0001-01-01T00:00:00Z",
"updated_at": "2024-02-27T17:53:14.070484767+03:00",
"deleted_at": null,
"name": "new",
"port": 3645,
"proxy_ip": "172.25.78.151",
"site_ip": "172.25.78.151",
"internet_uri": "",
"description": "create... upd...",
"is_online": true
},
{
"id": 3,
"created_at": "2024-02-26T17:50:07.191225008+03:00",
"updated_at": "2024-02-28T09:41:49.274089436+03:00",
"deleted_at": null,
"name": "new",
"port": 3645,
"proxy_ip": "172.25.78.151",
"site_ip": "172.25.78.151",
"internet_uri": "",
"description": "new updated...",
"is_online": false
}]
const mockStore = createStore({
state: {
sites: sites,
selectedSite: selectedSite,
isSaveData: isSaveData,
routes: routes,
routesState: routesState,
},
getters: {
sites: (state) => state.sites,
selectedSite: (state) => state.selectedSite,
},
mutations: {
setSites: setSites,
setSelectedSite: setSelectedSite,
setIsSaveData: setIsSaveData,
setRoutes: setRoutes,
setRoutesState: setRoutesState
},
actions: {
addNewSiteLayout: addNewSiteLayout,
createNewSite: createNewSite,
breakeAddingSite: breakeAddingSite,
editSelectedSite: editSelectedSite,
saveSite: saveSite,
breakSavingSite: breakSavingSite,
removeSite: removeSite,
resetStore
},
})
describe('mutations', () => {
//eslint-disable-next-line no-undef
it('Set to empty Services', () => {
const state = { sites: sites }
setSites(state, null)
expect(state.sites).to.equal(null)
})
//eslint-disable-next-line no-undef
it('Add new Service', () => {
const newSite = {
"id": 34,
"created_at": "2024-02-28T15:32:21.187767309+03:00",
"updated_at": "2024-02-28T15:32:50.820118413+03:00",
"deleted_at": null,
"name": "Test site 10",
"port": 7575,
"proxy_ip": "172.25.78.151",
"site_ip": "172.25.78.151",
"internet_uri": "",
"description": "10 test create",
"is_online": true
}
const updatedSites = [...sites, newSite]
const state = { sites: sites }
setSites(state, updatedSites)
expect(state.sites[0]).to.toMatchObject({"name": "Test site 10", "port": 7575, "id": 34, "is_online": true})
})
})
describe('actions', () => {
//eslint-disable-next-line no-undef
it('action addNewSiteLayout - added fields for new Service', async () => {
mockStore.dispatch('addNewSiteLayout')
expect(mockStore.state.sites[0]).toMatchObject({id: -1})
})
//eslint-disable-next-line no-undef
it('action editSelectedSite - edited fields values in selected site', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
mockStore.commit('setSelectedSite', {id: 7, name: 'custom test name', port: 3333, description: 'default'}) // added params - key, vakue for selected site
mockStore.dispatch('editSelectedSite', {key: 'name', value: 'edited test name'})
mockStore.dispatch('editSelectedSite', {key: 'port', value: 5555})
mockStore.dispatch('editSelectedSite', {key: 'description', value: 'updated'})
expect(mockStore.state.selectedSite).toMatchObject({name: 'edited test name', port: 5555, description: 'updated'})
})
//eslint-disable-next-line no-undef
it('action editSelectedSite - edited fields empty values in selected site', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
mockStore.commit('setSelectedSite', {id: 7, name: '', port: '', description: ''}) // added params - key, vakue for selected site
mockStore.dispatch('editSelectedSite', {key: 'name', value: ''})
mockStore.dispatch('editSelectedSite', {key: 'port', value: ''})
mockStore.dispatch('editSelectedSite', {key: 'description', value: ''})
expect(mockStore.state.selectedSite).toMatchObject({name: '', port: '', description: ''})
})
//eslint-disable-next-line no-undef
it('action breakSavingSite - cleared selected site, replaced to default status routesState', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
mockStore.commit('setSelectedSite', {id: 9, name: 'custom test name', port: 3333, description: 'default'}) // added params - key, vakue for selected site
mockStore.commit('setRoutesState', 'active') // added status for RoutesState
expect(mockStore.state.routesState).toBe('active') // check RoutesState status
mockStore.dispatch('breakSavingSite', {key: 'name', value: 'edited test name'})
expect(mockStore.state.selectedSite).toBeNull()
expect(mockStore.state.routesState).toBe('await')
})
//eslint-disable-next-line no-undef
it('action resetStore - cleared all values in store', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
expect(mockStore.state.selectedSite).toBeNull() // check default statuses
expect(mockStore.state.routesState).toBe('await') // check default statuses
expect(mockStore.state.isSaveData).toBe(false) // check default statuses
mockStore.commit('setSelectedSite', {id: 9, name: 'custom test name', port: 3333, description: 'default'}) // added params - key, vakue for selected site
mockStore.commit('setRoutesState', 'active') // updated status for RoutesState
mockStore.commit('setIsSaveData', true) // updated status for isSaveData
expect(mockStore.state.selectedSite).toMatchObject({id: 9, name: 'custom test name', port: 3333, description: 'default'}) // check updated statuses
expect(mockStore.state.routesState).toBe('active') // check updated statuses
expect(mockStore.state.isSaveData).toBe(true) // check updated statuses
mockStore.dispatch('resetStore')
expect(mockStore.state.selectedSite).toBeNull() // again check default statuses
expect(mockStore.state.routesState).toBe('await') // again check default statuses
expect(mockStore.state.isSaveData).toBe(false) // again check default statuses
})
})
describe('helpers', () => {
//eslint-disable-next-line no-undef
it('added new site', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const newSite = {
"id": 34,
"created_at": "2024-02-28T15:32:21.187767309+03:00",
"updated_at": "2024-02-28T15:32:50.820118413+03:00",
"deleted_at": null,
"name": "new site 9",
"port": 7475,
"proxy_ip": "172.25.78.151",
"site_ip": "172.25.78.151",
"internet_uri": "",
"description": "9 create",
"is_online": true
}
mockStore.commit('setSites', defaultSites)
const updatedSites = addedSite(newSite, mockStore.state.sites)
mockStore.commit('setRoutes', updatedSites)
const addedNewSite = mockStore.state.routes.find(route => route.id === 34)
expect(addedNewSite).not.toBe(undefined)
expect(addedNewSite).toMatchObject(newSite)
})
//eslint-disable-next-line no-undef
it('updated site', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const editedSite = {
"id": 3,
"created_at": "2024-02-26T17:50:07.191225008+03:00",
"updated_at": "2024-02-28T09:41:49.274089436+03:00",
"deleted_at": null,
"name": "edited test name",
"port": 5555,
"proxy_ip": "172.25.78.151",
"site_ip": "172.25.78.151",
"internet_uri": "",
"description": "new updated... updated site",
"is_online": true
}
mockStore.commit('setSites', defaultSites)
const currentSiteBeforeEdit = mockStore.state.sites.find(site => site.id === 3)
expect(currentSiteBeforeEdit).toMatchObject({name: 'new', port: 3645, description: 'new updated...'}) // default params site
const updatedSites = updatedSite(editedSite, mockStore.state.sites)
mockStore.commit('setSites', updatedSites)
const updatedSiteToStore = mockStore.state.sites.find(site => site.id === 3)
expect(updatedSiteToStore).not.toBe(undefined)
expect(updatedSiteToStore).toMatchObject(editedSite)
})
//eslint-disable-next-line no-undef
it('updated site - empty params', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const editedSite = {
"id": 3,
"created_at": "2024-02-26T17:50:07.191225008+03:00",
"updated_at": "2024-02-28T09:41:49.274089436+03:00",
"deleted_at": null,
"name": "",
"port": null,
"proxy_ip": "172.25.78.151",
"site_ip": "172.25.78.151",
"internet_uri": "",
"description": "",
"is_online": true
}
mockStore.commit('setSites', defaultSites)
const currentSiteBeforeEdit = mockStore.state.sites.find(site => site.id === 3)
expect(currentSiteBeforeEdit).toMatchObject({name: 'new', port: 3645, description: 'new updated...'}) // default params site
const updatedSites = updatedSite(editedSite, mockStore.state.sites)
mockStore.commit('setSites', updatedSites)
const updatedSiteToStore = mockStore.state.sites.find(site => site.id === 3)
expect(updatedSiteToStore).not.toBe(undefined)
expect(updatedSiteToStore).toMatchObject(editedSite)
})
//eslint-disable-next-line no-undef
it('removed new site before saving to server', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const newSite = {"port": "", "name": "", id: -1}
mockStore.commit('setSites', defaultSites)
const sitesWithNewSite = addedSite(newSite, mockStore.state.sites)
mockStore.commit('setSites', sitesWithNewSite)
const currentSiteBeforeDelete = mockStore.state.sites[0]
expect(currentSiteBeforeDelete).toMatchObject({id: -1}) // default id for new site if this not saving to server
const updatedSites = removedNewSite(mockStore.state.sites)
mockStore.commit('setSites', updatedSites)
const removedSiteToStore = mockStore.state.sites.find(site => site.id === -1)
expect(removedSiteToStore).toBe(undefined) // removed site to be undefined in array sites to store
})
//eslint-disable-next-line no-undef
it('deleted selected site before saving to server', async () => {
beforeEach(() => {
mockStore.dispatch('resetStore')
})
const deleteSiteId = 3
mockStore.commit('setSites', defaultSites)
const currentSiteBeforeDelete = mockStore.state.sites.find(site => site.id === 3)
expect(currentSiteBeforeDelete).toMatchObject({id: 3}) // exists site before deleteting
const updatedSites = deletedSite(deleteSiteId, mockStore.state.sites)
mockStore.commit('setSites', updatedSites)
const deletedSiteToStore = mockStore.state.sites.find(site => site.id === 3)
expect(deletedSiteToStore).toBe(undefined) // deleted site to be undefined in array sites to store
})
})
// describe('class Services', () => {
// it('upload Sites', async () => {
// // const services = new Services(import.meta.env.VITE_API_ADDR, config)
// const equalService = [{
// "id": 1,
// "created_at": "2024-02-22T17:08:37.715772388+03:00",
// "updated_at": "2024-02-26T14:11:38.64094899+03:00",
// "deleted_at": null,
// "name": "jsonplaceholder.typicode.com",
// "port": 9965,
// "proxy_ip": "172.25.78.153",
// "site_ip": "172.25.78.153",
// "internet_uri": "localhost",
// "description": "localhost",
// "is_online": true
// }]
// vi.fn(axios.get).mockResolvedValue({
// data: equalService,
// })
// const services = new Services(import.meta.env.VITE_API_ADDR, config)
// const uploadedServices = await services.getServices()
// mockStore.dispatch('uploadSites')
// expect(uploadedServices[0]).to.toMatchObject(equalService[0])
// })
// })

View File

@ -0,0 +1,25 @@
import { mount } from '@vue/test-utils'
import { expect, test, describe } from 'vitest'
import PageHeader from "@atoms/PageHeader.vue"
import { createStore } from 'vuex'
import {store as proxy} from '@/store/modules/proxy';
describe("tests PageHeader with vuex", () => {
const store = createStore({
plugins: [],
modules: {
proxy
},
})
test('tests PageHeader with vuex', async () => {
const wrapper = mount(PageHeader, {
global: {
plugins: [store]
}
})
expect(wrapper.text()).toContain('На главную')
})
})

View File

@ -0,0 +1,177 @@
import {mount} from '@vue/test-utils'
import {expect, it, describe} from 'vitest'
import RoutesList from "@organisms/RoutersEditor/index.vue"
// import RoutersEditor from "@organisms/RoutersEditor/index.vue"
import routeOptions from '@store/modules/proxy/routeOptions.json'
import {createStore} from 'vuex'
const defaultRoutes = [{
"id": 7,
"created_at": "2024-02-27T12:02:34.666042252+03:00",
"updated_at": "2024-02-27T12:02:34.666042252+03:00",
"deleted_at": null,
"server_id": -1,
"path": "/route",
"role": 0,
"description": "with route",
"order": 0,
"deepness": 0,
"is_cb_on": true,
"cb_request_limit": 0,
"cb_min_requests": 7,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 0,
"cb_open_state_timeout": 0,
"is_online": false
},
{
"id": 8,
"created_at": "2024-02-27T12:05:45.268921795+03:00",
"updated_at": "2024-02-27T12:05:45.268921795+03:00",
"deleted_at": null,
"server_id": 7,
"path": "new/3",
"role": 0,
"description": "",
"order": 0,
"deepness": 0,
"is_cb_on": false,
"cb_request_limit": 3,
"cb_min_requests": 5,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 0,
"cb_open_state_timeout": 0,
"is_online": true
},
{
"id": 9,
"created_at": "2024-02-27T12:11:09.940033992+03:00",
"updated_at": "2024-02-27T12:11:09.940033992+03:00",
"deleted_at": null,
"server_id": -1,
"path": "new/1",
"role": 0,
"description": "",
"order": 0,
"deepness": 9,
"is_cb_on": true,
"cb_request_limit": 0,
"cb_min_requests": 0,
"cb_error_threshold_percentage": 0,
"cb_interval_duration": 0,
"cb_open_state_timeout": 5,
"is_online": false
}]
const store = createStore({
plugins: [],
modules: {
proxy: {
state: {
routes: [],
routesState: "await",
routesLib: {},
routeOptions: routeOptions
},
getters: {
routes: (state) => state.routes,
routesState: (state) => state.routesState,
routesLib: (state) => state.routesLib,
routeOptions: (state) => state.routeOptions,
},
mutations: {
setRoutes: (state, updval) => state.routes = updval,
setRoutesState: (state, updval) => state.routesState = updval,
setRoutesLib: (state, updval) => state.routesLib = updval,
},
actions: {
uploadSiteRoutes: async ({commit}, siteProps) => {
commit('setSelectedSite', siteProps)
commit('setRoutes', defaultRoutes)
},
updateRouteRow: () => ({}),
removeRoute: () => ({}),
breateAddingRoute: () => ({}),
},
namespaced: true,
}
}
})
describe("Routes", () => {
it("Not renders routes and buttons in RoutesList, if routesState value not 'active'", async () => {
const wrapper = mount(RoutesList, {
global: {
plugins: [store],
},
shallow: true,
})
expect(wrapper.html()).not.toContain('Добавить роут')
expect(wrapper.html()).not.toContain('Закрыть')
})
it("Not renders routes - empty array in RoutesList, if routesState value 'active', renders buttons", async () => {
store.commit('proxy/setRoutesState', 'active')
const getters = {...store.getters }
const routesState = getters['proxy/routesState']
const wrapper = mount(RoutesList, {
global: {
plugins: [store],
},
data() {
return {
open: false,
}
},
})
if (routesState === 'active') {
await wrapper.setData({ open: true })
}
expect(wrapper.html()).toContain('Добавить роут')
expect(wrapper.html()).toContain('Закрыть')
expect(wrapper.html()).not.toContain('ri-delete-bin-line')
})
it("Renders routes and buttons in RoutesList, if routesState value 'active' and not empty routes array", async () => {
store.commit('proxy/setRoutesState', 'active')
store.commit('proxy/setRoutes', defaultRoutes)
const getters = {...store.getters }
const routesState = getters['proxy/routesState']
const routes = getters['proxy/routes']
const wrapper = mount(RoutesList, {
global: {
plugins: [store],
},
data() {
return {
open: false,
}
},
})
console.log('routes 3 test', routes)
if (routesState === 'active') {
await wrapper.setData({ open: true })
}
console.log('wrapper 3 test', wrapper.html())
expect(wrapper.html()).toContain('Добавить роут')
expect(wrapper.html()).toContain('Закрыть')
// expect(wrapper.html()).toContain('ri-delete-bin-line')
})
})

View File

@ -0,0 +1,306 @@
import SiteList from '@organisms/SitesEditor/SiteList.vue'
import SiteCard from '@organisms/SitesEditor/SiteCard.vue'
import EditCard from '@organisms/SitesEditor/EditCard.vue'
import Input from '@atoms/Input.vue'
import Textarea from '@atoms/Textarea.vue'
import DoubleSwitch from '@atoms/DoubleSwitch.vue'
import {mount} from '@vue/test-utils'
import {createStore} from 'vuex'
import {describe, expect, it} from 'vitest'
const defaultSites = [ {
"id": 1,
"created_at": "2024-02-22T17:08:37.715772388+03:00",
"updated_at": "2024-02-26T14:11:38.64094899+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "172.25.78.153",
"internet_uri": "localhost",
"description": "localhost",
"is_online": true
},
{
"id": 2,
"created_at": "0001-01-01T00:00:00Z",
"updated_at": "2024-02-27T17:53:14.070484767+03:00",
"deleted_at": null,
"name": "new",
"port": 3645,
"proxy_ip": "172.25.78.151",
"site_ip": "172.25.78.151",
"internet_uri": "",
"description": "create... upd...",
"is_online": true
},
{
"id": 3,
"created_at": "2024-02-26T17:50:07.191225008+03:00",
"updated_at": "2024-02-28T09:41:49.274089436+03:00",
"deleted_at": null,
"name": "new",
"port": 3645,
"proxy_ip": "172.25.78.151",
"site_ip": "172.25.78.151",
"internet_uri": "",
"description": "new updated...",
"is_online": false
}]
const store = createStore({
plugins: [],
modules: {
proxy: {
state: {
sites: [],
sitesState: 'loading',
selectedSite: null,
isSaveData: false,
},
getters: {
sites: (state) => state.sites,
sitesState: (state) => state.sitesState,
selectedSite: (state) => state.selectedSite,
isSaveData: (state) => state.isSaveData,
},
mutations: {
setSites: (state, updval) => state.sites = updval,
setSitesState: (state, updval) => state.sitesState = updval,
setSelectedSite: (state, updval) => state.selectedSite = updval,
setIsSaveData: (state, updval) => state.isSaveData = updval,
},
actions: {
uploadSites({commit}, sites) {
commit('setSites', sites)
},
uploadSiteRoutes: async ({commit}, siteProps) => {
commit('setSelectedSite', siteProps)
}
},
namespaced: true,
}
}
})
describe("Services", () => {
it("Not renders SiteCard and NewSiteButton in SiteList, if sitesState value not 'active', renders SiteList, fwb-spinner", async () => {
const wrapper = mount(SiteList, {
global: {
plugins: [store],
},
shallow: true,
})
expect(wrapper.html()).not.toContain('Добавить сайт')
expect(wrapper.find(".text-slate-700").text()).toBe("Загрузка сайтов...")
})
it("Not renders fwb-spinner, SiteCard if sitesState value 'active', sites empty array, renders SiteList, NewSiteButton", async () => {
store.commit('proxy/setSitesState', 'active')
const wrapper = mount(SiteList, {
global: {
plugins: [store],
},
})
expect(wrapper.html()).not.toContain('Загрузка сайтов...')
expect(wrapper.find('span').exists()).toBe(false)
expect(wrapper.html()).toContain('Добавить сайт')
})
it("Not renders fwb-spinner, sites array not empty, sitesState value 'active', renders SiteList, NewSiteButton, SiteCard", async () => {
store.dispatch('proxy/uploadSites', defaultSites)
store.commit('proxy/setSitesState', 'active')
const wrapper = mount(SiteList, {
global: {
mocks: {
$store: store
}
},
})
expect(wrapper.find('span').exists()).toBe(true)
expect(wrapper.find('i').exists()).toBe(true)
expect(wrapper.html()).toContain('ri-pencil-line')
expect(wrapper.html()).toContain('Добавить сайт')
})
it("dynamicHeight null in SiteList component", async () => {
store.dispatch('proxy/uploadSites', defaultSites)
store.commit('proxy/setSitesState', 'active')
// const getters = {...store.getters }
// console.log('sites 3 test', getters['proxy/sites'])
const wrapper = mount(SiteList, {
global: {
mocks: {
$store: store
}
},
data() {
return {
dynamicHeight: null
}
},
shallow: true,
})
console.log('wrapper.html', wrapper.html())
expect(wrapper.classes()).not.toContain('shadow-lg')
expect(wrapper.classes()).not.toContain('p-3')
expect(wrapper.html()).not.toContain('style="max-height: 100px;"')
})
it("set dynamicHeight, this data prop not null in SiteList component", async () => {
console.log('store - 5 test', store)
store.dispatch('proxy/uploadSites', defaultSites)
store.commit('proxy/setSitesState', 'active')
const wrapper = mount(SiteList, {
global: {
plugins: [store],
},
data() {
return {
dynamicHeight: null
}
},
shallow: true,
})
await wrapper.setData({ dynamicHeight: 100 })
expect(wrapper.html()).toContain('style="max-height: 100px;"')
})
it("renders buttons - 'Отменить' and 'Удалить' in this site, after click to delete button", async () => {
const wrapper = mount(SiteCard, {
global: {
plugins: [store],
},
data () {
return {
isDelete: false,
}
},
})
await wrapper.setData({ isDelete: true })
await wrapper.get('.ri-delete-bin-line').trigger('click')
expect(wrapper.html()).toContain('Отменить')
expect(wrapper.html()).toContain('Удалить')
})
it("render EditCard, ids selectedSite and prop id is Equal", async () => {
const editSite = {
"id": 1,
"created_at": "2024-03-04T14:30:40.64845074+03:00",
"updated_at": "2024-03-04T14:30:40.64845074+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "https://jsonplaceholder.typicode.com/",
"internet_uri": "localhost",
"description": "localhost",
"is_online": true
}
store.dispatch('proxy/uploadSites', defaultSites)
store.commit('proxy/setSitesState', 'active')
store.dispatch('proxy/uploadSiteRoutes', editSite)
const wrapper = mount(EditCard, {
global: {
plugins: [store],
stubs: ['SiteCard', 'EditCard'],
},
props: {
id: 1,
},
})
const editCard = wrapper.getComponent(EditCard)
expect(editCard.props('id')).toEqual(editSite.id)
expect(wrapper.html()).toContain('ri-close-line')
expect(wrapper.html()).toContain('Онлайн')
})
it("render EditCard, set fields in selectedSite", async () => {
const editSite = {
"id": 1,
"created_at": "2024-03-04T14:30:40.64845074+03:00",
"updated_at": "2024-03-04T14:30:40.64845074+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "https://jsonplaceholder.typicode.com/",
"internet_uri": "localhost",
"description": "localhost",
"is_online": true
}
store.dispatch('proxy/uploadSites', defaultSites)
store.commit('proxy/setSitesState', 'active')
store.dispatch('proxy/uploadSiteRoutes', editSite)
expect(store.getters['proxy/selectedSite']).toEqual(editSite) // check default props values in selected site
store.commit('proxy/setSelectedSite', {...editSite, is_online: false, name: 'edit name in EditCard', description: 'edit description in EditCard'})
const wrapper = mount(EditCard, {
global: {
plugins: [store],
stubs: ['Input', 'Textarea', 'DoubleSwitch', 'EditCard'],
},
props: {
id: 1,
},
})
const editCard = wrapper.getComponent(EditCard)
const inputField = wrapper.getComponent(Input)
const textareaField = wrapper.getComponent(Textarea)
const doubleSwitchField = wrapper.getComponent(DoubleSwitch)
const nameField = inputField.props().value
const descriptionField = textareaField.props().value
const isOnlineField = doubleSwitchField.props().isCheck
const getters = {...store.getters }
const name = getters['proxy/selectedSite'].name // selected site name
const description = getters['proxy/selectedSite'].description // selected site description
const isOnline = getters['proxy/selectedSite'].is_online // selected site is_online
expect(editCard.props('id')).toEqual(editSite.id)
expect(name).toEqual(nameField)
expect(description).toEqual(descriptionField)
expect(isOnline).toEqual(isOnlineField)
})
})

View File

@ -0,0 +1,65 @@
import { mount} from '@vue/test-utils'
import { expect, test, describe, vi } from 'vitest'
import App from '@/App.vue';
import { createStore } from 'vuex'
import {store as proxy} from '@/store/modules/proxy';
import axios from "axios";
vi.mock('axios')
describe("tests App mounted with vuex", () => {
const store = createStore({
global: {
stubs: {
}
},
plugins: [],
modules: {
proxy
},
})
axios.get.mockResolvedValue({
data: [
{
"id": 1,
"created_at": "2024-02-22T17:08:37.715772388+03:00",
"updated_at": "2024-02-26T14:11:38.64094899+03:00",
"deleted_at": null,
"name": "jsonplaceholder.typicode.com",
"port": 9965,
"proxy_ip": "172.25.78.153",
"site_ip": "172.25.78.153",
"internet_uri": "localhost",
"description": "localhost",
"is_online": true
},
{
"id": 2,
"created_at": "0001-01-01T00:00:00Z",
"updated_at": "2024-02-26T17:38:00.255479017+03:00",
"deleted_at": null,
"name": "new",
"port": 3645,
"proxy_ip": "172.25.78.151",
"site_ip": "172.25.78.151",
"internet_uri": "",
"description": "create...",
"is_online": true
}
],
})
test('tests App mounted with vuex', async () => {
const wrapper = mount(App, {
global: {
plugins: [store]
}
})
expect(wrapper.text()).toContain('На главную')
})
})

View File

@ -0,0 +1,24 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {fileURLToPath, URL} from 'node:url';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('/src', import.meta.url)),
'@store': fileURLToPath(new URL('/src/store', import.meta.url)),
'@cmp': fileURLToPath(new URL('/src/components', import.meta.url)),
'@helpers': fileURLToPath(new URL('/src/helpers', import.meta.url)),
'@atoms': fileURLToPath(new URL('./src/components/1_atoms', import.meta.url)),
'@molecules': fileURLToPath(new URL('./src/components/2_molecules', import.meta.url)),
'@organisms': fileURLToPath(new URL('./src/components/3_organisms', import.meta.url)),
'@pages': fileURLToPath(new URL('./src/components/4_pages', import.meta.url)),
}
},
test: {
globals:true,
environment: 'happy-dom',
}
})

5
users-manage/.env Normal file
View File

@ -0,0 +1,5 @@
VITE_API_ADDR="http://172.25.78.64:8087"
VITE_USERS_MODE="dev"
VITE_SCAND_API_URL="http://172.25.78.64:9999/scand/api/get_data"
VITE_SCAND_API_ADDR="http://172.25.78.64:9999"

View File

@ -0,0 +1,29 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended",
],
rules: {
"no-multiple-empty-lines": ["error", {max: 2, "maxBOF": 0}],
"indent": ["error", 2],
"no-mixed-spaces-and-tabs": "error",
"vue/attribute-hyphenation": "off",
"vue/html-self-closing": ["error", {
"html": {
"void": "never",
"normal": "always",
"component": "always"
},
"svg": "always",
"math": "always"
}],
"vue/no-reserved-component-names": "off",
"vue/html-indent": ["error", 2, {
"attribute": 1,
"baseIndent": 1,
"closeBracket": 0,
"alignAttributesVertically": true,
"ignores": []
}]
}
}

24
users-manage/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
users-manage/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

48
users-manage/Makefile Normal file
View File

@ -0,0 +1,48 @@
HOOK_NAME=pre-push
HOOK_PATH=../.git/hooks/$(HOOK_NAME)
restart:
docker build -t proxy-ui-vue .
docker run -d --name proxy-ui-vue --rm -p 5000:80 proxy-ui-vue
stoprestart:
docker stop proxy-ui-vue
docker build -t proxy-ui-vue .
docker run -d --name proxy-ui-vue --rm -p 5000:80 proxy-ui-vue
rmrestart:
docker image rm proxy-ui-vue
docker build -t proxy-ui-vue .
docker stop proxy-ui-vue
docker run -d --name proxy-ui-vue --rm -p 5000:80 proxy-ui-vue
prehook:
@echo "Setting up pre-push hook..."
@rm -f $(HOOK_PATH)
@echo '#!/bin/sh' >> $(HOOK_PATH)
@echo 'echo "Running lint for users-manage"' >> $(HOOK_PATH)
@echo 'cd ./users-manage' >> $(HOOK_PATH)
@echo 'yarn lint' >> $(HOOK_PATH)
@echo 'if [ $$? -ne 0 ]; then' >> $(HOOK_PATH)
@echo ' echo "Tests failed, push aborted."' >> $(HOOK_PATH)
@echo ' exit 1' >> $(HOOK_PATH)
@echo 'fi' >> $(HOOK_PATH)
@echo 'echo "Running tests for proxy-ui"' >> $(HOOK_PATH)
@echo 'yarn test_ones' >> $(HOOK_PATH)
@echo 'if [ $$? -ne 0 ]; then' >> $(HOOK_PATH)
@echo ' echo "Tests failed, push aborted."' >> $(HOOK_PATH)
@echo ' exit 1' >> $(HOOK_PATH)
@echo 'fi' >> $(HOOK_PATH)
@echo 'exit 0' >> $(HOOK_PATH)
@chmod +x $(HOOK_PATH)
@echo "Pre-push hook set successfully."
push:
ifeq ($(commit),)
$(error mn is not set)
endif
make prehook
git add . && git commit -m "feat($(commit)):" && git push

7
users-manage/README.md Normal file
View File

@ -0,0 +1,7 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).

17
users-manage/index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/css/remixicons/remixicon.css">
<link rel="stylesheet" href="/css/fonts.css">
<link rel="stylesheet" href="/css/global.css">
<link rel="stylesheet" href="/css/components/2_tabulator-table.css">
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

55
users-manage/package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "users-manage",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test_dom": "vitest --dom",
"test_dom_ones": "vitest run --dom",
"test": "vitest",
"test_ones": "vitest run",
"lint": "yarn eslint './**/*.{js,vue}'",
"lint_fix": "yarn eslint './**/*.{js,vue}' --fix"
},
"dependencies": {
"axios": "^1.6.7",
"flowbite": "^2.3.0",
"flowbite-vue": "^0.1.2",
"handlebars": "^4.7.8",
"moment": "^2.30.1",
"postcss-import": "^16.0.1",
"ramda": "^0.29.1",
"ramda-vue": "^0.0.9",
"tabulator-tables": "5.5.2",
"tippy.js": "^6.3.7",
"vue": "^3.4.15",
"vue-multiselect": "^2.1.8",
"vue-router": "^4.2.5",
"vue-tippy": "^6.4.1",
"vuex": "^4.1.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.21.1",
"happy-dom": "^13.6.2",
"postcss": "^8.4.35",
"postcss-advanced-variables": "^3.0.1",
"postcss-apply": "^0.12.0",
"postcss-nesting": "^12.0.3",
"tailwindcss": "^3.4.1",
"vite": "^5.1.0",
"vitest": "^1.3.1"
},
"resolutions": {
"strip-ansi": "6.0.1",
"string-width": "4.2.2",
"wrap-ansi": "7.0.0"
}
}

Some files were not shown because too many files have changed in this diff Show More