first commit

This commit is contained in:
Gabriel Pastori
2023-10-30 10:31:12 -03:00
commit 0dcb482258
296 changed files with 3757 additions and 0 deletions

7
src/App.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<router-view />
</template>
<script setup>
//
</script>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

6
src/assets/logo.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1,75 @@
<template>
<v-container class="fill-height">
<v-responsive class="align-center text-center fill-height">
<v-img height="300" src="@/assets/logo.svg" />
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
<div class="py-14" />
<v-row class="d-flex align-center justify-center">
<v-col cols="auto">
<v-btn
href="https://vuetifyjs.com/components/all/"
min-width="164"
rel="noopener noreferrer"
target="_blank"
variant="text"
>
<v-icon
icon="mdi-view-dashboard"
size="large"
start
/>
Components
</v-btn>
</v-col>
<v-col cols="auto">
<v-btn
color="primary"
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
min-width="228"
rel="noopener noreferrer"
size="x-large"
target="_blank"
variant="flat"
>
<v-icon
icon="mdi-speedometer"
size="large"
start
/>
Get Started
</v-btn>
</v-col>
<v-col cols="auto">
<v-btn
href="https://community.vuetifyjs.com/"
min-width="164"
rel="noopener noreferrer"
target="_blank"
variant="text"
>
<v-icon
icon="mdi-account-group"
size="large"
start
/>
Community
</v-btn>
</v-col>
</v-row>
</v-responsive>
</v-container>
</template>
<script setup>
//
</script>

View File

