mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-16 04:02:45 +01:00
Add Dashboards (#41)
This commit is contained in:
@@ -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>
|
||||
|
||||
64
ui/src/views/DashboardList.vue
Normal file
64
ui/src/views/DashboardList.vue
Normal 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
219
ui/src/views/Home.vue
Normal 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>
|
||||
Reference in New Issue
Block a user