Add Dashboards (#41)

This commit is contained in:
Jonas Plum
2022-03-14 00:23:29 +01:00
committed by GitHub
parent 18a4dc54e7
commit 02c7da91da
30 changed files with 2824 additions and 279 deletions

View File

@@ -2,7 +2,7 @@
<v-app class="background">
<v-navigation-drawer dark permanent :mini-variant="mini" :expand-on-hover="mini" app color="statusbar">
<v-list>
<v-list-item class="px-2" :to="{ name: 'Dashboard' }">
<v-list-item class="px-2" :to="{ name: 'Home' }">
<v-list-item-avatar rounded="0">
<v-img src="/flask_white.svg" :width="40"></v-img>
</v-list-item-avatar>
@@ -182,6 +182,7 @@ export default Vue.extend({
},
internal: function (): Array<any> {
return [
{ icon: "mdi-view-dashboard", name: "Dashboards", to: "DashboardList", role: "admin:dashboard:write" },
{ icon: "mdi-check-bold", name: "Open Tasks", to: "TaskList", count: this.$store.state.task_count },
]
},

View File

@@ -290,6 +290,50 @@ export interface Context {
*/
'ticket'?: TicketResponse;
}
/**
*
* @export
* @interface Dashboard
*/
export interface Dashboard {
/**
*
* @type {string}
* @memberof Dashboard
*/
'name': string;
/**
*
* @type {Array<Widget>}
* @memberof Dashboard
*/
'widgets': Array<Widget>;
}
/**
*
* @export
* @interface DashboardResponse
*/
export interface DashboardResponse {
/**
*
* @type {string}
* @memberof DashboardResponse
*/
'id': string;
/**
*
* @type {string}
* @memberof DashboardResponse
*/
'name': string;
/**
*
* @type {Array<Widget>}
* @memberof DashboardResponse
*/
'widgets': Array<Widget>;
}
/**
*
* @export
@@ -2279,6 +2323,52 @@ export interface UserResponse {
*/
'roles': Array<string>;
}
/**
*
* @export
* @interface Widget
*/
export interface Widget {
/**
*
* @type {string}
* @memberof Widget
*/
'aggregation': string;
/**
*
* @type {string}
* @memberof Widget
*/
'filter'?: string;
/**
*
* @type {string}
* @memberof Widget
*/
'name': string;
/**
*
* @type {string}
* @memberof Widget
*/
'type': WidgetTypeEnum;
/**
*
* @type {number}
* @memberof Widget
*/
'width': number;
}
export const WidgetTypeEnum = {
Bar: 'bar',
Line: 'line',
Pie: 'pie'
} as const;
export type WidgetTypeEnum = typeof WidgetTypeEnum[keyof typeof WidgetTypeEnum];
/**
* AutomationsApi - axios parameter creator
@@ -2657,6 +2747,461 @@ export class AutomationsApi extends BaseAPI {
}
/**
* DashboardsApi - axios parameter creator
* @export
*/
export const DashboardsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @summary Create a new dashboard
* @param {Dashboard} template New template
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createDashboard: async (template: Dashboard, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'template' is not null or undefined
assertParamExists('createDashboard', 'template', template)
const localVarPath = `/dashboards`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(template, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Get widget data
* @param {string} aggregation Aggregation
* @param {string} [filter] Filter
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
dashboardData: async (aggregation: string, filter?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'aggregation' is not null or undefined
assertParamExists('dashboardData', 'aggregation', aggregation)
const localVarPath = `/dashboard/data`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
if (aggregation !== undefined) {
localVarQueryParameter['aggregation'] = aggregation;
}
if (filter !== undefined) {
localVarQueryParameter['filter'] = filter;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Delete a dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteDashboard: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('deleteDashboard', 'id', id)
const localVarPath = `/dashboards/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Get a single dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getDashboard: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getDashboard', 'id', id)
const localVarPath = `/dashboards/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary List dashboards
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listDashboards: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/dashboards`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Update an existing dashboard
* @param {string} id Dashboard ID
* @param {Dashboard} dashboard Dashboard object that needs to be added
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateDashboard: async (id: string, dashboard: Dashboard, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updateDashboard', 'id', id)
// verify required parameter 'dashboard' is not null or undefined
assertParamExists('updateDashboard', 'dashboard', dashboard)
const localVarPath = `/dashboards/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(dashboard, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* DashboardsApi - functional programming interface
* @export
*/
export const DashboardsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = DashboardsApiAxiosParamCreator(configuration)
return {
/**
*
* @summary Create a new dashboard
* @param {Dashboard} template New template
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createDashboard(template: Dashboard, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DashboardResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createDashboard(template, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Get widget data
* @param {string} aggregation Aggregation
* @param {string} [filter] Filter
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async dashboardData(aggregation: string, filter?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.dashboardData(aggregation, filter, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Delete a dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteDashboard(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteDashboard(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Get a single dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getDashboard(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DashboardResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getDashboard(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary List dashboards
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listDashboards(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<DashboardResponse>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listDashboards(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @summary Update an existing dashboard
* @param {string} id Dashboard ID
* @param {Dashboard} dashboard Dashboard object that needs to be added
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateDashboard(id: string, dashboard: Dashboard, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DashboardResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateDashboard(id, dashboard, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* DashboardsApi - factory interface
* @export
*/
export const DashboardsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = DashboardsApiFp(configuration)
return {
/**
*
* @summary Create a new dashboard
* @param {Dashboard} template New template
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createDashboard(template: Dashboard, options?: any): AxiosPromise<DashboardResponse> {
return localVarFp.createDashboard(template, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Get widget data
* @param {string} aggregation Aggregation
* @param {string} [filter] Filter
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
dashboardData(aggregation: string, filter?: string, options?: any): AxiosPromise<object> {
return localVarFp.dashboardData(aggregation, filter, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Delete a dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteDashboard(id: string, options?: any): AxiosPromise<void> {
return localVarFp.deleteDashboard(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Get a single dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getDashboard(id: string, options?: any): AxiosPromise<DashboardResponse> {
return localVarFp.getDashboard(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary List dashboards
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listDashboards(options?: any): AxiosPromise<Array<DashboardResponse>> {
return localVarFp.listDashboards(options).then((request) => request(axios, basePath));
},
/**
*
* @summary Update an existing dashboard
* @param {string} id Dashboard ID
* @param {Dashboard} dashboard Dashboard object that needs to be added
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateDashboard(id: string, dashboard: Dashboard, options?: any): AxiosPromise<DashboardResponse> {
return localVarFp.updateDashboard(id, dashboard, options).then((request) => request(axios, basePath));
},
};
};
/**
* DashboardsApi - object-oriented interface
* @export
* @class DashboardsApi
* @extends {BaseAPI}
*/
export class DashboardsApi extends BaseAPI {
/**
*
* @summary Create a new dashboard
* @param {Dashboard} template New template
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public createDashboard(template: Dashboard, options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).createDashboard(template, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Get widget data
* @param {string} aggregation Aggregation
* @param {string} [filter] Filter
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public dashboardData(aggregation: string, filter?: string, options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).dashboardData(aggregation, filter, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Delete a dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public deleteDashboard(id: string, options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).deleteDashboard(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Get a single dashboard
* @param {string} id Dashboard ID
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public getDashboard(id: string, options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).getDashboard(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary List dashboards
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public listDashboards(options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).listDashboards(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Update an existing dashboard
* @param {string} id Dashboard ID
* @param {Dashboard} dashboard Dashboard object that needs to be added
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DashboardsApi
*/
public updateDashboard(id: string, dashboard: Dashboard, options?: AxiosRequestConfig) {
return DashboardsApiFp(this.configuration).updateDashboard(id, dashboard, options).then((request) => request(this.axios, this.basePath));
}
}
/**
* GraphApi - axios parameter creator
* @export

View File

@@ -68,19 +68,26 @@ const v = new Vue({
}).$mount("#app");
axios.interceptors.response.use(
response => response,
// response => response,
response => {
lodash.unset(response.data, 'notoast');
return Promise.resolve(response);
},
error => {
console.log(error)
if (error.response.data && 'title' in error.response.data && 'detail' in error.response.data) {
const problem = error.response.data as Problem;
v.$store.dispatch("alertError", { name: problem.title, detail: problem.detail });
return Promise.reject(error);
if (!lodash.has(error.response.data, 'notoast')) {
if (error.response.data && 'title' in error.response.data && 'detail' in error.response.data) {
const problem = error.response.data as Problem;
v.$store.dispatch("alertError", { name: problem.title, detail: problem.detail });
return Promise.reject(error);
}
if (error.response.data && 'error' in error.response.data) {
v.$store.dispatch("alertError", { name: "Error", detail: error.response.data.error });
return Promise.reject(error);
}
v.$store.dispatch("alertError", { name: "Error", detail: JSON.stringify(error.response.data) });
}
if (error.response.data && 'error' in error.response.data) {
v.$store.dispatch("alertError", { name: "Error", detail: error.response.data.error });
return Promise.reject(error);
}
v.$store.dispatch("alertError", { name: "Error", detail: JSON.stringify(error.response.data) });
return Promise.reject(error);
}
);

View File

@@ -15,13 +15,15 @@ import Rule from "../views/Rule.vue";
import RuleList from "../views/RuleList.vue";
import Template from "../views/Template.vue";
import TemplateList from "../views/TemplateList.vue";
import Dashboard from "../views/Dashboard.vue";
import DashboardList from "../views/DashboardList.vue";
import API from "../views/API.vue";
import User from '../views/User.vue';
import UserList from "@/views/UserList.vue";
import Job from '../views/Job.vue';
import JobList from "@/views/JobList.vue";
import GroupList from "@/views/GroupList.vue";
import Dashboard from "@/views/Dashboard.vue";
import Home from "@/views/Home.vue";
import Group from "@/views/Group.vue";
import TicketType from '../views/TicketType.vue';
import TicketTypeList from "@/views/TicketTypeList.vue";
@@ -59,10 +61,10 @@ const routes: Array<RouteConfig> = [
},
{
path: "/dashboard",
name: "Dashboard",
component: Dashboard,
meta: { title: "Dashboard" },
path: "/home",
name: "Home",
component: Home,
meta: { title: "Home" },
},
{
@@ -227,6 +229,21 @@ const routes: Array<RouteConfig> = [
]
},
{
path: "/dashboards",
name: "DashboardList",
component: DashboardList,
meta: { title: "Dashboards" },
children: [
{
path: ":id",
name: "Dashboard",
component: Dashboard,
},
]
},
{
path: "/settings",
name: "Settings",

View File

@@ -29,7 +29,7 @@ import {
SettingsApi,
SettingsApiFactory,
JobsApi,
JobsApiFactory,
JobsApiFactory, DashboardsApiFactory, DashboardsApi,
} from "@/client";
const config = new Configuration({
@@ -56,7 +56,8 @@ export const API: TicketsApi &
SettingsApi &
TickettypesApi &
JobsApi &
TasksApi = Object.assign(
TasksApi &
DashboardsApi = Object.assign(
{},
TicketsApiFactory(config),
PlaybooksApiFactory(config),
@@ -74,5 +75,6 @@ export const API: TicketsApi &
TickettypesApiFactory(config),
TasksApiFactory(config),
SettingsApiFactory(config),
JobsApiFactory(config)
JobsApiFactory(config),
DashboardsApiFactory(config)
);

View File

@@ -1,7 +1,7 @@
import Vue from "vue";
import Vuex, {ActionContext} from "vuex";
import {API} from "@/services/api";
import {UserData, TicketList, Settings, UserResponse, SettingsResponse} from "@/client";
import {UserData, TicketList, UserResponse, SettingsResponse} from "@/client";
import {AxiosResponse} from "axios";
import {Alert} from "@/types/types";
import {templateStore} from "./modules/templates";
@@ -19,7 +19,7 @@ export default new Vuex.Store({
counts: {} as Record<string, number>,
task_count: 0 as number,
settings: {} as Settings,
settings: {} as SettingsResponse,
userdata: {} as UserData,
alert: {} as Alert,
@@ -46,7 +46,7 @@ export default new Vuex.Store({
setUserData (state, msg: UserData) {
state.userdata = msg
},
setSettings (state, msg: Settings) {
setSettings (state, msg: SettingsResponse) {
state.settings = msg
},
setAlert (state, msg: Alert) {

View File

@@ -1,110 +1,123 @@
<template>
<v-main>
<div v-if="dashboard">
<h2 class="d-flex">
<span v-if="!editmode">{{ dashboard.name }}</span>
<v-text-field v-else v-model="dashboard.name" outlined dense class="mb-0" hide-details></v-text-field>
<v-spacer></v-spacer>
<v-btn v-if="editmode" small outlined @click="addWidget" class="mr-1">
<v-icon>mdi-plus</v-icon>
Add Widget
</v-btn>
<v-btn v-if="editmode" small outlined @click="save" class="mr-1">
<v-icon>mdi-content-save</v-icon>
Save
</v-btn>
<v-btn v-if="editmode && $route.params.id !== 'new'" small outlined @click="cancel">
<v-icon>mdi-cancel</v-icon>
Cancel
</v-btn>
<v-btn v-if="!editmode" small outlined @click="edit">
<v-icon>mdi-pencil</v-icon>
Edit
</v-btn>
</h2>
<v-row>
<v-col v-if="statistics" cols="12" lg="7">
<v-row>
<v-col cols="4">
<v-subheader>Unassigned tickets</v-subheader>
<span style="font-size: 60pt; text-align: center; display: block">
<router-link :to="{
name: 'TicketList',
params: { query: 'status == \'open\' AND !owner' }
}">
{{ statistics.unassigned }}
</router-link>
</span>
<v-subheader>Your tickets</v-subheader>
<span style="font-size: 60pt; text-align: center; display: block">
<router-link :to="{
name: 'TicketList',
params: { query: 'status == \'open\' AND owner == \'' + $store.state.user.id + '\'' }
}">
{{ $store.state.user.id in statistics.open_tickets_per_user ? statistics.open_tickets_per_user[$store.state.user.id] : 0 }}
</router-link>
</span>
</v-col>
<v-col cols="8">
<v-subheader>Open tickets per owner</v-subheader>
<bar-chart
v-if="open_tickets_per_user"
:chart-data="open_tickets_per_user"
:styles="{
width: '100%',
'max-height': '400px',
position: 'relative'
}"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: undefined,
scales: { xAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] },
onClick: clickUser,
hover: {
onHover: function(e) {
var point = this.getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
}"
></bar-chart>
</v-col>
</v-row>
<v-row>
<v-col cols="7">
<v-subheader>Tickets created per week</v-subheader>
<v-col v-for="(widget, index) in dashboard.widgets" :key="index" :cols="widget.width">
<v-card class="mb-2">
<v-card-title>
<span v-if="!editmode">{{ widget.name }}</span>
<v-text-field v-else outlined dense hide-details v-model="widget.name" class="mr-1"></v-text-field>
<v-btn v-if="editmode" outlined @click="removeWidget(index)">
<v-icon>mdi-close</v-icon>
Remove
</v-btn>
</v-card-title>
<v-card-text v-if="editmode">
<v-row>
<v-col cols="8">
<v-select label="Type" v-model="widget.type" :items="['line', 'bar', 'pie']"></v-select>
</v-col>
<v-col cols="4">
<v-text-field label="Width" type="number" v-model="widget.width"></v-text-field>
</v-col>
</v-row>
<v-text-field label="Aggregation" v-model="widget.aggregation"></v-text-field>
<v-text-field label="Filter" v-model="widget.filter" clearable></v-text-field>
</v-card-text>
<v-card-text v-if="data[index] === null">
{{ widgetErrors[index] }}
</v-card-text>
<div v-else>
<line-chart
v-if="tickets_per_week"
:chart-data="tickets_per_week"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: undefined,
scales: { yAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] }
}"
v-if="widget.type === 'line' && data[index]"
:chart-data="data[index]"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: false,
scales: { yAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] }
}"
>
</line-chart>
</v-col>
<v-col cols="5">
<v-subheader>Ticket Types</v-subheader>
<pie-chart
v-if="tickets_per_type"
:chart-data="tickets_per_type"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
onClick: clickPie,
hover: {
onHover: function(e) {
var point = this.getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
}"
v-if="widget.type === 'pie' && data[index]"
:chart-data="data[index]"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
}"
>
</pie-chart>
</v-col>
</v-row>
</v-col>
<v-col cols="12" lg="5">
<TicketList :type="this.$route.params.type" @click="open"></TicketList>
<bar-chart
v-if="widget.type === 'bar' && data[index]"
:chart-data="data[index]"
:styles="{
width: '100%',
'max-height': '400px',
position: 'relative'
}"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: false,
scales: { xAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] },
}"
></bar-chart>
</div>
</v-card>
</v-col>
</v-row>
</v-main>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import {DashboardResponse, Widget} from "@/client";
import { API } from "@/services/api";
import {createHash} from "crypto";
import {colors} from "@/plugins/vuetify";
import LineChart from "../components/charts/Line";
import BarChart from "../components/charts/Bar";
import PieChart from "../components/charts/Doughnut";
import { API } from "@/services/api";
import {Statistics, TicketResponse} from "@/client";
import {DateTime} from "luxon";
import { colors } from "@/plugins/vuetify";
import TicketList from "@/components/TicketList.vue";
import { createHash } from "crypto";
import {ChartData} from "chart.js";
import {AxiosError, AxiosTransformer} from "axios";
interface State {
dashboard?: DashboardResponse;
undodashboard?: DashboardResponse;
data: Record<string, any>;
editmode: boolean;
widgetErrors: Record<number, string>;
}
export default Vue.extend({
name: "Dashboard",
@@ -112,108 +125,130 @@ export default Vue.extend({
LineChart,
BarChart,
PieChart,
TicketList
},
data() {
return {
statistics: (undefined as unknown) as Statistics
};
},
computed: {
tickets_per_type: function () {
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
this.lodash.forEach(this.statistics.tickets_per_type, (count, type) => {
data.labels.push(type);
data.datasets[0].data.push(count);
data.datasets[0].backgroundColor.push(this.color(type));
})
return data
data: (): State => ({
dashboard: undefined,
undodashboard: undefined,
data: {},
editmode: false,
widgetErrors: {},
}),
watch: {
$route: function () {
this.loadDashboard();
},
open_tickets_per_user: function () {
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
this.lodash.forEach(this.statistics.open_tickets_per_user, (count, user) => {
if (!user) {
data.labels.push("unassigned");
} else {
data.labels.push(user);
}
data.datasets[0].data.push(count);
data.datasets[0].backgroundColor.push(this.color(user));
})
return data
},
tickets_per_week: function () {
let data = {labels: [] as Array<string>, datasets: [{backgroundColor: [] as Array<string>, data: [] as Array<number> }]}
this.lodash.forEach(this.weeks(), (week) => {
data.labels.push(week);
if (week in this.statistics.tickets_per_week) {
data.datasets[0].data.push(this.statistics.tickets_per_week[week]);
} else {
data.datasets[0].data.push(0);
}
data.datasets[0].backgroundColor.push("#607d8b");
})
return data
}
},
methods: {
open: function (ticket: TicketResponse) {
if (ticket.id === undefined) {
return;
edit: function () {
this.undodashboard = this.lodash.cloneDeep(this.dashboard);
this.editmode = true;
},
save: function () {
if (!this.dashboard) {
return
}
this.$router.push({
name: "Ticket",
params: {type: '-', id: ticket.id.toString()}
});
},
clickUser: function (evt, elem) {
let owner = this.open_tickets_per_user.labels[elem[0]._index];
let query = 'status == \'open\' AND owner == \'' + owner + '\'';
let widgets = [] as Array<Widget>;
this.lodash.forEach(this.dashboard.widgets, (widget) => {
widget.width = this.lodash.toInteger(widget.width);
if (!widget.filter) {
this.lodash.unset(widget, "filter")
}
widgets.push(widget);
})
this.dashboard.widgets = widgets;
if (owner == 'unassigned') {
query = 'status == \'open\' AND !owner';
if (this.$route.params.id === 'new') {
API.createDashboard(this.dashboard).then((response) => {
this.loadWidgetData(response.data.widgets);
this.dashboard = response.data;
this.editmode = false;
this.$router.push({ name: "Dashboard", params: { id: response.data.id }})
})
} else {
API.updateDashboard(this.dashboard.id, this.dashboard).then((response) => {
this.loadWidgetData(response.data.widgets);
this.dashboard = response.data;
this.editmode = false;
})
}
},
cancel: function () {
this.dashboard = this.lodash.cloneDeep(this.undodashboard);
this.editmode = false;
},
addWidget: function () {
if (!this.dashboard) {
return
}
this.$router.push({
name: "TicketList",
params: {query: query}
});
this.dashboard.widgets.push({name: "new widget", width: 6, aggregation: "", type: "line"})
},
clickPie: function (evt, elem) {
this.$router.push({
name: "TicketList",
params: {type: this.tickets_per_type.labels[elem[0]._index]}
});
removeWidget: function (id: number) {
if (!this.dashboard) {
return
}
console.log(id);
let widgets = this.lodash.cloneDeep(this.dashboard.widgets);
this.lodash.pullAt(widgets, [id]);
Vue.set(this.dashboard, "widgets", widgets);
},
loadDashboard: function () {
if (this.$route.params.id === 'new') {
this.dashboard = {
name: "New dashboard",
widgets: [{name: "new widget", width: 6, aggregation: "", type: "line"}],
} as DashboardResponse
this.editmode = true;
} else {
API.getDashboard(this.$route.params.id).then((response) => {
this.loadWidgetData(response.data.widgets);
this.dashboard = response.data;
});
}
},
loadWidgetData: function (widgets: Array<Widget>) {
this.lodash.forEach(widgets, (widget: Widget, index: number) => {
let widgetErrors = {};
let defaultTransformers = this.axios.defaults.transformResponse as AxiosTransformer[]
let transformResponse = defaultTransformers.concat((data) => {
data.notoast = true;
return data
});
API.dashboardData(widget.aggregation, widget.filter, {transformResponse: transformResponse}).then((response) => {
let d = { labels: [], datasets: [{data: [], backgroundColor: []}] } as ChartData;
this.lodash.forEach(response.data, (v: any, k: string) => {
// @ts-expect-error T2532
d.labels.push(k)
// @ts-expect-error T2532
d.datasets[0].data.push(v)
if (widget.type !== 'line') {
// @ts-expect-error T2532
d.datasets[0].backgroundColor.push(this.color(this.lodash.toString(v)));
}
})
Vue.set(this.data, index, d);
}).catch((err: AxiosError) => {
widgetErrors[index] = this.lodash.toString(err.response?.data.error);
Vue.set(this.data, index, null);
})
Vue.set(this, 'widgetErrors', widgetErrors);
})
},
color: function (s: string): string {
let pos = createHash('md5').update(s).digest().readUInt32BE(0) % colors.length;
return colors[pos];
},
fillData() {
API.getStatistics().then(response => {
this.statistics = response.data;
});
},
weeks: function () {
let w = [] as Array<string>;
for (let i = 0; i < 53; i++) {
w.push(DateTime.utc().minus({ weeks: i }).toFormat("kkkk-WW"))
}
this.lodash.reverse(w);
return w
}
},
mounted() {
this.fillData();
this.loadDashboard();
}
});
</script>
<style>
canvas {
position: relative !important;
margin: 0 auto;
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<v-main style="min-height: 100vh;">
<List
:items="dashboards"
routername="Dashboard"
itemid="id"
itemname="name"
singular="Dashboard"
plural="Dashboards"
writepermission="admin:dashboard:write"
@delete="deleteDashboard"
></List>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import {Dashboard} from "@/client";
import {API} from "@/services/api";
import List from "../components/List.vue";
interface State {
dashboards: Array<Dashboard>;
}
export default Vue.extend({
name: "DashboardList",
components: {List},
data: (): State => ({
dashboards: [],
}),
methods: {
loadDashboards() {
API.listDashboards().then((response) => {
this.dashboards = response.data;
});
},
deleteDashboard(id: string) {
API.deleteDashboard(id).then(() => {
this.loadDashboards();
})
}
},
mounted() {
this.loadDashboards();
this.$store.subscribeAction((action, state) => {
if (!action.payload || !(this.lodash.has(action.payload, "ids")) || !action.payload["ids"]) {
return
}
let reload = false;
Vue.lodash.forEach(action.payload["ids"], (id) => {
if (this.lodash.startsWith(id, "dashboard/")) {
reload = true;
}
});
if (reload) {
this.loadDashboards()
}
})
},
});
</script>

219
ui/src/views/Home.vue Normal file
View File

@@ -0,0 +1,219 @@
<template>
<v-main>
<v-row>
<v-col v-if="statistics" cols="12" lg="7">
<v-row>
<v-col cols="4">
<v-subheader>Unassigned tickets</v-subheader>
<span style="font-size: 60pt; text-align: center; display: block">
<router-link :to="{
name: 'TicketList',
params: { query: 'status == \'open\' AND !owner' }
}">
{{ statistics.unassigned }}
</router-link>
</span>
<v-subheader>Your tickets</v-subheader>
<span style="font-size: 60pt; text-align: center; display: block">
<router-link :to="{
name: 'TicketList',
params: { query: 'status == \'open\' AND owner == \'' + $store.state.user.id + '\'' }
}">
{{ $store.state.user.id in statistics.open_tickets_per_user ? statistics.open_tickets_per_user[$store.state.user.id] : 0 }}
</router-link>
</span>
</v-col>
<v-col cols="8">
<v-subheader>Open tickets per owner</v-subheader>
<bar-chart
v-if="open_tickets_per_user"
:chart-data="open_tickets_per_user"
:styles="{
width: '100%',
'max-height': '400px',
position: 'relative'
}"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: undefined,
scales: { xAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] },
onClick: clickUser,
hover: {
onHover: function(e) {
var point = this.getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
}"
></bar-chart>
</v-col>
</v-row>
<v-row>
<v-col cols="7">
<v-subheader>Tickets created per week</v-subheader>
<line-chart
v-if="tickets_per_week"
:chart-data="tickets_per_week"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
responsive: true,
maintainAspectRatio: false,
legend: undefined,
scales: { yAxes: [ { ticks: { beginAtZero: true, precision: 0 } } ] }
}"
>
</line-chart>
</v-col>
<v-col cols="5">
<v-subheader>Ticket Types</v-subheader>
<pie-chart
v-if="tickets_per_type"
:chart-data="tickets_per_type"
:styles="{ width: '100%', position: 'relative' }"
:chart-options="{
onClick: clickPie,
hover: {
onHover: function(e) {
var point = this.getElementAtEvent(e);
if (point.length) e.target.style.cursor = 'pointer';
else e.target.style.cursor = 'default';
}
}
}"
>
</pie-chart>
</v-col>
</v-row>
</v-col>
<v-col cols="12" lg="5">
<TicketList :type="this.$route.params.type" @click="open"></TicketList>
</v-col>
</v-row>
</v-main>
</template>
<script lang="ts">
import Vue from "vue";
import LineChart from "../components/charts/Line";
import BarChart from "../components/charts/Bar";
import PieChart from "../components/charts/Doughnut";
import { API } from "@/services/api";
import {Statistics, TicketResponse} from "@/client";
import {DateTime} from "luxon";
import { colors } from "@/plugins/vuetify";
import TicketList from "@/components/TicketList.vue";
import { createHash } from "crypto";
export default Vue.extend({
name: "Home",
components: {
LineChart,
BarChart,
PieChart,
TicketList
},
data() {
return {
statistics: (undefined as unknown) as Statistics
};
},
computed: {
tickets_per_type: function () {
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
this.lodash.forEach(this.statistics.tickets_per_type, (count, type) => {
data.labels.push(type);
data.datasets[0].data.push(count);
data.datasets[0].backgroundColor.push(this.color(type));
})
return data
},
open_tickets_per_user: function () {
let data = { labels: [] as Array<string>, datasets: [{ backgroundColor: [] as Array<string>, data: [] as Array<number> }] }
this.lodash.forEach(this.statistics.open_tickets_per_user, (count, user) => {
if (!user) {
data.labels.push("unassigned");
} else {
data.labels.push(user);
}
data.datasets[0].data.push(count);
data.datasets[0].backgroundColor.push(this.color(user));
})
return data
},
tickets_per_week: function () {
let data = {labels: [] as Array<string>, datasets: [{backgroundColor: [] as Array<string>, data: [] as Array<number> }]}
this.lodash.forEach(this.weeks(), (week) => {
data.labels.push(week);
if (week in this.statistics.tickets_per_week) {
data.datasets[0].data.push(this.statistics.tickets_per_week[week]);
} else {
data.datasets[0].data.push(0);
}
data.datasets[0].backgroundColor.push("#607d8b");
})
return data
}
},
methods: {
open: function (ticket: TicketResponse) {
if (ticket.id === undefined) {
return;
}
this.$router.push({
name: "Ticket",
params: {type: '-', id: ticket.id.toString()}
});
},
clickUser: function (evt, elem) {
let owner = this.open_tickets_per_user.labels[elem[0]._index];
let query = 'status == \'open\' AND owner == \'' + owner + '\'';
if (owner == 'unassigned') {
query = 'status == \'open\' AND !owner';
}
this.$router.push({
name: "TicketList",
params: {query: query}
});
},
clickPie: function (evt, elem) {
this.$router.push({
name: "TicketList",
params: {type: this.tickets_per_type.labels[elem[0]._index]}
});
},
color: function (s: string): string {
let pos = createHash('md5').update(s).digest().readUInt32BE(0) % colors.length;
return colors[pos];
},
fillData() {
API.getStatistics().then(response => {
this.statistics = response.data;
});
},
weeks: function () {
let w = [] as Array<string>;
for (let i = 0; i < 53; i++) {
w.push(DateTime.utc().minus({ weeks: i }).toFormat("kkkk-WW"))
}
this.lodash.reverse(w);
return w
}
},
mounted() {
this.fillData();
}
});
</script>
<style>
canvas {
position: relative !important;
margin: 0 auto;
}
</style>