@@ -0,0 +1,116 @@
<template>
<v-dialog
v-model="dialog"
max-width="500px"
:persistent="!AppStore.validConnection"
>
<v-card>
<v-card-text>
<v-form v-model="valid">
<h3 class="mb-4">Criar instancia</h3>
<v-text-field
v-model="instance.instanceName"
label="Nome"
required
outlined
:rules="[
// Verify is not have any caracter except letters, numbers, _ and -
(v) =>
new RegExp('^[a-zA-Z0-9_-]*$', 'i').test(v) ||
'Nome inválido (apenas letras, números, _ e -)',
]"
/>
<v-text-field
v-model="instance.apiKey"
label="API Key"
required
outlined
@click:prepend-inner="generateApiKey"
prepend-inner-icon="mdi-lock-reset"
:rules="[
// Verify is not have any caracter except letters, numbers, _ and -
(v) =>
new RegExp('^[a-zA-Z0-9_-]*$', 'i').test(v) ||
'Nome inválido (apenas letras, números, _ e -)',
]"
/>
</v-form>
<v-alert type="error" v-if="error">
{{ Array.isArray(error) ? error.join(", ") : error }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
v-if="AppStore.validConnection"
text
@click="dialog = false"
:disabled="loading"
>
Cancel
</v-btn>
<v-btn
color="success"
variant="tonal"
@click="save"
:disabled="!valid"
:loading="loading"
>
Conectar
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import instanceController from "@/services/instanceController";
import { useAppStore } from "@/store/app";
export default {
name: "SettingsModal",
data: () => ({
dialog: false,
valid: false,
instance: {
instanceName: "",
apiKey: "",
},
loading: false,
error: false,
AppStore: useAppStore(),
}),
methods: {
generateApiKey() {
this.instance.apiKey =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
},
async save() {
try {
this.loading = true;
this.error = false;
const instance = await instanceController.create(this.instance);
this.$router.push({
name: "instance",
params: { id: instance.instance.instanceName },
});
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
open() {
this.dialog = true;
this.generateApiKey();
},
},
emits: ["close"],
};
</script>

View File

@@ -0,0 +1,89 @@
<template>
<v-dialog v-model="dialog" max-width="500px" :persistent="!AppStore.validConnection">
<v-card>
<v-card-text>
<v-form v-model="valid">
<h3 class="mb-4">Configurar conexão</h3>
<v-text-field
v-model="connection.host"
label="URL"
required
outlined
:rules="[
// regex to verify is has http or https
(v) =>
new RegExp('^(http|https)://', 'i').test(v) || 'URL inválida',
]"
/>
<v-text-field
v-model="connection.globalApiKey"
label="Global API Key"
required
outlined
:type="revelPassword ? 'text' : 'password'"
:append-inner-icon="revelPassword ? 'mdi-eye' : 'mdi-eye-off'"
@click:append-inner="revelPassword = !revelPassword"
/>
</v-form>
<v-alert type="error" v-if="error">
{{ Array.isArray(error) ? error.join(", ") : error }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn v-if="AppStore.validConnection" text @click="dialog = false" :disabled="loading">Cancel</v-btn>
<v-btn
color="success"
variant="tonal"
@click="save"
:disabled="!valid"
:loading="loading"
>
Conectar
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { useAppStore } from "@/store/app";
export default {
name: "SettingsModal",
data: () => ({
dialog: false,
valid: false,
revelPassword: false,
connection: {
host: "",
globalApiKey: "",
},
loading: false,
error: false,
AppStore: useAppStore(),
}),
methods: {
async save() {
try {
this.loading = true;
this.error = false;
await this.AppStore.setConnection(this.connection);
this.dialog = false;
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
open() {
this.dialog = true;
this.connection = this.AppStore.connection;
},
},
emits: ["close"],
};
</script>

View File

@@ -0,0 +1,12 @@
export default {
close: {
color: "red",
text: "Desconectado",
icon: "mdi-close-circle",
},
open: {
color: "green",
text: "Conectado",
icon: "mdi-check-circle",
},
}

23
src/http-common.js Normal file
View File

@@ -0,0 +1,23 @@
import axios from "axios";
import { useAppStore } from "@/store/app";
const appStore = useAppStore();
appStore
const http = axios.create({
headers: {
"Content-type": "application/json"
}
});
http.interceptors.request.use(
config => {
config.baseURL = appStore.connection.host;
config.headers["apikey"] = appStore.connection.globalApiKey;
return config;
},
error => Promise.reject(error)
);
export default http;

View File

@@ -0,0 +1,40 @@
<template>
<v-app-bar flat>
<v-app-bar-title>
<v-icon icon="mdi-whatsapp" left />
Evolution Manager
</v-app-bar-title>
<v-icon v-if="AppStore.validConnection" color="success">
mdi-check-circle
</v-icon>
<v-icon v-else color="error"> mdi-alert-circle </v-icon>
<v-btn @click="openSettings" icon>
<v-icon>mdi-cog</v-icon>
</v-btn>
</v-app-bar>
<SettingsModal ref="settings" />
</template>
<script>
import SettingsModal from "@/components/modal/Settings.vue";
import { useAppStore } from "@/store/app";
export default {
name: "AppBar",
data: () => ({
AppStore: useAppStore(),
}),
components: {
SettingsModal,
},
methods: {
openSettings() {
this.$refs.settings.open();
},
},
mounted() {
if (!this.AppValidConnection) this.openSettings();
},
};
</script>

View File

@@ -0,0 +1,12 @@
<template>
<v-app>
<default-bar />
<default-view />
</v-app>
</template>
<script setup>
import DefaultBar from './AppBar.vue'
import DefaultView from './View.vue'
</script>

View File

@@ -0,0 +1,11 @@
<template>
<v-main>
<v-container>
<router-view />
</v-container>
</v-main>
</template>
<script setup>
//
</script>

20
src/main.js Normal file
View File

@@ -0,0 +1,20 @@
/**
* main.js
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Components
import App from './App.vue'
// Composables
import { createApp } from 'vue'
// Plugins
import { registerPlugins } from '@/plugins'
const app = createApp(App)
registerPlugins(app)
app.mount('#app')

17
src/plugins/index.js Normal file
View File

@@ -0,0 +1,17 @@
/**
* plugins/index.js
*
* Automatically included in `./src/main.js`
*/
// Plugins
import vuetify from './vuetify'
import pinia from '../store'
import router from '../router'
export function registerPlugins (app) {
app
.use(vuetify)
.use(router)
.use(pinia)
}

39
src/plugins/vuetify.js Normal file
View File

@@ -0,0 +1,39 @@
/**
* plugins/vuetify.js
*
* Framework documentation: https://vuetifyjs.com`
*/
// Styles
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
// Composables
import { createVuetify } from 'vuetify'
// Labs features
import {
VDataTable,
VDataTableServer,
VDataTableVirtual,
} from "vuetify/labs/VDataTable";
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
components: {
VDataTable,
VDataTableServer,
VDataTableVirtual,
},
theme: {
themes: {
light: {
colors: {
primary: '#1867C0',
secondary: '#5CBBF6',
},
},
},
},
})

29
src/router/index.js Normal file
View File

@@ -0,0 +1,29 @@
// Composables
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
name: 'instances',
component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'),
},
{
path: ':id',
name: 'instance',
component: () => import('@/views/Instance.vue'),
}
],
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
})
export default router

View File

@@ -0,0 +1,26 @@
import http from "../http-common";
const fetchAll = async () => {
return await http
.get("/instance/fetchInstances")
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
};
const create = async (data) => {
return await http
.post("/instance/create", data)
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
export default {
fetchAll,
create
};

65
src/store/app.js Normal file
View File

@@ -0,0 +1,65 @@
// Utilities
import axios from 'axios'
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
getters: {
validConnection: (state) => state.connection.valid,
instances: (state) => state.instancesList,
},
state: () => ({
connection: {
valid: false,
host: null,
globalApiKey: null,
},
instancesList: [],
}),
actions: {
async setConnection({ host, globalApiKey }) {
try {
const responde = await axios({
method: 'GET',
baseURL: host,
headers: {
'Content-Type': 'application/json',
'apikey': globalApiKey
},
url: '/instance/fetchInstances'
})
this.connection.valid = true
this.connection.host = host
this.connection.globalApiKey = globalApiKey
this.instancesList = responde.data
} catch (e) {
this.connection.valid = false
throw e.response?.data?.response?.message || e.response || e
}
},
async reconnect() {
try {
const { host, globalApiKey } = this.connection
const responde = await axios({
method: 'GET',
baseURL: host,
headers: {
'Content-Type': 'application/json',
'apikey': globalApiKey
},
url: '/instance/fetchInstances'
})
this.connection.valid = true
this.connection.host = host
this.connection.globalApiKey = globalApiKey
this.instancesList = responde.data
} catch (e) {
this.connection.valid = false
throw e.response?.data?.response?.message || e.response || e
}
}
}
})

