Compare commits
No commits in common. "main" and "dev" have entirely different histories.
4
.browserslistrc
Normal file
4
.browserslistrc
Normal file
@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
||||
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
69
.eslintrc-auto-import.json
Normal file
69
.eslintrc-auto-import.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"effectScope": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSlots": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
}
|
||||
}
|
||||
29
.eslintrc.js
Normal file
29
.eslintrc.js
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* .eslint.js
|
||||
*
|
||||
* ESLint configuration file.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'vue3-recommended',
|
||||
'plugin:prettier/recommend',
|
||||
'@vue/eslint-config-typescript',
|
||||
],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/valid-v-slot': 'off',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
endOfLine: 'auto',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
132
README.md
132
README.md
@ -1,93 +1,81 @@
|
||||
# Interface
|
||||
# Vuetify (Default)
|
||||
|
||||
This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
|
||||
|
||||
## ❗️ Important Links
|
||||
|
||||
## Getting started
|
||||
- 📄 [Docs](https://vuetifyjs.com/)
|
||||
- 🚨 [Issues](https://issues.vuetifyjs.com/)
|
||||
- 🏬 [Store](https://store.vuetifyjs.com/)
|
||||
- 🎮 [Playground](https://play.vuetifyjs.com/)
|
||||
- 💬 [Discord](https://community.vuetifyjs.com)
|
||||
|
||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
||||
## 💿 Install
|
||||
|
||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
||||
Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
|
||||
|
||||
## Add your files
|
||||
| Package Manager | Command |
|
||||
|---------------------------------------------------------------|----------------|
|
||||
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
|
||||
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
|
||||
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
|
||||
| [bun](https://bun.sh/#getting-started) | `bun install` |
|
||||
|
||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
||||
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
||||
After completing the installation, your environment is ready for Vuetify development.
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin https://git.peresvet.it/system-trace/controller/interface.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
## ✨ Features
|
||||
|
||||
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
|
||||
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
|
||||
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
|
||||
- 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/)
|
||||
- ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
|
||||
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
|
||||
- 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc)
|
||||
|
||||
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
|
||||
|
||||
## 💡 Usage
|
||||
|
||||
This section covers how to start the development server and build your project for production.
|
||||
|
||||
### Starting the Development Server
|
||||
|
||||
To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||
|
||||
- [ ] [Set up project integrations](https://git.peresvet.it/system-trace/controller/interface/-/settings/integrations)
|
||||
> Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
|
||||
|
||||
## Collaborate with your team
|
||||
### Building for Production
|
||||
|
||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
||||
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
||||
To build your project for production, use:
|
||||
|
||||
## Test and Deploy
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
Use the built-in continuous integration in GitLab.
|
||||
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||
|
||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
||||
Once the build process is completed, your application will be ready for deployment in a production environment.
|
||||
|
||||
***
|
||||
## 💪 Support Vuetify Development
|
||||
|
||||
# Editing this README
|
||||
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
|
||||
|
||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
||||
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
|
||||
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
|
||||
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
|
||||
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
|
||||
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
|
||||
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
|
||||
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
|
||||
|
||||
## Suggestions for a good README
|
||||
## 📑 License
|
||||
[MIT](http://opensource.org/licenses/MIT)
|
||||
|
||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
||||
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
|
||||
## Description
|
||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
||||
|
||||
## Badges
|
||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
||||
|
||||
## Visuals
|
||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
||||
|
||||
## Installation
|
||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
||||
|
||||
## Usage
|
||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
||||
|
||||
## Support
|
||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
|
||||
## Contributing
|
||||
State if you are open to contributions and what your requirements are for accepting them.
|
||||
|
||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
||||
|
||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
Copyright (c) 2016-present Vuetify, LLC
|
||||
|
||||
16
index.html
Normal file
16
index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Peresvet - System Trace</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
6672
package-lock.json
generated
Normal file
6672
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
package.json
Normal file
53
package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "interface",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "6.2.95",
|
||||
"@vueuse/core": "^10.10.0",
|
||||
"axios": "^1.7.2",
|
||||
"chart.js": "^4.4.3",
|
||||
"core-js": "^3.34.0",
|
||||
"moment": "^2.30.1",
|
||||
"roboto-fontface": "*",
|
||||
"vue": "^3.4.21",
|
||||
"vue-chartjs": "^5.3.1",
|
||||
"vuetify": "^3.5.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.24.0",
|
||||
"@types/node": "^20.11.25",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-vue": "^9.22.0",
|
||||
"pinia": "^2.1.7",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "3.3.2",
|
||||
"sass": "^1.71.1",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.2",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"unplugin-vue-router": "^0.8.4",
|
||||
"vite": "^5.1.5",
|
||||
"vite-plugin-pages": "^0.32.2",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-plugin-vuetify": "^2.0.3",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue-tsc": "^2.0.6"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
25
public/images/flag_en.svg
Normal file
25
public/images/flag_en.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_270_67366)">
|
||||
<rect width="32" height="24" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="#2E42A5"/>
|
||||
<mask id="mask0_270_67366" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="24">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_270_67366)">
|
||||
<path d="M-3.56311 22.2854L3.47858 25.2635L32.1598 3.23787L35.8741 -1.18761L28.3441 -2.18297L16.6457 7.3085L7.22968 13.7035L-3.56311 22.2854Z" fill="white"/>
|
||||
<path d="M-2.59912 24.3719L0.988295 26.1001L34.5403 -1.59881H29.5032L-2.59912 24.3719Z" fill="#F50100"/>
|
||||
<path d="M35.5631 22.2854L28.5214 25.2635L-0.159817 3.23787L-3.87415 -1.18761L3.65593 -2.18297L15.3543 7.3085L24.7703 13.7035L35.5631 22.2854Z" fill="white"/>
|
||||
<path d="M35.3229 23.7829L31.7355 25.5111L17.4487 13.6518L13.2129 12.3267L-4.23151 -1.17246H0.805637L18.2403 12.0063L22.8713 13.5952L35.3229 23.7829Z" fill="#F50100"/>
|
||||
<mask id="path-7-inside-1_270_67366" fill="white">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7778 -2H12.2222V8H-1.97247V16H12.2222V26H19.7778V16H34.0275V8H19.7778V-2Z"/>
|
||||
</mask>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7778 -2H12.2222V8H-1.97247V16H12.2222V26H19.7778V16H34.0275V8H19.7778V-2Z" fill="#F50100"/>
|
||||
<path d="M12.2222 -2V-4H10.2222V-2H12.2222ZM19.7778 -2H21.7778V-4H19.7778V-2ZM12.2222 8V10H14.2222V8H12.2222ZM-1.97247 8V6H-3.97247V8H-1.97247ZM-1.97247 16H-3.97247V18H-1.97247V16ZM12.2222 16H14.2222V14H12.2222V16ZM12.2222 26H10.2222V28H12.2222V26ZM19.7778 26V28H21.7778V26H19.7778ZM19.7778 16V14H17.7778V16H19.7778ZM34.0275 16V18H36.0275V16H34.0275ZM34.0275 8H36.0275V6H34.0275V8ZM19.7778 8H17.7778V10H19.7778V8ZM12.2222 0H19.7778V-4H12.2222V0ZM14.2222 8V-2H10.2222V8H14.2222ZM-1.97247 10H12.2222V6H-1.97247V10ZM0.0275269 16V8H-3.97247V16H0.0275269ZM12.2222 14H-1.97247V18H12.2222V14ZM14.2222 26V16H10.2222V26H14.2222ZM19.7778 24H12.2222V28H19.7778V24ZM17.7778 16V26H21.7778V16H17.7778ZM34.0275 14H19.7778V18H34.0275V14ZM32.0275 8V16H36.0275V8H32.0275ZM19.7778 10H34.0275V6H19.7778V10ZM17.7778 -2V8H21.7778V-2H17.7778Z" fill="white" mask="url(#path-7-inside-1_270_67366)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_270_67366">
|
||||
<rect width="32" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
18
public/images/flag_ru.svg
Normal file
18
public/images/flag_ru.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_270_67492)">
|
||||
<rect width="32" height="24" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="#3D58DB"/>
|
||||
<mask id="mask0_270_67492" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="24">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_270_67492)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V8H32V0H0Z" fill="#F7FCFF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 16V24H32V16H0Z" fill="#C51918"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_270_67492">
|
||||
<rect width="32" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 788 B |
17
src/App.vue
Normal file
17
src/App.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<router-view />
|
||||
|
||||
<!-- Confirmation modal -->
|
||||
<confirm-modal />
|
||||
|
||||
<!-- Info modal -->
|
||||
<info-modal />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
41
src/api/auth.ts
Normal file
41
src/api/auth.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { $axios } from '@/plugins/axios';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
const endpoints = {
|
||||
login: 'v1/auth/login',
|
||||
logout: 'v1/auth/logout',
|
||||
me: 'v1/auth/me',
|
||||
};
|
||||
|
||||
export default {
|
||||
login: async (
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.post(endpoints.login, {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
getMe: async (): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.get(endpoints.me);
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
logout: async (): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.post(endpoints.logout);
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
};
|
||||
84
src/api/groups.ts
Normal file
84
src/api/groups.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { $axios } from '@/plugins/axios';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { OrderBy } from '@/types/order-by';
|
||||
import { camelToSnake } from '@/mixins/case-converter';
|
||||
import { setURIParams } from '@/mixins/query-params';
|
||||
|
||||
export default {
|
||||
getAll: async (
|
||||
count: number = 10,
|
||||
page: number = 1,
|
||||
orderBy: OrderBy,
|
||||
search: string,
|
||||
): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const sp = new URLSearchParams([
|
||||
...Object.entries(orderBy),
|
||||
['count', String(count)],
|
||||
['page', String(page)],
|
||||
['input', search],
|
||||
]);
|
||||
setURIParams(sp);
|
||||
|
||||
const params: string = camelToSnake(sp.toString());
|
||||
const data = await $axios.get(`v1/groups?${params}`);
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
create: async (
|
||||
name: string,
|
||||
permissions: number[],
|
||||
): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.post('v1/groups', {
|
||||
name,
|
||||
permissions: permissions.map((i) => ({
|
||||
value: i,
|
||||
})),
|
||||
});
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
update: async (
|
||||
ID: number,
|
||||
name: string,
|
||||
permissions: number[],
|
||||
): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.patch('v1/groups', {
|
||||
ID,
|
||||
name,
|
||||
permissions: permissions.map((i) => ({
|
||||
value: i,
|
||||
})),
|
||||
});
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
delete: async (ID: number): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.delete(`v1/groups/${ID}`);
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
deleteMany: async (IDs: number[]): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.delete('v1/groups', {
|
||||
data: {
|
||||
array: IDs,
|
||||
},
|
||||
});
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
};
|
||||
13
src/api/index.ts
Normal file
13
src/api/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import auth from './auth';
|
||||
import users from './users';
|
||||
import groups from './groups';
|
||||
import permissions from './permissions';
|
||||
import plugins from './plugins';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
users,
|
||||
groups,
|
||||
permissions,
|
||||
plugins,
|
||||
};
|
||||
13
src/api/permissions.ts
Normal file
13
src/api/permissions.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { $axios } from '@/plugins/axios';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
export default {
|
||||
getAll: async (): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.get('v1/permissions');
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
};
|
||||
13
src/api/plugins.ts
Normal file
13
src/api/plugins.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { $axios } from '@/plugins/axios';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
export default {
|
||||
getAll: async (): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.get('v1/plugins');
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
};
|
||||
117
src/api/users.ts
Normal file
117
src/api/users.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import { $axios } from '@/plugins/axios';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { OrderBy } from '@/types/order-by';
|
||||
import { camelToSnake } from '@/mixins/case-converter';
|
||||
import { setURIParams } from '@/mixins/query-params';
|
||||
import { NewCredentials } from '@/types/new-credentials';
|
||||
|
||||
export default {
|
||||
getAll: async (
|
||||
count: number = 10,
|
||||
page: number = 1,
|
||||
orderBy: OrderBy,
|
||||
search: string,
|
||||
groupId: string,
|
||||
): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const sp = new URLSearchParams([
|
||||
...Object.entries(orderBy),
|
||||
['count', String(count)],
|
||||
['page', String(page)],
|
||||
['input', search],
|
||||
['group_id', groupId],
|
||||
]);
|
||||
setURIParams(sp);
|
||||
|
||||
const params: string = camelToSnake(sp.toString());
|
||||
const data = await $axios.get(`v1/users?${params}`);
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
create: async (
|
||||
email: string,
|
||||
realName: string,
|
||||
groupID: number,
|
||||
): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.post('v1/users', {
|
||||
email,
|
||||
realName,
|
||||
groupID,
|
||||
});
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
update: async (
|
||||
ID: number,
|
||||
email: string,
|
||||
realName: string,
|
||||
groupID: number,
|
||||
): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.patch('v1/users', {
|
||||
ID,
|
||||
email,
|
||||
realName,
|
||||
groupID,
|
||||
});
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
delete: async (ID: number): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.delete(`v1/users/${ID}`);
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
deleteMany: async (IDs: number[]): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.delete('v1/users', {
|
||||
data: {
|
||||
array: IDs,
|
||||
},
|
||||
});
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
blockMany: async (IDs: number[]): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.patch('v1/users/block', {
|
||||
array: IDs,
|
||||
});
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
unblockMany: async (IDs: number[]): Promise<[AxiosResponse?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.patch('v1/users/unblock', {
|
||||
array: IDs,
|
||||
});
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
resetPassword: async (
|
||||
ID: number,
|
||||
): Promise<[AxiosResponse<NewCredentials>?, string?]> => {
|
||||
try {
|
||||
const data = await $axios.patch(`v1/users/password/${ID}`);
|
||||
return [data, undefined];
|
||||
} catch (e: any) {
|
||||
return [undefined, e?.response?.data?.error ?? e.message];
|
||||
}
|
||||
},
|
||||
};
|
||||
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Light.ttf
Normal file
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Light.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Regular.ttf
Normal file
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Black.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Black.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Bold.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Light.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Medium.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Regular.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Thin.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Thin.ttf
Normal file
Binary file not shown.
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
8
src/assets/logo.svg
Normal file
8
src/assets/logo.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="81" height="24" viewBox="0 0 81 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="48" y="16.0547" width="32.1081" height="7.94595" fill="black"/>
|
||||
<rect width="32.1081" height="7.94595" fill="black"/>
|
||||
<rect x="40.0547" y="7.94531" width="7.94595" height="8.10811" fill="black"/>
|
||||
<rect x="32.1094" y="7.94531" width="7.94595" height="16.0541" fill="black"/>
|
||||
<rect y="7.94531" width="7.94595" height="16.0541" fill="black"/>
|
||||
<rect x="40.0547" width="40.0541" height="7.94595" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 518 B |
3
src/assets/styles/README.md
Normal file
3
src/assets/styles/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Styles
|
||||
|
||||
This directory is for configuring the styles of the application.
|
||||
37
src/assets/styles/animations.css
Normal file
37
src/assets/styles/animations.css
Normal file
@ -0,0 +1,37 @@
|
||||
/* Fade */
|
||||
.fade-leave-active {
|
||||
animation: fadeOut 0.15s var(--transition);
|
||||
}
|
||||
|
||||
.fade-enter-active {
|
||||
position: absolute;
|
||||
animation: fadeIn 0.15s var(--transition);
|
||||
}
|
||||
|
||||
.fade-delay-leave-active {
|
||||
animation: fadeOut 0.15s 0.5s var(--transition);
|
||||
}
|
||||
|
||||
.fade-delay-enter-active {
|
||||
animation: fadeIn 0.15s 0.5s var(--transition);
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
95
src/assets/styles/core.scss
Normal file
95
src/assets/styles/core.scss
Normal file
@ -0,0 +1,95 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import './fonts.css';
|
||||
@import './table.css';
|
||||
@import './animations.css';
|
||||
|
||||
:root {
|
||||
--transition: cubic-bezier(0.42, 0, 0.58, 1);
|
||||
}
|
||||
|
||||
.accent-modal {
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
background: rgb(var(--v-theme-code)) !important;
|
||||
}
|
||||
|
||||
.border-b {
|
||||
border-bottom-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.border-r {
|
||||
border-right-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.border-l {
|
||||
border-left-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.border-a {
|
||||
border-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
.text-h6 {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.cm-mini {
|
||||
min-height: 30px !important;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 0.875rem;
|
||||
/* 14px */
|
||||
line-height: 1.25rem;
|
||||
/* 20px */
|
||||
overscroll-behavior: none;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.v-theme--light {
|
||||
--v-theme-primary: 11, 87, 208 !important;
|
||||
--v-theme-warning: 176, 126, 0 !important;
|
||||
--v-theme-error: 217, 48, 37 !important;
|
||||
--selected-row-bg: rgb(241, 245, 253);
|
||||
}
|
||||
|
||||
.v-theme--dark {
|
||||
--v-theme-primary: 51, 129, 255 !important;
|
||||
--v-theme-warning: 252, 192, 10 !important;
|
||||
--v-theme-error: 234, 67, 53 !important;
|
||||
--selected-row-bg: rgba(241, 245, 253, 0.075);
|
||||
|
||||
.v-overlay__scrim {
|
||||
--v-theme-on-surface: 0, 0, 0;
|
||||
--v-overlay-opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.v-btn__overlay {
|
||||
background-color: rgb(var(--v-theme-on-surface)) !important;
|
||||
}
|
||||
.v-btn:hover > .v-btn__overlay {
|
||||
opacity: 0.06 !important;
|
||||
}
|
||||
|
||||
.disabled-button {
|
||||
filter: grayscale(1);
|
||||
opacity: 0.5 !important;
|
||||
.v-btn__overlay {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
61
src/assets/styles/fonts.css
Normal file
61
src/assets/styles/fonts.css
Normal file
@ -0,0 +1,61 @@
|
||||
html {
|
||||
font-family: 'Roboto';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto/Roboto-Black.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'MDI';
|
||||
src: url('../fonts/MDI/MaterialSymbolsOutlined-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'MDI';
|
||||
src: url('../fonts/MDI/MaterialSymbolsOutlined-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.mso {
|
||||
font-family: 'MDI';
|
||||
font-size: 24px;
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 300,
|
||||
'GRAD' 0,
|
||||
'opsz' 24
|
||||
}
|
||||
85
src/assets/styles/settings.scss
Normal file
85
src/assets/styles/settings.scss
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* src/styles/settings.scss
|
||||
*
|
||||
* Configures SASS variables and Vuetify overwrites
|
||||
*/
|
||||
|
||||
// https://vuetifyjs.com/features/sass-variables/`
|
||||
@use 'vuetify/settings' with (
|
||||
// $color-pack: false
|
||||
$card-item-padding: 0.625rem 0,
|
||||
$selection-control-size: 25px,
|
||||
$utilities: (
|
||||
'align-content': false,
|
||||
'align-items': false,
|
||||
'align-self': false,
|
||||
'border-bottom': false,
|
||||
'border-end': false,
|
||||
'border-opacity': false,
|
||||
'border-start': false,
|
||||
'border-style': false,
|
||||
'border-top': false,
|
||||
'border': false,
|
||||
'display': false,
|
||||
'flex-direction': false,
|
||||
'flex-grow': false,
|
||||
'flex-shrink': false,
|
||||
'flex-wrap': false,
|
||||
'flex': false,
|
||||
'float-ltr': false,
|
||||
'float-rtl': false,
|
||||
'float': false,
|
||||
'font-italic': false,
|
||||
'font-weight': false,
|
||||
'justify-content': false,
|
||||
'margin-bottom': false,
|
||||
'margin-end': false,
|
||||
'margin-left': false,
|
||||
'margin-right': false,
|
||||
'margin-start': false,
|
||||
'margin-top': false,
|
||||
'margin-x': false,
|
||||
'margin-y': false,
|
||||
'margin': false,
|
||||
'negative-margin-bottom': false,
|
||||
'negative-margin-end': false,
|
||||
'negative-margin-left': false,
|
||||
'negative-margin-right': false,
|
||||
'negative-margin-start': false,
|
||||
'negative-margin-top': false,
|
||||
'negative-margin-x': false,
|
||||
'negative-margin-y': false,
|
||||
'negative-margin': false,
|
||||
'order': false,
|
||||
'overflow-wrap': false,
|
||||
'overflow-x': false,
|
||||
'overflow-y': false,
|
||||
'overflow': false,
|
||||
'padding-bottom': false,
|
||||
'padding-end': false,
|
||||
'padding-left': false,
|
||||
'padding-right': false,
|
||||
'padding-start': false,
|
||||
'padding-top': false,
|
||||
'padding-x': false,
|
||||
'padding-y': false,
|
||||
'padding': false,
|
||||
// "rounded-bottom-end": false,
|
||||
// "rounded-bottom-start": false,
|
||||
// "rounded-bottom": false,
|
||||
// "rounded-end": false,
|
||||
// "rounded-start": false,
|
||||
// "rounded-top-end": false,
|
||||
// "rounded-top-start": false,
|
||||
// "rounded-top": false,
|
||||
// "rounded": false,
|
||||
'text-align': false,
|
||||
'text-decoration': false,
|
||||
'text-mono': false,
|
||||
'text-opacity': false,
|
||||
'text-overflow': false,
|
||||
'text-transform': false,
|
||||
// "typography": false,
|
||||
'white-space': false
|
||||
)
|
||||
);
|
||||
71
src/assets/styles/table.css
Normal file
71
src/assets/styles/table.css
Normal file
@ -0,0 +1,71 @@
|
||||
.v-table .v-table__wrapper > table > tbody > tr:not(:last-child) > td,
|
||||
.v-table .v-table__wrapper > table > tbody > tr:not(:last-child) > th {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.v-table .v-table__wrapper > table > tbody > tr > td,
|
||||
.v-table .v-table__wrapper > table > tbody > tr > th {
|
||||
box-shadow: inset 0 -1px 0 rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.v-data-table--loading .v-data-table__td {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
table > tbody > tr:has(div.v-selection-control--dirty) {
|
||||
background-color: var(--selected-row-bg);
|
||||
}
|
||||
|
||||
table > thead > tr > .v-data-table__th--sticky {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
table
|
||||
> tbody
|
||||
> tr:not(.v-data-table-rows-no-data):not(.v-data-table-rows-loading)
|
||||
> td:last-child,
|
||||
table
|
||||
> thead
|
||||
> tr:not(.v-data-table-rows-no-data):not(.v-data-table-rows-loading)
|
||||
> th:last-child {
|
||||
position: sticky !important;
|
||||
position: -webkit-sticky !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
right: 0;
|
||||
border-right: none;
|
||||
border-bottom: none !important;
|
||||
z-index: 8 !important;
|
||||
background-color: rgb(var(--v-theme-surface)) !important;
|
||||
border-left-width: 1px !important;
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
|
||||
table
|
||||
> thead
|
||||
> tr:not(.v-data-table-rows-no-data):not(.v-data-table-rows-loading)
|
||||
> th:last-child {
|
||||
z-index: 9 !important;
|
||||
}
|
||||
table > thead {
|
||||
z-index: 9 !important;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
.table-actions::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--selected-row-bg);
|
||||
}
|
||||
|
||||
.v-theme--light
|
||||
table
|
||||
.v-selection-control__input:has(i.mdi-checkbox-marked)
|
||||
i::before {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
191
src/auto-imports.d.ts
vendored
Normal file
191
src/auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,191 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useRoute: typeof import('vue-router/auto')['useRoute']
|
||||
const useRouter: typeof import('vue-router/auto')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
}
|
||||
}
|
||||
declare module '@vue/runtime-core' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
}
|
||||
}
|
||||
24
src/components.d.ts
vendored
Normal file
24
src/components.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AppFooter: typeof import('./components/AppFooter.vue')['default']
|
||||
ConfirmModal: typeof import('./components/ConfirmModal.vue')['default']
|
||||
copy: typeof import('./components/ConfirmModal copy.vue')['default']
|
||||
ErrorPlate: typeof import('./components/ErrorPlate.vue')['default']
|
||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||
InfoModal: typeof import('./components/InfoModal.vue')['default']
|
||||
LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default']
|
||||
LogoComponent: typeof import('./components/Icons/LogoComponent.vue')['default']
|
||||
NavigationDrawer: typeof import('./components/NavigationDrawer.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
TableLoader: typeof import('./components/TableLoader.vue')['default']
|
||||
ThemeSwitcher: typeof import('./components/ThemeSwitcher.vue')['default']
|
||||
}
|
||||
}
|
||||
79
src/components/AppFooter.vue
Normal file
79
src/components/AppFooter.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<v-footer height="40" app>
|
||||
<a
|
||||
v-for="item in items"
|
||||
:key="item.title"
|
||||
:href="item.href"
|
||||
:title="item.title"
|
||||
class="d-inline-block mx-2 social-link"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon
|
||||
:icon="item.icon"
|
||||
:size="item.icon === '$vuetify' ? 24 : 16"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="text-caption text-disabled"
|
||||
style="position: absolute; right: 16px;"
|
||||
>
|
||||
© 2016-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Vuetify, LLC</span>
|
||||
—
|
||||
<a
|
||||
class="text-decoration-none on-surface"
|
||||
href="https://vuetifyjs.com/about/licensing/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
MIT License
|
||||
</a>
|
||||
</div>
|
||||
</v-footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const items = [
|
||||
{
|
||||
title: 'Vuetify Documentation',
|
||||
icon: `$vuetify`,
|
||||
href: 'https://vuetifyjs.com/',
|
||||
},
|
||||
{
|
||||
title: 'Vuetify Support',
|
||||
icon: 'mdi-shield-star-outline',
|
||||
href: 'https://support.vuetifyjs.com/',
|
||||
},
|
||||
{
|
||||
title: 'Vuetify X',
|
||||
icon: `svg:M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z`,
|
||||
href: 'https://x.com/vuetifyjs',
|
||||
},
|
||||
{
|
||||
title: 'Vuetify GitHub',
|
||||
icon: `mdi-github`,
|
||||
href: 'https://github.com/vuetifyjs/vuetify',
|
||||
},
|
||||
{
|
||||
title: 'Vuetify Discord',
|
||||
icon: `mdi-discord`,
|
||||
href: 'https://community.vuetifyjs.com/',
|
||||
},
|
||||
{
|
||||
title: 'Vuetify Reddit',
|
||||
icon: `mdi-reddit`,
|
||||
href: 'https://reddit.com/r/vuetifyjs',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
.social-link :deep(.v-icon)
|
||||
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
|
||||
text-decoration: none
|
||||
transition: .2s ease-in-out
|
||||
|
||||
&:hover
|
||||
color: rgba(25, 118, 210, 1)
|
||||
</style>
|
||||
42
src/components/ConfirmModal.vue
Normal file
42
src/components/ConfirmModal.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { useConfirmationStore } from '@/stores/confirmation';
|
||||
import { useLocale } from 'vuetify';
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const $confirmation = useConfirmationStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog persistent :model-value="$confirmation.show" width="450">
|
||||
<v-card>
|
||||
<template v-slot:title>
|
||||
<v-card-title class="p-0 w-full flex items-center">
|
||||
<span class="mso mr-1 text-2xl">warning</span>
|
||||
<span>{{ $confirmation.title }}</span>
|
||||
</v-card-title>
|
||||
</template>
|
||||
|
||||
<v-card-text>{{ $confirmation.message }}</v-card-text>
|
||||
|
||||
<!-- Actions -->
|
||||
<v-card-actions class="flex justify-end p-5 gap-1">
|
||||
<!-- Cancel -->
|
||||
<v-btn
|
||||
variant="text"
|
||||
class="px-4"
|
||||
:text="t('$vuetify.actions.cancel')"
|
||||
@click="$confirmation.doDecline()"
|
||||
></v-btn>
|
||||
<!-- Confirm -->
|
||||
<v-btn
|
||||
variant="flat"
|
||||
color="primary"
|
||||
class="px-4"
|
||||
:text="t('$vuetify.actions.confirm')"
|
||||
@click="$confirmation.doConfirm()"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
74
src/components/ErrorPlate.vue
Normal file
74
src/components/ErrorPlate.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocale } from 'vuetify';
|
||||
|
||||
defineProps({
|
||||
error: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noBorder: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
function parseError(e: string): string {
|
||||
if (e.includes('$vuetify', 0)) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if (e.includes('validator')) {
|
||||
const errs = e.split(' || ');
|
||||
for (const err of errs) {
|
||||
if (err.includes('validator', 0)) {
|
||||
const splitted = err.split(':');
|
||||
if (splitted?.length) {
|
||||
const right = splitted.pop();
|
||||
if (right && right.includes('/')) {
|
||||
const [field, key] = right.split('/');
|
||||
return t(
|
||||
`$vuetify.validator.field`,
|
||||
field.toLowerCase(),
|
||||
t(`$vuetify.validator.${key.replace(/'/gm, '')}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const te = t(`$vuetify.${e}`);
|
||||
if (!te.includes('$vuetify', 0)) {
|
||||
return te;
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
v-if="error?.length"
|
||||
class="text-sm text-error flex items-center"
|
||||
:class="{
|
||||
'error-block': block,
|
||||
'my-2': !block,
|
||||
'border-b': !noBorder,
|
||||
}"
|
||||
><span class="mso mr-2 text-2xl">error</span> {{ parseError(error) }}</span
|
||||
>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error-block {
|
||||
background-color: rgba(var(--v-theme-error), var(--v-border-opacity));
|
||||
border-color: rgba(var(--v-theme-error), var(--v-border-opacity));
|
||||
padding: 0 0.8rem;
|
||||
}
|
||||
</style>
|
||||
157
src/components/HelloWorld.vue
Normal file
157
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<v-container class="fill-height">
|
||||
<v-responsive
|
||||
class="align-centerfill-height mx-auto"
|
||||
max-width="900"
|
||||
>
|
||||
<v-img
|
||||
class="mb-4"
|
||||
height="150"
|
||||
src="@/assets/logo.png"
|
||||
/>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||
|
||||
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
|
||||
prepend-icon="mdi-rocket-launch-outline"
|
||||
rounded="lg"
|
||||
variant="outlined"
|
||||
>
|
||||
<template #image>
|
||||
<v-img position="top right" />
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<h2 class="text-h5 font-weight-bold">Get started</h2>
|
||||
</template>
|
||||
|
||||
<template #subtitle>
|
||||
<div class="text-subtitle-1">
|
||||
Replace this page by removing <v-kbd>{{ `<HelloWorld />` }}</v-kbd> in <v-kbd>pages/index.vue</v-kbd>.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-overlay
|
||||
opacity=".12"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/"
|
||||
prepend-icon="mdi-text-box-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Learn about all things Vuetify in our documentation."
|
||||
target="_blank"
|
||||
title="Documentation"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
|
||||
prepend-icon="mdi-star-circle-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Explore available framework Features."
|
||||
target="_blank"
|
||||
title="Features"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/components/all"
|
||||
prepend-icon="mdi-widgets-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Discover components in the API Explorer."
|
||||
target="_blank"
|
||||
title="Components"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://discord.vuetifyjs.com"
|
||||
prepend-icon="mdi-account-group-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Connect with Vuetify developers."
|
||||
target="_blank"
|
||||
title="Community"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
//
|
||||
</script>
|
||||
23
src/components/Icons/LogoComponent.vue
Normal file
23
src/components/Icons/LogoComponent.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, type ComputedRef } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'dark'
|
||||
}
|
||||
})
|
||||
|
||||
const fill: ComputedRef<string> = computed(() => props.theme === 'dark' ? '#FFF' : '#000')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg width="81" height="24" viewBox="0 0 81 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="48" y="16.0547" width="32.1081" height="7.94595" :fill="fill"/>
|
||||
<rect width="32.1081" height="7.94595" :fill="fill"/>
|
||||
<rect x="40.0547" y="7.94531" width="7.94595" height="8.10811" :fill="fill"/>
|
||||
<rect x="32.1094" y="7.94531" width="7.94595" height="16.0541" :fill="fill"/>
|
||||
<rect y="7.94531" width="7.94595" height="16.0541" :fill="fill"/>
|
||||
<rect x="40.0547" width="40.0541" height="7.94595" :fill="fill"/>
|
||||
</svg>
|
||||
</template>
|
||||
34
src/components/InfoModal.vue
Normal file
34
src/components/InfoModal.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocale } from 'vuetify';
|
||||
import { useInfoStore } from '@/stores/info';
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const $info = useInfoStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog persistent :model-value="$info.show" width="450">
|
||||
<v-card>
|
||||
<template v-slot:title>
|
||||
<v-card-title class="p-0 w-full flex items-center">
|
||||
<span class="mso mr-1 text-2xl">warning</span>
|
||||
<span>{{ $info.title }}</span>
|
||||
</v-card-title>
|
||||
</template>
|
||||
|
||||
<v-card-text class="whitespace-pre-line">{{ $info.message }}</v-card-text>
|
||||
|
||||
<!-- Actions -->
|
||||
<v-card-actions class="flex justify-end p-5 gap-1">
|
||||
<!-- Cancel -->
|
||||
<v-btn
|
||||
variant="text"
|
||||
class="px-4"
|
||||
:text="t('$vuetify.actions.hide')"
|
||||
@click="$info.close()"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
68
src/components/LanguageSwitcher.vue
Normal file
68
src/components/LanguageSwitcher.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<script lang="ts" setup>
|
||||
import { useLocale } from 'vuetify';
|
||||
import { $cookie } from '@/plugins/cookie';
|
||||
import { onBeforeMount } from 'vue';
|
||||
import { COOKIE_LOCALE } from '../constants/static';
|
||||
import Moment from '@/plugins/moment';
|
||||
|
||||
const { current } = useLocale();
|
||||
const languages = [
|
||||
{
|
||||
title: 'Русский',
|
||||
locale: 'ru',
|
||||
},
|
||||
{
|
||||
title: 'English',
|
||||
locale: 'en',
|
||||
},
|
||||
];
|
||||
const key = COOKIE_LOCALE;
|
||||
|
||||
const get = (): string => {
|
||||
const v = $cookie.get(key);
|
||||
Moment.$moment.locale(v);
|
||||
return v;
|
||||
};
|
||||
const set = (locale: string): void => {
|
||||
current.value = locale;
|
||||
Moment.$moment.locale(locale);
|
||||
$cookie.set(key, locale);
|
||||
};
|
||||
const setLocaleFromExistCookie = (): void => {
|
||||
const v = get();
|
||||
if (v?.length && typeof v === 'string') {
|
||||
set(v);
|
||||
}
|
||||
};
|
||||
onBeforeMount(setLocaleFromExistCookie);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-menu transition="none" close-delay="0" open-delay="0">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon density="comfortable">
|
||||
<span class="mso" v-bind="props">language</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact" class="mt-3 accent-modal" elevation="0">
|
||||
<v-list-item
|
||||
v-for="(item, i) in languages"
|
||||
:key="i"
|
||||
class="cm-mini"
|
||||
@click="set(item.locale)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<img class="flag mr-2" :src="`/images/flag_${item.locale}.svg`" />
|
||||
</template>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.flag {
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
74
src/components/NavigationDrawer.vue
Normal file
74
src/components/NavigationDrawer.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useLocale } from 'vuetify';
|
||||
import { ROUTES } from '@/constants/routes';
|
||||
|
||||
const props = defineProps({
|
||||
drawer: Boolean,
|
||||
});
|
||||
const emits = defineEmits(['change']);
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const modelValue = ref<boolean>(props.drawer);
|
||||
|
||||
watch(props, (v: any, _) => {
|
||||
modelValue.value = v.drawer;
|
||||
});
|
||||
|
||||
function change(e: boolean) {
|
||||
emits('change', e);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="modelValue"
|
||||
class="pt-3 elevation-0 border-r"
|
||||
:scrim="false"
|
||||
close-on-click
|
||||
@update:modelValue="change"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, i) in ROUTES"
|
||||
:key="i"
|
||||
:value="item"
|
||||
color="primary"
|
||||
:to="item.value"
|
||||
@click="change(false)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<span class="mso mr-3">{{ item.icon }}</span>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>{{ t(item.title) }}</v-list-item-title>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-menu
|
||||
v-if="item.children?.length"
|
||||
open-on-hover
|
||||
close-on-click
|
||||
:open-delay="0"
|
||||
:close-delay="0"
|
||||
transition="none"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<span class="mso" v-bind="props">chevron_right</span>
|
||||
</template>
|
||||
|
||||
<v-list class="accent-modal" elevation="0" density="compact">
|
||||
<v-list-item
|
||||
v-for="(c, cIndex) in item.children"
|
||||
:key="cIndex"
|
||||
:title="t(c.title)"
|
||||
:to="c.value"
|
||||
:active="false"
|
||||
class="cm-mini"
|
||||
@click="change(false)"
|
||||
></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
35
src/components/README.md
Normal file
35
src/components/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Components
|
||||
|
||||
Vue template files in this folder are automatically imported.
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
|
||||
|
||||
The following example assumes a component located at `src/components/MyComponent.vue`:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<MyComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
```
|
||||
|
||||
When your template is rendered, the component's import will automatically be inlined, which renders to this:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<MyComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MyComponent from '@/components/MyComponent.vue'
|
||||
</script>
|
||||
```
|
||||
14
src/components/TableLoader.vue
Normal file
14
src/components/TableLoader.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'TableLoader',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute left-0 top-0 h-full w-full z-[100]"
|
||||
style="
|
||||
background-color: rgba(var(--v-theme-surface), var(--v-disabled-opacity));
|
||||
"
|
||||
></div>
|
||||
</template>
|
||||
35
src/components/ThemeSwitcher.vue
Normal file
35
src/components/ThemeSwitcher.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from 'vuetify'
|
||||
import { $cookie } from '@/plugins/cookie'
|
||||
import { onBeforeMount } from 'vue';
|
||||
import { COOKIE_THEME } from '../constants/static';
|
||||
|
||||
const theme = useTheme()
|
||||
const key = COOKIE_THEME
|
||||
|
||||
const get = (): string => {
|
||||
return $cookie.get(key);
|
||||
}
|
||||
const set = (value: string): void => {
|
||||
theme.global.name.value = value;
|
||||
$cookie.set(key, value);
|
||||
}
|
||||
const toggle = (): void => {
|
||||
const t = theme.global.current?.value?.dark ? 'light' : 'dark'
|
||||
theme.global.name.value = t
|
||||
$cookie.set(key, t);
|
||||
}
|
||||
const setThemeFromExistCookie = (): void => {
|
||||
const v = get()
|
||||
if (v?.length && typeof v === 'string') {
|
||||
set(v)
|
||||
}
|
||||
}
|
||||
onBeforeMount(setThemeFromExistCookie)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn icon density="comfortable" @click="toggle">
|
||||
<span class="mso">routine</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
11
src/constants/defaults.ts
Normal file
11
src/constants/defaults.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const defaultCursor = JSON.stringify({
|
||||
count: 10,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalRows: 10,
|
||||
});
|
||||
|
||||
export const defaultSort = JSON.stringify({
|
||||
key: 'id',
|
||||
order: 'asc',
|
||||
});
|
||||
18
src/constants/permissions.ts
Normal file
18
src/constants/permissions.ts
Normal file
@ -0,0 +1,18 @@
|
||||
// This file must be synced with backend
|
||||
export enum Permissions {
|
||||
DASHBOARD_READ = 1,
|
||||
GROUPS_CREATE = 14,
|
||||
GROUPS_DELETE = 16,
|
||||
GROUPS_READ = 13,
|
||||
GROUPS_UPDATE = 15,
|
||||
LOGS_READ = 4,
|
||||
SETTINGS_READ = 2,
|
||||
SETTINGS_UPDATE = 3,
|
||||
TASKS_READ = 5,
|
||||
TASKS_UPDATE = 6,
|
||||
USERS_BLOCK = 12,
|
||||
USERS_CREATE = 9,
|
||||
USERS_DELETE = 11,
|
||||
USERS_READ = 7,
|
||||
USERS_UPDATE = 10,
|
||||
}
|
||||
44
src/constants/routes.ts
Normal file
44
src/constants/routes.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { UsersPageTabs } from '../enums/user-tab.enum';
|
||||
|
||||
export const ROUTES = [
|
||||
{
|
||||
title: '$vuetify.navigation.dashboards',
|
||||
value: '/',
|
||||
icon: 'dashboard',
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.tasks',
|
||||
value: '/tasks',
|
||||
icon: 'web_traffic',
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.servers',
|
||||
value: '/servers',
|
||||
icon: 'dns',
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.users',
|
||||
value: `/users?tab=${UsersPageTabs.USERS}`,
|
||||
icon: 'group',
|
||||
children: [
|
||||
{
|
||||
title: '$vuetify.navigation.users',
|
||||
value: `/users?tab=${UsersPageTabs.USERS}`,
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.groups',
|
||||
value: `/users?tab=${UsersPageTabs.GROUPS}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.patterns',
|
||||
value: '/patterns',
|
||||
icon: 'layers',
|
||||
},
|
||||
{
|
||||
title: '$vuetify.navigation.logs',
|
||||
value: '/logs',
|
||||
icon: 'menu',
|
||||
},
|
||||
];
|
||||
4
src/constants/static.ts
Normal file
4
src/constants/static.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const ITEMS_PER_PAGE = [10, 25, 50, 100]
|
||||
|
||||
export const COOKIE_LOCALE = 'user_locale'
|
||||
export const COOKIE_THEME = 'user_theme'
|
||||
5
src/enums/time-units.enum.ts
Normal file
5
src/enums/time-units.enum.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum timeUnits {
|
||||
Seconds,
|
||||
Minutes,
|
||||
Hours,
|
||||
}
|
||||
7
src/enums/user-actions.enum.ts
Normal file
7
src/enums/user-actions.enum.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum UserActions {
|
||||
EDIT,
|
||||
BLOCK,
|
||||
UNBLOCK,
|
||||
RESET_PASSWORD,
|
||||
DELETE,
|
||||
}
|
||||
4
src/enums/user-tab.enum.ts
Normal file
4
src/enums/user-tab.enum.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum UsersPageTabs {
|
||||
USERS,
|
||||
GROUPS,
|
||||
}
|
||||
5
src/layouts/README.md
Normal file
5
src/layouts/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Layouts
|
||||
|
||||
Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages.
|
||||
|
||||
Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) repository.
|
||||
13
src/layouts/centered.vue
Normal file
13
src/layouts/centered.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<div class="flex flex-col items-center justify-center h-full w-full">
|
||||
<router-view />
|
||||
</div>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
42
src/layouts/default.vue
Normal file
42
src/layouts/default.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useTheme } from 'vuetify';
|
||||
import NavigationDrawer from '@/components/NavigationDrawer.vue';
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher.vue';
|
||||
import ThemeSwitcher from '@/components/ThemeSwitcher.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const $auth = useAuthStore();
|
||||
|
||||
const drawer = ref(false);
|
||||
const theme = useTheme();
|
||||
const currentTheme = computed(() => theme.global.name.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app
|
||||
:style="`background-color: ${currentTheme === 'light' ? '#f0f4f9' : '#1c1c1c'}`"
|
||||
>
|
||||
<v-app-bar prominent class="elevation-0 border-b px-2">
|
||||
<v-app-bar-nav-icon
|
||||
density="comfortable"
|
||||
:disabled="!$auth.loggedIn"
|
||||
@click="drawer = !drawer"
|
||||
></v-app-bar-nav-icon>
|
||||
<v-app-bar-title>System Trace</v-app-bar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!-- Theme switcher -->
|
||||
<ThemeSwitcher class="mr-1" />
|
||||
<!-- Language switcher -->
|
||||
<LanguageSwitcher />
|
||||
</v-app-bar>
|
||||
|
||||
<NavigationDrawer :drawer="drawer" @change="(e) => (drawer = e)" />
|
||||
|
||||
<v-main>
|
||||
<router-view></router-view>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
19
src/locales/en/actions.json
Normal file
19
src/locales/en/actions.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
"create-group": "Create group",
|
||||
"create-user": "Create user",
|
||||
"reset-password": "Reset password",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"block": "Block",
|
||||
"unblock": "Unblock",
|
||||
"apply": "Apply",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"hide": "Hide",
|
||||
"start-task": "Start task",
|
||||
"stop-task": "Stop task",
|
||||
"save": "Save"
|
||||
}
|
||||
}
|
||||
11
src/locales/en/auth.json
Normal file
11
src/locales/en/auth.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"auth": {
|
||||
"copyright": "Product of Peresvet LLC",
|
||||
"title": "Authentication",
|
||||
"email": "E-Mail address",
|
||||
"password": "Password",
|
||||
"login": "Log in",
|
||||
"help": "Any problems? Contact ",
|
||||
"help-link": "administrator"
|
||||
}
|
||||
}
|
||||
12
src/locales/en/errors.json
Normal file
12
src/locales/en/errors.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"User with specified E-Mail and password not found": "User with specified email and password was not found",
|
||||
"validator": {
|
||||
"field": "Field {0} is not compliant: {1}",
|
||||
"required": "required",
|
||||
"min": "min. value",
|
||||
"max": "max. value",
|
||||
"password": "password rules",
|
||||
"email": "E-Mail rules",
|
||||
"endswith": "invalid value"
|
||||
}
|
||||
}
|
||||
36
src/locales/en/groups.json
Normal file
36
src/locales/en/groups.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"groups": {
|
||||
"heads": {
|
||||
"id": "ID",
|
||||
"name": "Title",
|
||||
"issuer": "Issuer",
|
||||
"status": "Status",
|
||||
"created-at": "Created at",
|
||||
"updated-at": "Last update",
|
||||
"permissions": "Permissions"
|
||||
},
|
||||
"statuses": {
|
||||
"active": "Active",
|
||||
"deleted": "Deleted"
|
||||
},
|
||||
"creation": {
|
||||
"create": "New group",
|
||||
"edit": "Edit group",
|
||||
"fields": {
|
||||
"ID": "ID of group",
|
||||
"name": "Name of group",
|
||||
"permissions": "Permissions of users in group"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"delete-one": {
|
||||
"title": "Delete group",
|
||||
"message": "Are you sure you want to delete group with ID {0}? This action cannot be undone"
|
||||
},
|
||||
"delete-many": {
|
||||
"title": "Delete groups",
|
||||
"message": "Are you sure you want to delete {0} groups? This action cannot be undone"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/locales/en/index.ts
Normal file
19
src/locales/en/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import errors from './errors.json';
|
||||
import navigation from './navigation.json';
|
||||
import users from './users.json';
|
||||
import groups from './groups.json';
|
||||
import actions from './actions.json';
|
||||
import auth from './auth.json';
|
||||
import misc from './misc.json';
|
||||
import permissions from './permissions.json';
|
||||
|
||||
export default {
|
||||
...errors,
|
||||
...navigation,
|
||||
...users,
|
||||
...groups,
|
||||
...actions,
|
||||
...auth,
|
||||
...misc,
|
||||
...permissions,
|
||||
};
|
||||
6
src/locales/en/misc.json
Normal file
6
src/locales/en/misc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"misc": {
|
||||
"actions": "Actions",
|
||||
"selected": "Selected: {0}"
|
||||
}
|
||||
}
|
||||
11
src/locales/en/navigation.json
Normal file
11
src/locales/en/navigation.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"navigation": {
|
||||
"dashboards": "Dashboards",
|
||||
"tasks": "Tasks",
|
||||
"servers": "Servers",
|
||||
"users": "Users",
|
||||
"groups": "Groups",
|
||||
"patterns": "Patterns",
|
||||
"logs": "Logs"
|
||||
}
|
||||
}
|
||||
19
src/locales/en/permissions.json
Normal file
19
src/locales/en/permissions.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"permissions": {
|
||||
"1": "Access to dashboards",
|
||||
"2": "Read settings",
|
||||
"3": "Editing settings",
|
||||
"4": "Reading logs",
|
||||
"5": "Reading tasks",
|
||||
"6": "Editing tasks",
|
||||
"7": "Read users",
|
||||
"8": "Creating users",
|
||||
"9": "Editing users",
|
||||
"10": "Deleting users",
|
||||
"11": "Blocking users",
|
||||
"12": "Read groups",
|
||||
"13": "Creating groups",
|
||||
"14": "Editing groups",
|
||||
"15": "Delete groups"
|
||||
}
|
||||
}
|
||||
54
src/locales/en/users.json
Normal file
54
src/locales/en/users.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"search": "Search",
|
||||
"selected-group": "Search by group: ID {0}",
|
||||
"tabs": {
|
||||
"users": "Users",
|
||||
"groups": "Groups"
|
||||
},
|
||||
"heads": {
|
||||
"id": "ID",
|
||||
"email": "E-Mail",
|
||||
"group": "Group",
|
||||
"real-name": "Name",
|
||||
"password": "Password",
|
||||
"status": "Status",
|
||||
"last-visit": "Last visit",
|
||||
"updated-at": "Last update",
|
||||
"created-at": "Created at"
|
||||
},
|
||||
"statuses": {
|
||||
"active": "Active",
|
||||
"blocked": "Blocked",
|
||||
"deleted": "Deleted",
|
||||
"set": "Set",
|
||||
"unset": "Not set"
|
||||
},
|
||||
"creation": {
|
||||
"create": "New user",
|
||||
"edit": "Edit user",
|
||||
"fields": {
|
||||
"ID": "ID of user",
|
||||
"email": "E-Mail address",
|
||||
"name": "Real name",
|
||||
"group": "Group",
|
||||
"no-group": "Not in group"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"delete-one": {
|
||||
"title": "Delete user",
|
||||
"message": "Are you sure you want to delete user with ID {0}? This action cannot be undone"
|
||||
},
|
||||
"delete-many": {
|
||||
"title": "Delete users",
|
||||
"message": "Are you sure you want to delete {0} users? This action cannot be undone"
|
||||
}
|
||||
},
|
||||
"new-credentials": {
|
||||
"title": "Login details",
|
||||
"message": "E-Mail address: {0}\nPassword: {1}\n\nRemember or write down this data, it will be impossible to find it out again"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/locales/ru/actions.json
Normal file
19
src/locales/ru/actions.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"actions": {
|
||||
"refresh": "Обновить",
|
||||
"create-group": "Создать группу",
|
||||
"create-user": "Создать пользователя",
|
||||
"reset-password": "Сбросить пароль",
|
||||
"delete": "Удалить",
|
||||
"edit": "Редактировать",
|
||||
"block": "Заблокировать",
|
||||
"unblock": "Разблокировать",
|
||||
"apply": "Применить",
|
||||
"confirm": "Подтвердить",
|
||||
"cancel": "Отменить",
|
||||
"hide": "Скрыть",
|
||||
"start-task": "Запустить задачу",
|
||||
"stop-task": "Остановить задачу",
|
||||
"save": "Сохранить"
|
||||
}
|
||||
}
|
||||
11
src/locales/ru/auth.json
Normal file
11
src/locales/ru/auth.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"auth": {
|
||||
"copyright": "Продукт компании Пересвет",
|
||||
"title": "Авторизация",
|
||||
"email": "Адрес эл. почты",
|
||||
"password": "Пароль",
|
||||
"login": "Войти",
|
||||
"help": "Проблемы? Сообщите ",
|
||||
"help-link": "администратору"
|
||||
}
|
||||
}
|
||||
12
src/locales/ru/errors.json
Normal file
12
src/locales/ru/errors.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"User with specified E-Mail and password not found": "Пользователь с указанными E-Mail и паролем не найден",
|
||||
"validator": {
|
||||
"field": "Поле {0} не соответствует требованиям: {1}",
|
||||
"required": "обязательное поле",
|
||||
"min": "мин. значение",
|
||||
"max": "макс. значение",
|
||||
"password": "требования к паролю",
|
||||
"email": "требования к E-Mail",
|
||||
"endswith": "недействительное значение"
|
||||
}
|
||||
}
|
||||
36
src/locales/ru/groups.json
Normal file
36
src/locales/ru/groups.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"groups": {
|
||||
"heads": {
|
||||
"id": "ID",
|
||||
"name": "Название",
|
||||
"issuer": "Издатель",
|
||||
"status": "Статус",
|
||||
"created-at": "Создана",
|
||||
"updated-at": "Посл. изменения",
|
||||
"permissions": "Права"
|
||||
},
|
||||
"statuses": {
|
||||
"active": "Активна",
|
||||
"deleted": "Удалена"
|
||||
},
|
||||
"creation": {
|
||||
"create": "Новая группа",
|
||||
"edit": "Редактирование группы",
|
||||
"fields": {
|
||||
"ID": "ID редактируемой группы",
|
||||
"name": "Название группы",
|
||||
"permissions": "Права пользователей в группе"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"delete-one": {
|
||||
"title": "Удаление группы",
|
||||
"message": "Вы уверены, что хотите удалить группу с ID {0}? Данное действие нельзя будет отменить"
|
||||
},
|
||||
"delete-many": {
|
||||
"title": "Удаление групп",
|
||||
"message": "Вы уверены, что хотите удалить {0} групп? Данное действие нельзя будет отменить"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/locales/ru/index.ts
Normal file
19
src/locales/ru/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import errors from './errors.json';
|
||||
import navigation from './navigation.json';
|
||||
import users from './users.json';
|
||||
import groups from './groups.json';
|
||||
import actions from './actions.json';
|
||||
import auth from './auth.json';
|
||||
import misc from './misc.json';
|
||||
import permissions from './permissions.json';
|
||||
|
||||
export default {
|
||||
...errors,
|
||||
...navigation,
|
||||
...users,
|
||||
...groups,
|
||||
...actions,
|
||||
...auth,
|
||||
...misc,
|
||||
...permissions,
|
||||
};
|
||||
6
src/locales/ru/misc.json
Normal file
6
src/locales/ru/misc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"misc": {
|
||||
"actions": "Действия",
|
||||
"selected": "Выбрано: {0}"
|
||||
}
|
||||
}
|
||||
11
src/locales/ru/navigation.json
Normal file
11
src/locales/ru/navigation.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"navigation": {
|
||||
"dashboards": "Дашборды",
|
||||
"tasks": "Задачи",
|
||||
"servers": "Серверы",
|
||||
"users": "Пользователи",
|
||||
"groups": "Группы",
|
||||
"patterns": "Паттерны",
|
||||
"logs": "Логи"
|
||||
}
|
||||
}
|
||||
19
src/locales/ru/permissions.json
Normal file
19
src/locales/ru/permissions.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"permissions": {
|
||||
"1": "Доступ к дашбордам",
|
||||
"2": "Чтение настроек",
|
||||
"3": "Редактирование настроек",
|
||||
"4": "Чтение логов",
|
||||
"5": "Чтение задач",
|
||||
"6": "Редактирование задач",
|
||||
"7": "Чтение пользователей",
|
||||
"8": "Создание пользователей",
|
||||
"9": "Редактирование пользователей",
|
||||
"10": "Удаление пользователей",
|
||||
"11": "Блокировка пользователей",
|
||||
"12": "Чтение групп",
|
||||
"13": "Создание групп",
|
||||
"14": "Редактирование групп",
|
||||
"15": "Удаление групп"
|
||||
}
|
||||
}
|
||||
9
src/locales/ru/tasks.json
Normal file
9
src/locales/ru/tasks.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"tasks": {
|
||||
"title": "Задачи",
|
||||
"new-task": {
|
||||
"title": "Новая задача",
|
||||
"parameters": "Параметры задачи"
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/locales/ru/users.json
Normal file
54
src/locales/ru/users.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"users": {
|
||||
"title": "Пользователи",
|
||||
"search": "Поиск",
|
||||
"selected-group": "Поиск по группе: ID {0}",
|
||||
"tabs": {
|
||||
"users": "Пользователи",
|
||||
"groups": "Группы"
|
||||
},
|
||||
"heads": {
|
||||
"id": "ID",
|
||||
"email": "E-Mail",
|
||||
"group": "Группа",
|
||||
"real-name": "Имя",
|
||||
"password": "Пароль",
|
||||
"status": "Статус",
|
||||
"last-visit": "Посл. вход",
|
||||
"updated-at": "Посл. изменения",
|
||||
"created-at": "Создан"
|
||||
},
|
||||
"statuses": {
|
||||
"active": "Активный",
|
||||
"blocked": "Заблокирован",
|
||||
"deleted": "Удалён",
|
||||
"set": "Установлен",
|
||||
"unset": "Не установлен"
|
||||
},
|
||||
"creation": {
|
||||
"create": "Новый пользователь",
|
||||
"edit": "Редактирование пользователя",
|
||||
"fields": {
|
||||
"ID": "ID редактируемого пользователя",
|
||||
"email": "Адрес эл. почты",
|
||||
"name": "Имя пользователя",
|
||||
"group": "Группа пользователя",
|
||||
"no-group": "Без группы"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"delete-one": {
|
||||
"title": "Удаление пользователя",
|
||||
"message": "Вы уверены, что хотите удалить пользователя с ID {0}? Данное действие нельзя будет отменить"
|
||||
},
|
||||
"delete-many": {
|
||||
"title": "Удаление пользователей",
|
||||
"message": "Вы уверены, что хотите удалить {0} пользователей? Данное действие нельзя будет отменить"
|
||||
}
|
||||
},
|
||||
"new-credentials": {
|
||||
"title": "Данные для входа",
|
||||
"message": "Адрес эл. почты: {0}\nПароль: {1}\n\nЗапомните или запишите эти данные, повторно узнать их будет невозможно"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/main.ts
Normal file
23
src/main.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* main.ts
|
||||
*
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
|
||||
// Styles
|
||||
import './assets/styles/core.scss'
|
||||
|
||||
// Composables
|
||||
import { createApp } from 'vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
registerPlugins(app)
|
||||
|
||||
app.mount('#app')
|
||||
3
src/mixins/case-converter.ts
Normal file
3
src/mixins/case-converter.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function camelToSnake(str: string): string {
|
||||
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
||||
}
|
||||
12
src/mixins/format.ts
Normal file
12
src/mixins/format.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export function secondsToHms(seconds: any): string {
|
||||
const d = Math.floor(seconds / (3600 * 24));
|
||||
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
|
||||
const dDisplay = d > 0 ? d + 'd ' : '';
|
||||
const hDisplay = h > 0 ? h + 'h ' : '';
|
||||
const mDisplay = m > 0 ? m + 'm ' : '';
|
||||
const sDisplay = s > 0 ? s + 's ' : '';
|
||||
return dDisplay + hDisplay + mDisplay + sDisplay;
|
||||
}
|
||||
7
src/mixins/item-validation.ts
Normal file
7
src/mixins/item-validation.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function isDeleted(date: string): boolean {
|
||||
return new Date(date).getFullYear() > 1;
|
||||
}
|
||||
|
||||
export function isModifiedDate(date: string): boolean {
|
||||
return isDeleted(date);
|
||||
}
|
||||
13
src/mixins/query-params.ts
Normal file
13
src/mixins/query-params.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export function setURIParams(params: URLSearchParams, remove = false): void {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
params.forEach((value: string, key: string) => {
|
||||
if (!remove) {
|
||||
url.searchParams.set(key, value);
|
||||
} else {
|
||||
url.searchParams.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
window.history.replaceState(null, '', url);
|
||||
}
|
||||
17
src/mixins/validator.ts
Normal file
17
src/mixins/validator.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export function validEmail(s?: string): boolean {
|
||||
return (
|
||||
s !== undefined &&
|
||||
s.trim()?.length > 0 &&
|
||||
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i.test(
|
||||
s,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function validName(s?: string): boolean {
|
||||
return (
|
||||
s !== undefined &&
|
||||
s.trim()?.length > 0 &&
|
||||
!/[^a-zA-Zа-яА-Я0-9]+$/gim.test(s)
|
||||
);
|
||||
}
|
||||
5
src/pages/README.md
Normal file
5
src/pages/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Pages
|
||||
|
||||
Vue components created in this folder will automatically be converted to navigatable routes.
|
||||
|
||||
Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository.
|
||||
89
src/pages/auth.vue
Normal file
89
src/pages/auth.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import ErrorPlate from '@/components/ErrorPlate.vue';
|
||||
import LogoComponent from '@/components/Icons/LogoComponent.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useLocale, useTheme } from 'vuetify';
|
||||
|
||||
const { t } = useLocale();
|
||||
const theme = useTheme();
|
||||
const currentTheme = computed(() => theme.global.name.value);
|
||||
|
||||
const $auth = useAuthStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex relative flex-col items-center justify-center h-full">
|
||||
<!-- Card -->
|
||||
<v-card width="800px" class="p-12 rounded-3xl" elevation="0">
|
||||
<!-- Logo -->
|
||||
<LogoComponent :theme="currentTheme" />
|
||||
<div class="grid grid-cols-5 mt-8">
|
||||
<!-- Left side -->
|
||||
<div class="items-start flex flex-col col-span-2">
|
||||
<span class="text-3xl font-medium">{{
|
||||
t('$vuetify.auth.title')
|
||||
}}</span>
|
||||
<!-- Copyright -->
|
||||
<span class="text-sm mt-2 opacity-75">{{
|
||||
t('$vuetify.auth.copyright')
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- Right side -->
|
||||
<div class="flex flex-col col-span-3">
|
||||
<!-- Login -->
|
||||
<v-text-field
|
||||
v-model="$auth.form.email"
|
||||
:label="t('$vuetify.auth.email')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
flat
|
||||
hide-details
|
||||
rounded
|
||||
density="comfortable"
|
||||
/>
|
||||
<!-- Password -->
|
||||
<v-text-field
|
||||
v-model="$auth.form.password"
|
||||
:label="t('$vuetify.auth.password')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
hide-details
|
||||
flat
|
||||
rounded
|
||||
density="comfortable"
|
||||
class="mt-2"
|
||||
/>
|
||||
<!-- Error -->
|
||||
<ErrorPlate :error="$auth.err" no-border />
|
||||
<!-- Help -->
|
||||
<span class="mt-5 flex items-center gap-1"
|
||||
>{{ t('$vuetify.auth.help') }}
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center gap-1 text-nowrap text-primary"
|
||||
>
|
||||
<span class="font-bold">{{ t('$vuetify.auth.help-link') }}</span>
|
||||
<span class="mso text-sm">open_in_new</span>
|
||||
</a>
|
||||
</span>
|
||||
<!-- Buttons -->
|
||||
<v-card-actions class="flex justify-end px-0 mt-8">
|
||||
<v-btn
|
||||
:variant="currentTheme === 'dark' ? 'flat' : 'flat'"
|
||||
color="primary"
|
||||
class="px-8 rounded-3xl"
|
||||
@click="$auth.doAuth()"
|
||||
>
|
||||
<i class="mi-log-in"></i>
|
||||
<span>{{ t('$vuetify.auth.login') }}</span>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- <route lang="yaml">
|
||||
meta:
|
||||
layout: centered
|
||||
</route> -->
|
||||
7
src/pages/index.vue
Normal file
7
src/pages/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<HelloWorld />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
16
src/pages/tasks/components/TaskParameters.vue
Normal file
16
src/pages/tasks/components/TaskParameters.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { useLocale } from 'vuetify';
|
||||
|
||||
// Common variables
|
||||
const { t } = useLocale();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-expansion-panel
|
||||
:title="t('$vuetify.')"
|
||||
text="Lorem ipsum dolor sit amet consectetur adipisicing elit. Commodi, ratione debitis quis est labore voluptatibus! Eaque cupiditate minima"
|
||||
>
|
||||
</v-expansion-panel>
|
||||
</div>
|
||||
</template>
|
||||
140
src/pages/tasks/components/TweaksConstructor.vue
Normal file
140
src/pages/tasks/components/TweaksConstructor.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<script lang="ts" setup>
|
||||
import { Line as LineChart } from 'vue-chartjs';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
} from 'chart.js';
|
||||
import { ref } from 'vue';
|
||||
import { useTasksStore } from '@/stores/tasks';
|
||||
import { timeUnits } from '@/enums/time-units.enum';
|
||||
|
||||
const unitsArray = Object.values(timeUnits);
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
);
|
||||
|
||||
const $tasks = useTasksStore();
|
||||
|
||||
const result = ref<string | null>(null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container>
|
||||
<v-card>
|
||||
<v-card-title> Interval Form </v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model.number="$tasks.duration"
|
||||
label="Duration (seconds)"
|
||||
type="number"
|
||||
:rules="[(v) => v > 0 || 'Must be greater than zero']"
|
||||
required
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model.number="$tasks.limit"
|
||||
label="Limit"
|
||||
type="number"
|
||||
:rules="[(v) => v > 0 || 'Must be greater than zero']"
|
||||
required
|
||||
></v-text-field>
|
||||
<v-btn @click="$tasks.addInterval">Add Interval</v-btn>
|
||||
<div v-for="(interval, index) in $tasks.intervals" :key="index">
|
||||
<v-row>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
v-model.number="interval.start"
|
||||
label="Start Time"
|
||||
type="number"
|
||||
:rules="[
|
||||
(v) => v >= 0 || 'Must be positive',
|
||||
(v) =>
|
||||
$tasks.validateStartTime(v, interval.startUnit, index) ||
|
||||
'Start time must be valid and within duration',
|
||||
]"
|
||||
required
|
||||
></v-text-field>
|
||||
<v-select
|
||||
v-model="interval.startUnit"
|
||||
:items="unitsArray"
|
||||
label="Unit"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
v-model.number="interval.end"
|
||||
label="End Time"
|
||||
type="number"
|
||||
:rules="[
|
||||
(v) =>
|
||||
$tasks.validateEndTime(v, interval.endUnit, index) ||
|
||||
'End time must be valid and within duration',
|
||||
]"
|
||||
required
|
||||
></v-text-field>
|
||||
<v-select
|
||||
v-model="interval.endUnit"
|
||||
:items="unitsArray"
|
||||
label="Unit"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<v-text-field
|
||||
v-model.number="interval.value"
|
||||
label="Value"
|
||||
type="number"
|
||||
:rules="[
|
||||
(v) =>
|
||||
v <= $tasks.limit ||
|
||||
`Value must be less than or equal to ${$tasks.limit}`,
|
||||
(v) => v >= 0 || 'Must be greater than or equal to 0',
|
||||
]"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-btn icon @click="$tasks.removeInterval(index)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn type="submit">Submit</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-card v-if="result">
|
||||
<v-card-title> Result </v-card-title>
|
||||
<v-card-text>
|
||||
<pre>{{ result }}</pre>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card v-if="$tasks.chartData.labels.length">
|
||||
<v-card-title> Graph </v-card-title>
|
||||
<v-card-text>
|
||||
<LineChart :data="$tasks.chartData" :options="$tasks.chartOptions" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
15
src/pages/tasks/constructor.vue
Normal file
15
src/pages/tasks/constructor.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocale } from 'vuetify';
|
||||
import TweaksConstructor from './components/TweaksConstructor.vue';
|
||||
|
||||
// Common variables
|
||||
const { t } = useLocale();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<TweaksConstructor />
|
||||
|
||||
<!-- -->
|
||||
</div>
|
||||
</template>
|
||||
46
src/pages/tasks/index.vue
Normal file
46
src/pages/tasks/index.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocale } from 'vuetify';
|
||||
|
||||
// Common variables
|
||||
const { t } = useLocale();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<v-app-bar prominent class="elevation-0 border-b">
|
||||
<!-- Title -->
|
||||
<span class="text-h6 ml-5 mr-10">{{ t('$vuetify.users.title') }}</span>
|
||||
|
||||
<!-- Create group/user -->
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
class="mr-3 px-3"
|
||||
@click="openCreationModal()"
|
||||
>
|
||||
<span class="mso mr-1 text-xl">{{
|
||||
tab === UsersPageTabs.USERS ? 'person_add' : 'group_add'
|
||||
}}</span>
|
||||
<span>{{
|
||||
t(
|
||||
tab === UsersPageTabs.USERS
|
||||
? '$vuetify.actions.create-user'
|
||||
: '$vuetify.actions.create-group',
|
||||
)
|
||||
}}</span>
|
||||
</v-btn>
|
||||
|
||||
<!-- User creation modal -->
|
||||
<AddUser v-model="$users.createModal" />
|
||||
<!-- Group creation modal -->
|
||||
<AddGroup v-model="$groups.createModal" />
|
||||
|
||||
<!-- Refresh users -->
|
||||
<v-btn class="px-3" color="primary" variant="text" @click="getAll()">
|
||||
<span class="mso mr-1 text-xl">refresh</span>
|
||||
<span>{{ t('$vuetify.actions.refresh') }}</span>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
</div>
|
||||
</template>
|
||||
134
src/pages/users/components/AddGroup.vue
Normal file
134
src/pages/users/components/AddGroup.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useLocale, useTheme } from 'vuetify';
|
||||
import { usePermissionsStore } from '@/stores/permissions';
|
||||
import { useGroupsStore } from '@/stores/groups';
|
||||
import { validName } from '@/mixins/validator';
|
||||
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const $permissions = usePermissionsStore();
|
||||
const $groups = useGroupsStore();
|
||||
|
||||
const { t } = useLocale();
|
||||
const theme = useTheme();
|
||||
const currentTheme = computed(() => theme.global.name.value);
|
||||
|
||||
const items = computed(() =>
|
||||
Object.values($permissions.all).map((v) => ({
|
||||
title: t(`$vuetify.permissions.${v}`),
|
||||
value: v,
|
||||
})),
|
||||
);
|
||||
|
||||
const validForm: ComputedRef<boolean> = computed(
|
||||
() =>
|
||||
!(
|
||||
!$groups.form.name ||
|
||||
!$groups.form.permissions ||
|
||||
!validName($groups.form.name)
|
||||
),
|
||||
);
|
||||
|
||||
const onConfirm = () => {
|
||||
$groups.isEditing ? $groups.editGroup() : $groups.createGroup();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog persistent :model-value="modelValue" width="450">
|
||||
<v-card>
|
||||
<template v-slot:title>
|
||||
<v-card-title class="p-0 w-full flex items-center">
|
||||
<span class="mso mr-1 text-2xl">{{
|
||||
$groups.isEditing ? 'group' : 'group_add'
|
||||
}}</span>
|
||||
<span>{{
|
||||
t(
|
||||
`$vuetify.groups.creation.${$groups.isEditing ? 'edit' : 'create'}`,
|
||||
)
|
||||
}}</span>
|
||||
</v-card-title>
|
||||
</template>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="flex flex-col w-full px-5">
|
||||
<!-- ID of group -->
|
||||
<v-text-field
|
||||
v-if="$groups.isEditing"
|
||||
v-model="$groups.form.ID"
|
||||
readonly
|
||||
:label="t('$vuetify.groups.creation.fields.ID')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
hide-details
|
||||
flat
|
||||
density="comfortable"
|
||||
class="mb-3"
|
||||
/>
|
||||
<!-- Name of group -->
|
||||
<v-text-field
|
||||
v-model="$groups.form.name"
|
||||
:label="t('$vuetify.groups.creation.fields.name')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
hide-details
|
||||
flat
|
||||
density="comfortable"
|
||||
/>
|
||||
|
||||
<!-- Group permissions -->
|
||||
<v-select
|
||||
v-model="$groups.form.permissions"
|
||||
:items="items"
|
||||
:label="t('$vuetify.groups.creation.fields.permissions')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
class="mt-3"
|
||||
multiple
|
||||
hide-details
|
||||
flat
|
||||
density="comfortable"
|
||||
>
|
||||
<template v-slot:selection="{ item, index }">
|
||||
<v-chip v-if="index < 2">
|
||||
<span>{{ item.title }}</span>
|
||||
</v-chip>
|
||||
<span
|
||||
v-if="index === 2"
|
||||
class="text-grey text-caption align-self-center"
|
||||
>
|
||||
(+{{ ($groups.form.permissions?.length ?? 0) - 2 }})
|
||||
</span>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<!-- Error -->
|
||||
<ErrorPlate :error="$groups.errorCreate" no-border />
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<v-card-actions class="flex justify-end p-5 gap-1">
|
||||
<!-- Cancel -->
|
||||
<v-btn
|
||||
variant="text"
|
||||
class="px-4"
|
||||
:text="t('$vuetify.actions.cancel')"
|
||||
@click="$groups.closeModal()"
|
||||
></v-btn>
|
||||
<!-- Confirm -->
|
||||
<v-btn
|
||||
variant="flat"
|
||||
color="primary"
|
||||
class="px-4"
|
||||
:text="t('$vuetify.actions.apply')"
|
||||
:loading="$groups.loadingCreate"
|
||||
:disabled="!validForm"
|
||||
:class="{
|
||||
'disabled-button': !validForm,
|
||||
}"
|
||||
@click="onConfirm()"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
150
src/pages/users/components/AddUser.vue
Normal file
150
src/pages/users/components/AddUser.vue
Normal file
@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocale, useTheme } from 'vuetify';
|
||||
import { useUsersStore } from '@/stores/users';
|
||||
import { validEmail, validName } from '@/mixins/validator';
|
||||
import { useGroupsStore } from '@/stores/groups';
|
||||
import { useInfoStore } from '@/stores/info';
|
||||
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const $users = useUsersStore();
|
||||
const $groups = useGroupsStore();
|
||||
const $info = useInfoStore();
|
||||
|
||||
const { t } = useLocale();
|
||||
const theme = useTheme();
|
||||
const currentTheme = computed(() => theme.global.name.value);
|
||||
|
||||
const groupItems = computed(() => {
|
||||
const d = Object.values($groups.all).map((v) => ({
|
||||
title: v.name,
|
||||
value: v.ID,
|
||||
}));
|
||||
d.unshift({
|
||||
title: t('$vuetify.users.creation.fields.no-group'),
|
||||
value: -1,
|
||||
});
|
||||
|
||||
return d;
|
||||
});
|
||||
|
||||
const validForm: ComputedRef<boolean> = computed(
|
||||
() =>
|
||||
!(
|
||||
!$users.form.email ||
|
||||
!$users.form.groupID ||
|
||||
!$users.form.realName ||
|
||||
!validEmail($users.form.email) ||
|
||||
!validName($users.form.realName)
|
||||
),
|
||||
);
|
||||
|
||||
const onConfirm = async () => {
|
||||
const res = await ($users.isEditing
|
||||
? $users.editUser()
|
||||
: $users.createUser());
|
||||
if (res) {
|
||||
console.log(res);
|
||||
$info.setInfoMessage(
|
||||
t('$vuetify.users.new-credentials.title'),
|
||||
t('$vuetify.users.new-credentials.message', res.email, res.password),
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog persistent :model-value="modelValue" width="450">
|
||||
<v-card>
|
||||
<template v-slot:title>
|
||||
<v-card-title class="p-0 w-full flex items-center">
|
||||
<span class="mso mr-1 text-2xl">{{
|
||||
$users.isEditing ? 'person' : 'person_add'
|
||||
}}</span>
|
||||
<span>{{
|
||||
t(`$vuetify.users.creation.${$users.isEditing ? 'edit' : 'create'}`)
|
||||
}}</span>
|
||||
</v-card-title>
|
||||
</template>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="flex flex-col w-full px-5">
|
||||
<!-- ID of user -->
|
||||
<v-text-field
|
||||
v-if="$users.isEditing"
|
||||
v-model="$users.form.ID"
|
||||
readonly
|
||||
:label="t('$vuetify.users.creation.fields.ID')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
hide-details
|
||||
flat
|
||||
density="comfortable"
|
||||
class="mb-3"
|
||||
/>
|
||||
<!-- User email -->
|
||||
<v-text-field
|
||||
v-model="$users.form.email"
|
||||
:label="t('$vuetify.users.creation.fields.email')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
hide-details
|
||||
flat
|
||||
density="comfortable"
|
||||
/>
|
||||
<!-- Name of user -->
|
||||
<v-text-field
|
||||
v-model="$users.form.realName"
|
||||
:label="t('$vuetify.users.creation.fields.name')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
hide-details
|
||||
class="mt-3"
|
||||
flat
|
||||
density="comfortable"
|
||||
/>
|
||||
<!-- User group -->
|
||||
<v-autocomplete
|
||||
v-model="$users.form.groupID"
|
||||
:items="groupItems"
|
||||
:label="t('$vuetify.users.creation.fields.group')"
|
||||
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||
class="mt-3"
|
||||
hide-details
|
||||
flat
|
||||
density="comfortable"
|
||||
>
|
||||
<template v-slot:selection="{ item }">
|
||||
<span>{{ item.title }}</span>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
|
||||
<!-- Error -->
|
||||
<ErrorPlate :error="$users.errorCreate" no-border />
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<v-card-actions class="flex justify-end p-5 gap-1">
|
||||
<!-- Cancel -->
|
||||
<v-btn
|
||||
variant="text"
|
||||
class="px-4"
|
||||
:text="t('$vuetify.actions.cancel')"
|
||||
@click="$users.closeModal()"
|
||||
></v-btn>
|
||||
<!-- Confirm -->
|
||||
<v-btn
|
||||
variant="flat"
|
||||
color="primary"
|
||||
class="px-4"
|
||||
:text="t('$vuetify.actions.apply')"
|
||||
:loading="$groups.loadingCreate"
|
||||
:disabled="!validForm"
|
||||
:class="{
|
||||
'disabled-button': !validForm,
|
||||
}"
|
||||
@click="onConfirm()"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
173
src/pages/users/components/GroupsTab.vue
Normal file
173
src/pages/users/components/GroupsTab.vue
Normal file
@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onBeforeMount, type ComputedRef } from 'vue';
|
||||
import { useLocale } from 'vuetify';
|
||||
import { ITEMS_PER_PAGE } from '@/constants/static';
|
||||
import { useGroupsStore } from '@/stores/groups';
|
||||
import { isDeleted, isModifiedDate } from '@/mixins/item-validation';
|
||||
import { Group } from '@/types/group';
|
||||
import Moment from '@/plugins/moment';
|
||||
import { useConfirmationStore } from '@/stores/confirmation';
|
||||
|
||||
defineProps<{
|
||||
tableHeight: number;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const $groups = useGroupsStore();
|
||||
const $confirmation = useConfirmationStore();
|
||||
|
||||
const headers: ComputedRef<any[]> = computed(() => [
|
||||
{ title: t('$vuetify.groups.heads.id'), key: 'ID' },
|
||||
{ title: t('$vuetify.groups.heads.name'), key: 'name' },
|
||||
{ title: t('$vuetify.groups.heads.issuer'), key: 'issuer' },
|
||||
{ title: t('$vuetify.groups.heads.status'), key: 'status' },
|
||||
{ title: t('$vuetify.groups.heads.created-at'), key: 'createdAt' },
|
||||
{ title: t('$vuetify.groups.heads.updated-at'), key: 'updatedAt' },
|
||||
{
|
||||
title: t('$vuetify.misc.actions'),
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
align: 'center',
|
||||
},
|
||||
]);
|
||||
|
||||
// Actions
|
||||
const editGroup = (g: Group) => {
|
||||
$groups.openModal();
|
||||
$groups.startEditGroup(g);
|
||||
};
|
||||
const deleteGroup = (g: Group) => {
|
||||
$confirmation.openModal();
|
||||
$confirmation.setTitle(t('$vuetify.groups.delete.delete-one.title'));
|
||||
$confirmation.setMessage(
|
||||
t('$vuetify.groups.delete.delete-one.message', [g.ID]),
|
||||
);
|
||||
$confirmation.setOnConfirm(() => $groups.deleteGroup(g.ID));
|
||||
};
|
||||
|
||||
// Status
|
||||
const calculateStatus = (group: Group) => {
|
||||
return isDeleted(group.deletedAt)
|
||||
? t('$vuetify.groups.statuses.deleted')
|
||||
: t('$vuetify.groups.statuses.active');
|
||||
};
|
||||
const calculateStatusColor = (group: Group) => {
|
||||
return isDeleted(group.deletedAt) ? 'text-error' : '';
|
||||
};
|
||||
|
||||
onBeforeMount($groups.getAll);
|
||||
watch(() => [$groups.page, $groups.perPage, $groups.orderBy], $groups.getAll);
|
||||
watch(
|
||||
() => [$groups.search],
|
||||
() => $groups.searchWithCooldown(1 * 1000),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-data-table-server
|
||||
v-model="$groups.selected"
|
||||
:search="$groups.search"
|
||||
:header-props="{ nowrap: true }"
|
||||
:headers="headers"
|
||||
:items="$groups.all"
|
||||
:items-per-page-options="ITEMS_PER_PAGE"
|
||||
:items-per-page="$groups.perPage"
|
||||
:items-length="$groups.cursor?.totalRows"
|
||||
:loading="$groups.loading"
|
||||
:height="tableHeight - 62"
|
||||
item-value="ID"
|
||||
fixed-footer
|
||||
fixed-header
|
||||
show-select
|
||||
checkbox-color="primary"
|
||||
hover
|
||||
density="compact"
|
||||
style="--fixed: 10"
|
||||
@update:sort-by="$groups.setSort"
|
||||
@update:page="$groups.setPage"
|
||||
@update:itemsPerPage="$groups.setPerPage"
|
||||
>
|
||||
<!-- Group name -->
|
||||
<template v-slot:item.name="{ item }">
|
||||
<template v-if="item.name?.length">
|
||||
<a
|
||||
:href="`?group_id=${item.ID}`"
|
||||
target="_blank"
|
||||
class="flex items-center gap-1 text-nowrap text-primary"
|
||||
>
|
||||
<span class="font-bold">{{ item.name }}</span>
|
||||
<span class="mso text-sm">open_in_new</span>
|
||||
</a>
|
||||
</template>
|
||||
<span v-else class="text-nowrap">—</span>
|
||||
</template>
|
||||
<!-- Group issuer -->
|
||||
<template v-slot:item.issuer="{ item }">
|
||||
<template v-if="item.issuer">
|
||||
<a
|
||||
:href="`?input=${item.issuer.ID}`"
|
||||
target="_blank"
|
||||
class="flex items-center gap-1 text-nowrap text-primary"
|
||||
>
|
||||
<span class="font-bold">{{
|
||||
`${item.issuer.email} (ID: ${item.issuer.ID})`
|
||||
}}</span>
|
||||
<span class="mso text-sm">open_in_new</span>
|
||||
</a>
|
||||
</template>
|
||||
<span v-else class="text-nowrap">—</span>
|
||||
</template>
|
||||
<!-- Group status -->
|
||||
<template v-slot:item.status="{ item }">
|
||||
<span :class="calculateStatusColor(item)">
|
||||
{{ calculateStatus(item) }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Created at -->
|
||||
<template v-slot:item.createdAt="{ item }">
|
||||
<template v-if="isModifiedDate(item.createdAt)">
|
||||
<span class="text-nowrap">
|
||||
{{ Moment.$moment(item.createdAt).format('DD MMM YYYY HH:mm') }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="text-nowrap">—</span>
|
||||
</template>
|
||||
<!-- Updated at -->
|
||||
<template v-slot:item.updatedAt="{ item }">
|
||||
<template v-if="isModifiedDate(item.updatedAt)">
|
||||
<span class="text-nowrap">
|
||||
{{ Moment.$moment(item.updatedAt).format('DD MMM YYYY HH:mm') }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="text-nowrap">—</span>
|
||||
</template>
|
||||
<!-- Actions -->
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
class="mr-1"
|
||||
:disabled="isDeleted(item.deletedAt)"
|
||||
:loading="$groups.loadingActionsID.includes(item.ID)"
|
||||
@click="editGroup(item)"
|
||||
>
|
||||
<span class="mso text-xl">edit</span>
|
||||
</v-btn>
|
||||
<!-- Delete -->
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
:disabled="isDeleted(item.deletedAt)"
|
||||
:loading="$groups.loadingActionsID.includes(item.ID)"
|
||||
@click="deleteGroup(item)"
|
||||
>
|
||||
<span class="mso text-xl">close</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</template>
|
||||
259
src/pages/users/components/UsersTab.vue
Normal file
259
src/pages/users/components/UsersTab.vue
Normal file
@ -0,0 +1,259 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onBeforeMount, type ComputedRef } from 'vue';
|
||||
import { useLocale } from 'vuetify';
|
||||
import { ITEMS_PER_PAGE } from '@/constants/static';
|
||||
import { useUsersStore } from '@/stores/users';
|
||||
import { UserActions } from '@/enums/user-actions.enum';
|
||||
import { User } from '@/types/user';
|
||||
import Moment from '@/plugins/moment';
|
||||
import { isDeleted, isModifiedDate } from '@/mixins/item-validation';
|
||||
import { useConfirmationStore } from '@/stores/confirmation';
|
||||
import { useInfoStore } from '@/stores/info';
|
||||
|
||||
defineProps<{
|
||||
tableHeight: number;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const $users = useUsersStore();
|
||||
const $confirmation = useConfirmationStore();
|
||||
const $info = useInfoStore();
|
||||
|
||||
const headers: ComputedRef<any[]> = computed(() => [
|
||||
{ title: t('$vuetify.users.heads.id'), key: 'ID' },
|
||||
{ title: t('$vuetify.users.heads.email'), key: 'email' },
|
||||
{ title: t('$vuetify.users.heads.group'), key: 'group' },
|
||||
{ title: t('$vuetify.users.heads.real-name'), key: 'realName' },
|
||||
{ title: t('$vuetify.users.heads.password'), key: 'isRequiredToSetPassword' },
|
||||
{ title: t('$vuetify.users.heads.status'), key: 'isActive' },
|
||||
{ title: t('$vuetify.users.heads.last-visit'), key: 'lastLogin' },
|
||||
{ title: t('$vuetify.users.heads.created-at'), key: 'createdAt' },
|
||||
{ title: t('$vuetify.users.heads.updated-at'), key: 'updatedAt' },
|
||||
{
|
||||
title: t('$vuetify.misc.actions'),
|
||||
key: 'actions',
|
||||
sortable: false,
|
||||
align: 'center',
|
||||
},
|
||||
]);
|
||||
const actions = computed(() => [
|
||||
{
|
||||
title: t('$vuetify.actions.edit'),
|
||||
value: UserActions.EDIT,
|
||||
blocked: false,
|
||||
onClick: (u: User) => editUser(u),
|
||||
},
|
||||
{
|
||||
title: t('$vuetify.actions.block'),
|
||||
value: UserActions.BLOCK,
|
||||
blocked: false,
|
||||
onClick: (u: User) => $users.blockMany([u.ID]),
|
||||
},
|
||||
{
|
||||
title: t('$vuetify.actions.reset-password'),
|
||||
value: UserActions.RESET_PASSWORD,
|
||||
blocked: false,
|
||||
onClick: (u: User) => resetPassword(u),
|
||||
},
|
||||
{
|
||||
title: t('$vuetify.actions.unblock'),
|
||||
value: UserActions.UNBLOCK,
|
||||
blocked: true,
|
||||
onClick: (u: User) => $users.unblockMany([u.ID]),
|
||||
},
|
||||
]);
|
||||
// Actions
|
||||
const editUser = (u: User) => {
|
||||
$users.openModal();
|
||||
$users.startEditUser(u);
|
||||
};
|
||||
const deleteUser = (u: User) => {
|
||||
$confirmation.openModal();
|
||||
$confirmation.setTitle(t('$vuetify.users.delete.delete-one.title'));
|
||||
$confirmation.setMessage(
|
||||
t('$vuetify.users.delete.delete-one.message', [u.ID]),
|
||||
);
|
||||
$confirmation.setOnConfirm(() => $users.deleteUser(u.ID));
|
||||
};
|
||||
const resetPassword = async (u: User) => {
|
||||
const res = await $users.resetPassword(u.ID);
|
||||
if (res) {
|
||||
console.log(res);
|
||||
$info.setInfoMessage(
|
||||
t('$vuetify.users.new-credentials.title'),
|
||||
t('$vuetify.users.new-credentials.message', res.email, res.password),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Status
|
||||
const calculateUserStatus = (user: User) => {
|
||||
return isDeleted(user.deletedAt)
|
||||
? t('$vuetify.users.statuses.deleted')
|
||||
: user.isActive
|
||||
? t('$vuetify.users.statuses.active')
|
||||
: t('$vuetify.users.statuses.blocked');
|
||||
};
|
||||
const calculateStatusColor = (user: User) => {
|
||||
return isDeleted(user.deletedAt)
|
||||
? 'text-error'
|
||||
: user.isActive
|
||||
? ''
|
||||
: 'text-warning';
|
||||
};
|
||||
// Password status
|
||||
const calculateUserPasswordStatus = (user: User) => {
|
||||
return user.isRequiredToSetPassword
|
||||
? t('$vuetify.users.statuses.unset')
|
||||
: t('$vuetify.users.statuses.set');
|
||||
};
|
||||
|
||||
onBeforeMount($users.getAll);
|
||||
watch(
|
||||
() => [$users.page, $users.perPage, $users.orderBy, $users.groupId],
|
||||
$users.getAll,
|
||||
);
|
||||
watch(
|
||||
() => [$users.search],
|
||||
() => $users.searchWithCooldown(1 * 1000),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-data-table-server
|
||||
v-model="$users.selected"
|
||||
:search="$users.search"
|
||||
:header-props="{ nowrap: true }"
|
||||
:headers="headers"
|
||||
:items="$users.all"
|
||||
:items-per-page-options="ITEMS_PER_PAGE"
|
||||
:items-per-page="$users.perPage"
|
||||
:items-length="$users.cursor?.totalRows"
|
||||
:loading="$users.loading"
|
||||
:height="tableHeight - 62"
|
||||
item-value="ID"
|
||||
fixed-footer
|
||||
fixed-header
|
||||
show-select
|
||||
hover
|
||||
density="compact"
|
||||
style="--fixed: 10"
|
||||
@update:sort-by="$users.setSort"
|
||||
@update:page="$users.setPage"
|
||||
@update:itemsPerPage="$users.setPerPage"
|
||||
>
|
||||
<!-- Group -->
|
||||
<template v-slot:item.group="{ item }">
|
||||
<template v-if="item.group">
|
||||
<a
|
||||
:href="`?group_id=${item.group.ID}`"
|
||||
target="_blank"
|
||||
class="flex items-center gap-1 text-nowrap text-primary"
|
||||
>
|
||||
<span class="font-bold">{{ item.group.name }}</span>
|
||||
<span class="mso text-sm">open_in_new</span>
|
||||
</a>
|
||||
</template>
|
||||
<span v-else class="text-nowrap">—</span>
|
||||
</template>
|
||||
<!-- Real name -->
|
||||
<template v-slot:item.realName="{ item }">
|
||||
<span class="text-nowrap">
|
||||
{{ item.realName?.length ? item.realName : '—' }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Required to set password -->
|
||||
<template v-slot:item.isRequiredToSetPassword="{ item }">
|
||||
<span
|
||||
:class="{
|
||||
'text-error': item.isRequiredToSetPassword,
|
||||
}"
|
||||
class="text-nowrap"
|
||||
>
|
||||
{{ calculateUserPasswordStatus(item) }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- User status -->
|
||||
<template v-slot:item.isActive="{ item }">
|
||||
<span :class="calculateStatusColor(item)">
|
||||
{{ calculateUserStatus(item) }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- Last visit -->
|
||||
<template v-slot:item.lastLogin="{ item }">
|
||||
<template v-if="isModifiedDate(item.lastLogin)">
|
||||
<span class="text-nowrap">
|
||||
{{ Moment.$moment(item.lastLogin).format('DD MMM YYYY HH:mm') }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="text-nowrap">—</span>
|
||||
</template>
|
||||
<!-- Created at -->
|
||||
<template v-slot:item.createdAt="{ item }">
|
||||
<template v-if="isModifiedDate(item.createdAt)">
|
||||
<span class="text-nowrap">
|
||||
{{ Moment.$moment(item.createdAt).format('DD MMM YYYY HH:mm') }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="text-nowrap">—</span>
|
||||
</template>
|
||||
<!-- Updated at -->
|
||||
<template v-slot:item.updatedAt="{ item }">
|
||||
<template v-if="isModifiedDate(item.updatedAt)">
|
||||
<span class="text-nowrap">
|
||||
{{ Moment.$moment(item.updatedAt).format('DD MMM YYYY HH:mm') }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="text-nowrap">—</span>
|
||||
</template>
|
||||
<!-- Actions -->
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-menu transition="none">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
class="mr-1"
|
||||
:disabled="isDeleted(item.deletedAt)"
|
||||
:loading="$users.loadingActionsID.includes(item.ID)"
|
||||
>
|
||||
<span class="mso text-xl">more_vert</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list class="mt-2 accent-modal" elevation="0" density="compact">
|
||||
<template v-for="(act, index) of actions">
|
||||
<v-list-item
|
||||
v-if="
|
||||
(act?.blocked && !item.isActive) ||
|
||||
(!act?.blocked && item.isActive)
|
||||
"
|
||||
:key="index"
|
||||
:value="act.value"
|
||||
lines="one"
|
||||
class="cm-mini"
|
||||
@click="act.onClick ? act.onClick(item) : null"
|
||||
>
|
||||
<v-list-item-title>{{ act.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<!-- Delete -->
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
:disabled="isDeleted(item.deletedAt)"
|
||||
:loading="$users.loadingActionsID.includes(item.ID)"
|
||||
@click="deleteUser(item)"
|
||||
>
|
||||
<span class="mso text-xl">close</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</template>
|
||||
346
src/pages/users/index.vue
Normal file
346
src/pages/users/index.vue
Normal file
@ -0,0 +1,346 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocale } from 'vuetify';
|
||||
import { UsersPageTabs } from '@/enums/user-tab.enum';
|
||||
import { useUsersStore } from '@/stores/users';
|
||||
import { useGroupsStore } from '@/stores/groups';
|
||||
import { useConfirmationStore } from '@/stores/confirmation';
|
||||
import ErrorPlate from '@/components/ErrorPlate.vue';
|
||||
import AddUser from './components/AddUser.vue';
|
||||
import AddGroup from './components/AddGroup.vue';
|
||||
import UsersTab from './components/UsersTab.vue';
|
||||
import GroupsTab from './components/GroupsTab.vue';
|
||||
import { setURIParams } from '@/mixins/query-params';
|
||||
import { OrderBy } from '@/types/order-by';
|
||||
|
||||
// Common variables
|
||||
const { t } = useLocale();
|
||||
|
||||
const $users = useUsersStore();
|
||||
const $groups = useGroupsStore();
|
||||
const $confirmation = useConfirmationStore();
|
||||
|
||||
const tableHeight: Ref<number> = ref(500);
|
||||
setInterval(() => {
|
||||
const wY = window.innerHeight;
|
||||
const el = document.getElementById('table');
|
||||
if (el) {
|
||||
const bb = el.getBoundingClientRect();
|
||||
tableHeight.value = wY - bb.top;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
//
|
||||
// Tabs
|
||||
//
|
||||
const tab: Ref<UsersPageTabs> = ref(UsersPageTabs.USERS);
|
||||
const parseURLSearch = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
// Get tab
|
||||
if (params.has('tab')) {
|
||||
tab.value = Number(params.get('tab'));
|
||||
}
|
||||
// Get page
|
||||
if (params.has('page')) {
|
||||
const v = Number(params.get('page'));
|
||||
tab.value === UsersPageTabs.USERS ? $users.setPage(v) : $groups.setPage(v);
|
||||
}
|
||||
// Get per page count
|
||||
if (params.has('count')) {
|
||||
const v = Number(params.get('count'));
|
||||
tab.value === UsersPageTabs.USERS
|
||||
? $users.setPerPage(v)
|
||||
: $groups.setPerPage(v);
|
||||
}
|
||||
// Get sort
|
||||
if (params.has('key') && params.has('order')) {
|
||||
const v: OrderBy = {
|
||||
key: params.get('key') ?? 'ID',
|
||||
order: params.get('order') ?? 'asc',
|
||||
};
|
||||
tab.value === UsersPageTabs.USERS
|
||||
? $users.setSort([v])
|
||||
: $groups.setSort([v]);
|
||||
}
|
||||
// Get search
|
||||
if (params.has('input')) {
|
||||
const v = params.get('input') ?? '';
|
||||
tab.value === UsersPageTabs.USERS
|
||||
? ($users.search = v)
|
||||
: ($groups.search = v);
|
||||
}
|
||||
if (params.has('group_id')) {
|
||||
const v = params.get('group_id') ?? '';
|
||||
if (v?.length > 0) {
|
||||
tab.value = UsersPageTabs.USERS;
|
||||
$users.setGroupId(v);
|
||||
$users.search = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
onBeforeMount(() => {
|
||||
parseURLSearch();
|
||||
tab.value === UsersPageTabs.USERS ? $groups.getAll() : $users.getAll();
|
||||
});
|
||||
onUpdated(parseURLSearch);
|
||||
watch(tab, () => {
|
||||
setURIParams(new URLSearchParams([['tab', String(tab.value)]]), !tab.value);
|
||||
resetActions();
|
||||
getAll();
|
||||
});
|
||||
|
||||
//
|
||||
// Creation
|
||||
//
|
||||
const openCreationModal = () => {
|
||||
tab.value === UsersPageTabs.USERS ? $users.openModal() : $groups.openModal();
|
||||
};
|
||||
|
||||
//
|
||||
// Search model
|
||||
//
|
||||
const $search: WritableComputedRef<string> = computed({
|
||||
get() {
|
||||
return tab.value === UsersPageTabs.USERS ? $users.search : $groups.search;
|
||||
},
|
||||
set(v: string) {
|
||||
tab.value === UsersPageTabs.USERS
|
||||
? ($users.search = v)
|
||||
: ($groups.search = v);
|
||||
},
|
||||
});
|
||||
//
|
||||
// Error
|
||||
//
|
||||
const $error: ComputedRef<string> = computed(() =>
|
||||
tab.value === UsersPageTabs.USERS ? $users.error : $groups.error,
|
||||
);
|
||||
|
||||
//
|
||||
// Common functions
|
||||
//
|
||||
const getAll = () => {
|
||||
tab.value === UsersPageTabs.USERS ? $users.getAll() : $groups.getAll();
|
||||
};
|
||||
const clearSearch = () => {
|
||||
tab.value === UsersPageTabs.USERS
|
||||
? ($users.search = '')
|
||||
: ($groups.search = '');
|
||||
};
|
||||
const resetActions = () => {
|
||||
$users.resetGroupId();
|
||||
$users.resetSelected();
|
||||
$groups.resetSelected();
|
||||
};
|
||||
const deleteMany = () => {
|
||||
const len =
|
||||
tab.value === UsersPageTabs.USERS
|
||||
? $users.selected.length
|
||||
: $groups.selected.length;
|
||||
$confirmation.openModal();
|
||||
$confirmation.setTitle(
|
||||
t(
|
||||
`$vuetify.${tab.value === UsersPageTabs.USERS ? 'users' : 'groups'}.delete.delete-many.title`,
|
||||
),
|
||||
);
|
||||
$confirmation.setMessage(
|
||||
t(
|
||||
`$vuetify.${tab.value === UsersPageTabs.USERS ? 'users' : 'groups'}.delete.delete-many.message`,
|
||||
[len],
|
||||
),
|
||||
);
|
||||
$confirmation.setOnConfirm(
|
||||
tab.value === UsersPageTabs.USERS
|
||||
? () => {
|
||||
$users.deleteMany($users.selected);
|
||||
$users.resetSelected();
|
||||
}
|
||||
: () => {
|
||||
$groups.deleteMany($groups.selected);
|
||||
$groups.resetSelected();
|
||||
},
|
||||
);
|
||||
};
|
||||
const blockMany = () => {
|
||||
$users.blockMany($users.selected);
|
||||
$users.resetSelected();
|
||||
};
|
||||
const unblockMany = () => {
|
||||
$users.unblockMany($users.selected);
|
||||
$users.resetSelected();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<v-app-bar prominent class="elevation-0 border-b">
|
||||
<!-- Selected -->
|
||||
<div
|
||||
v-if="
|
||||
$users.groupId?.length > 0 ||
|
||||
$groups.selected?.length ||
|
||||
$users.selected?.length
|
||||
"
|
||||
class="flex items-center absolute h-full w-full left-0 top-0 table-actions z-[99]"
|
||||
>
|
||||
<!-- Clear -->
|
||||
<v-btn
|
||||
color="primary"
|
||||
icon
|
||||
size="small"
|
||||
class="ml-2 mr-3"
|
||||
@click="resetActions()"
|
||||
>
|
||||
<span class="mso text-3xl">close</span>
|
||||
</v-btn>
|
||||
<!-- Count -->
|
||||
<span class="text-h6 z-[100]">{{
|
||||
!$groups.selected?.length && !$users.selected?.length
|
||||
? t('$vuetify.users.selected-group', [$users.groupId])
|
||||
: t('$vuetify.misc.selected', [
|
||||
tab === UsersPageTabs.USERS
|
||||
? $users.selected?.length
|
||||
: $groups.selected?.length,
|
||||
])
|
||||
}}</span>
|
||||
<!-- Actions -->
|
||||
<!-- Delete -->
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
class="ml-10 mr-3 px-3"
|
||||
@click="deleteMany()"
|
||||
>
|
||||
<span class="mso mr-1 text-xl">{{
|
||||
tab === UsersPageTabs.USERS ? 'person_remove' : 'group_remove'
|
||||
}}</span>
|
||||
<span>{{ t('$vuetify.actions.delete') }}</span>
|
||||
</v-btn>
|
||||
<template v-if="tab === UsersPageTabs.USERS">
|
||||
<!-- Block -->
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
class="mr-3 px-3"
|
||||
@click="blockMany()"
|
||||
>
|
||||
<span class="mso mr-1 text-xl">person_cancel</span>
|
||||
<span>{{ t('$vuetify.actions.block') }}</span>
|
||||
</v-btn>
|
||||
<!-- Unblock -->
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
class="mr-3 px-3"
|
||||
@click="unblockMany()"
|
||||
>
|
||||
<span class="mso mr-1 text-xl">person_check</span>
|
||||
<span>{{ t('$vuetify.actions.unblock') }}</span>
|
||||
</v-btn>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<span class="text-h6 ml-5 mr-10">{{
|
||||
t(
|
||||
tab === UsersPageTabs.USERS
|
||||
? '$vuetify.users.title'
|
||||
: '$vuetify.users.tabs.groups',
|
||||
)
|
||||
}}</span>
|
||||
|
||||
<!-- Create group/user -->
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
class="mr-3 px-3"
|
||||
@click="openCreationModal()"
|
||||
>
|
||||
<span class="mso mr-1 text-xl">{{
|
||||
tab === UsersPageTabs.USERS ? 'person_add' : 'group_add'
|
||||
}}</span>
|
||||
<span>{{
|
||||
t(
|
||||
tab === UsersPageTabs.USERS
|
||||
? '$vuetify.actions.create-user'
|
||||
: '$vuetify.actions.create-group',
|
||||
)
|
||||
}}</span>
|
||||
</v-btn>
|
||||
|
||||
<!-- User creation modal -->
|
||||
<AddUser v-model="$users.createModal" />
|
||||
<!-- Group creation modal -->
|
||||
<AddGroup v-model="$groups.createModal" />
|
||||
|
||||
<!-- Refresh users -->
|
||||
<v-btn class="px-3" color="primary" variant="text" @click="getAll()">
|
||||
<span class="mso mr-1 text-xl">refresh</span>
|
||||
<span>{{ t('$vuetify.actions.refresh') }}</span>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- Tabs -->
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
color="primary"
|
||||
class="border-b"
|
||||
density="comfortable"
|
||||
bg-color="surface"
|
||||
:disabled="$users.groupId?.length > 0"
|
||||
>
|
||||
<v-tab :value="UsersPageTabs.USERS">{{
|
||||
t('$vuetify.users.tabs.users')
|
||||
}}</v-tab>
|
||||
<v-tab :value="UsersPageTabs.GROUPS">{{
|
||||
t('$vuetify.users.tabs.groups')
|
||||
}}</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<!-- Search -->
|
||||
<v-card-title class="p-0 w-full">
|
||||
<v-text-field
|
||||
v-model="$search"
|
||||
density="compact"
|
||||
:label="t('$vuetify.users.search')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="solo-filled"
|
||||
class="border-b"
|
||||
:rounded="false"
|
||||
flat
|
||||
clearable
|
||||
hide-details
|
||||
single-line
|
||||
@click:clear="clearSearch()"
|
||||
></v-text-field>
|
||||
</v-card-title>
|
||||
|
||||
<!-- Error -->
|
||||
<ErrorPlate :error="$error" block />
|
||||
|
||||
<div class="h-0" id="table"></div>
|
||||
|
||||
<v-tabs-window v-model="tab">
|
||||
<!-- Loader -->
|
||||
<transition name="fade">
|
||||
<table-loader v-if="$users.loading || $groups.loading" />
|
||||
</transition>
|
||||
|
||||
<!-- USERS -->
|
||||
<v-tabs-window-item :value="UsersPageTabs.USERS">
|
||||
<v-card flat rounded="0" :height="`${tableHeight}px`">
|
||||
<!-- Data -->
|
||||
<users-tab :tableHeight="tableHeight" />
|
||||
</v-card>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<!-- GROUPS -->
|
||||
<v-tabs-window-item :value="UsersPageTabs.GROUPS">
|
||||
<!-- Data -->
|
||||
<v-card flat rounded="0" :height="`${tableHeight}px`">
|
||||
<groups-tab :tableHeight="tableHeight" />
|
||||
</v-card>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
</template>
|
||||
3
src/plugins/README.md
Normal file
3
src/plugins/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Plugins
|
||||
|
||||
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.
|
||||
12
src/plugins/axios.ts
Normal file
12
src/plugins/axios.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import axios, { Axios } from 'axios';
|
||||
import type { App } from 'vue';
|
||||
|
||||
export const $axios: Axios = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
});
|
||||
|
||||
export default {
|
||||
install: (app: App) => {
|
||||
app.config.globalProperties.$axios = $axios;
|
||||
},
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user