4
src/store/index.js Normal file
View File

@@ -0,0 +1,4 @@
// Utilities
import { createPinia } from 'pinia'
export default createPinia()

10
src/styles/settings.scss Normal file
View File

@@ -0,0 +1,10 @@
/**
* src/styles/settings.scss
*
* Configures SASS variables and Vuetify overwrites
*/
// https://vuetifyjs.com/features/sass-variables/`
// @use 'vuetify/settings' with (
// $color-pack: false
// );

141
src/views/Home.vue Normal file
View File

@@ -0,0 +1,141 @@
<template>
<div>
<div class="d-flex mb-4 align-center">
<h3>Instancias</h3>
<v-spacer></v-spacer>
<v-btn
:disabled="loading"
@click="getInstances"
icon
variant="text"
size="small"
class="mr-2"
>
<v-icon>mdi-refresh</v-icon>
</v-btn>
<v-btn
color="primary"
variant="tonal"
@click="addInstance"
:disabled="loading"
>
<v-icon>mdi-plus</v-icon>
<span class="ml-2">Instancia</span>
</v-btn>
</div>
<v-card v-if="!loading && instances?.length === 0" variant="outlined">
<v-card-text>
<div class="text-center">
<v-icon size="70">mdi-server-network-off</v-icon>
<h3 class="mt-4">Nenhuma instância encontrada</h3>
</div>
</v-card-text>
</v-card>
<v-data-table
v-if="!instances || instances?.length > 0"
:headers="headers"
:items="instances || []"
:loading="loading"
:items-per-page="10"
>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template v-slot:item.instance.status="{ item }">
<v-chip
:color="statusMapper[item.instance.status].color"
:text-color="statusMapper[item.instance.status].textColor"
size="small"
label
>
<v-icon
v-if="statusMapper[item.instance.status].icon"
start
size="small"
>
{{ statusMapper[item.instance.status].icon }}
</v-icon>
{{ statusMapper[item.instance.status].text }}
</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
:disabled="loading"
:to="`/${item.instance.instanceName}`"
icon
variant="text"
size="small"
class="mr-2"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<!-- <v-btn
:disabled="loading"
@click="AppStore.selectInstance(item.instance)"
icon
variant="text"
size="small"
>
<v-icon>mdi-delete</v-icon>
</v-btn> -->
</template>
</v-data-table>
<v-alert v-if="error" type="error">
{{ error }}
</v-alert>
</div>
<CreateInstance ref="createInstanceModal" />
</template>
<script>
import { useAppStore } from "@/store/app";
import CreateInstance from "@/components/modal/CreateInstance";
import statusMapper from "@/helpers/mappers/status";
export default {
name: "HomeInstance",
components: {
CreateInstance,
},
data: () => ({
AppStore: useAppStore(),
loading: false,
error: false,
statusMapper: statusMapper,
headers: [
{
title: "Nome",
align: "start",
sortable: true,
key: "instance.instanceName",
},
{ title: "Status", key: "instance.status" },
{ title: "Ações", key: "actions", sortable: false , align: "center"},
],
}),
methods: {
addInstance() {
this.$refs.createInstanceModal.open();
},
async getInstances() {
try {
this.loading = true;
this.instances = await this.AppStore.reconnect();
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
},
watch: {},
computed: {
instances() {
return this.AppStore.instances;
},
},
mounted() {
},
};
</script>

52
src/views/Instance.vue Normal file
View File

@@ -0,0 +1,52 @@
<template>
<v-alert v-if="error" type="error">
{{ error }}
</v-alert>
<div v-else-if="instance">
<v-card variant="outlined" class="d-flex align-center">
<v-avatar size="100">
<v-icon v-if="statusMapper[instance.instance.status].icon" size="70">
{{ statusMapper[instance.instance.status].icon }}
</v-icon>
</v-avatar>
<div>
<h2>{{ instance.instance.instanceName }}</h2>
</div>
</v-card>
</div>
</template>
<script>
import { useAppStore } from "@/store/app";
import statusMapper from "@/helpers/mappers/status";
export default {
name: "HomeInstance",
data: () => ({
AppStore: useAppStore(),
loading: true,
error: false,
instance: null,
statusMapper: statusMapper,
}),
methods: {
async loadInstance() {
if (!this.AppStore.instances) await this.AppStore.reconnect();
const instances = this.AppStore.instances;
const instance = instances.find(
(instance) => instance.instance.instanceName === this.$route.params.id
);
if (!instance) {
this.error = "Instância não encontrada";
return;
}
this.instance = instance;
},
},
watch: {},
async mounted() {
if (this.AppStore.validConnection) this.loadInstance();
else this.$router.push({ name: "instances" });
},
};
</script>