186 lines
4.2 KiB
Vue
186 lines
4.2 KiB
Vue
<template>
|
|
<div class="stats-container" :class="containerClass">
|
|
<h2>
|
|
{{ title() }}
|
|
<b-icon :id="chartSettingsID()" icon="gear-fill"></b-icon>
|
|
</h2>
|
|
<div class="chart-container">
|
|
<canvas :id="chartID()"></canvas>
|
|
</div>
|
|
<b-popover
|
|
:target="chartSettingsID()"
|
|
triggers="click"
|
|
:show.sync="settingsShow"
|
|
placement="auto"
|
|
container="stats-container"
|
|
ref="popover"
|
|
@show="onShow"
|
|
>
|
|
<template #title>
|
|
<b-button @click="onClose" class="close" aria-label="Close">
|
|
<span class="d-inline-block" aria-hidden="true">×</span>
|
|
</b-button>
|
|
Settings
|
|
</template>
|
|
|
|
<div>
|
|
<b-form-group
|
|
label="Limit"
|
|
label-for="limitinput"
|
|
label-cols="3"
|
|
class="mb-1"
|
|
description="Limit number of items"
|
|
invalid-feedback="This field is required"
|
|
>
|
|
<b-form-input
|
|
ref="limitinput"
|
|
id="limitinput"
|
|
v-model="limitState"
|
|
type="number"
|
|
size="sm"
|
|
></b-form-input>
|
|
</b-form-group>
|
|
|
|
<b-button @click="onClose" size="sm" variant="danger">Cancel</b-button>
|
|
<b-button @click="onOk" size="sm" variant="primary">Ok</b-button>
|
|
</div>
|
|
</b-popover>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
|
import { StatResult } from '@/apiary/apiary';
|
|
import axios from 'axios';
|
|
import { Chart, ArcElement, Legend, DoughnutController, Title } from 'chart.js';
|
|
import randomColor from 'randomcolor';
|
|
|
|
export type StatType = 'username' | 'password' | 'ip' | 'country' | 'total';
|
|
|
|
Chart.register(ArcElement, Legend, DoughnutController, Title);
|
|
|
|
@Component
|
|
export default class StatsPie extends Vue {
|
|
@Prop() private statType!: StatType;
|
|
|
|
limit: number;
|
|
|
|
limitState: number;
|
|
|
|
settingsShow = false;
|
|
|
|
stats: StatResult[];
|
|
|
|
chart?: Chart;
|
|
|
|
constructor() {
|
|
super();
|
|
this.stats = [];
|
|
this.limit = 10;
|
|
this.limitState = 10;
|
|
}
|
|
|
|
title(): string {
|
|
switch (this.statType) {
|
|
case 'password':
|
|
return `Top ${this.limit} Passwords`;
|
|
case 'username':
|
|
return `Top ${this.limit} Usernames`;
|
|
case 'ip':
|
|
return `Top ${this.limit} IPs`;
|
|
case 'country':
|
|
return `Top ${this.limit} Countries`;
|
|
case 'total':
|
|
return 'Totals';
|
|
// Why doesn't eslint know that this switch is exhaustive?
|
|
default:
|
|
return 'Top 10 Passwords';
|
|
}
|
|
}
|
|
|
|
containerClass(): string {
|
|
return `stats-container-${this.statType}`;
|
|
}
|
|
|
|
chartID(): string {
|
|
return `chart-${this.statType}`;
|
|
}
|
|
|
|
chartSettingsID(): string {
|
|
return `chartsettings-${this.statType}`;
|
|
}
|
|
|
|
settingsID(): string {
|
|
return `settings-${this.statType}`;
|
|
}
|
|
|
|
onClose(): void {
|
|
this.settingsShow = false;
|
|
}
|
|
|
|
onOk(): void {
|
|
if (this.limitState) {
|
|
this.limit = this.limitState;
|
|
this.settingsShow = false;
|
|
}
|
|
}
|
|
|
|
onShow(): void {
|
|
// This is called just before the popover is shown
|
|
// Reset our popover form variables
|
|
this.limitState = this.limit;
|
|
}
|
|
|
|
@Watch('limit')
|
|
limitChanged(value: number, oldValue: number): void {
|
|
this.fetchData();
|
|
}
|
|
|
|
fetchData(): void {
|
|
const url = `/api/stats?type=${this.statType}&limit=${this.limit}`;
|
|
axios.get<StatResult[]>(url).then((resp) => {
|
|
this.stats = resp.data;
|
|
this.renderPie();
|
|
});
|
|
}
|
|
|
|
mounted(): void {
|
|
this.fetchData();
|
|
}
|
|
|
|
renderPie(): void {
|
|
const elem = document.getElementById(this.chartID()) as HTMLCanvasElement;
|
|
const ctx = elem.getContext('2d') as CanvasRenderingContext2D;
|
|
const sortedStats = this.stats.sort();
|
|
const values = sortedStats.map((s) => s.count);
|
|
const headers = sortedStats.map((s) => s.name);
|
|
const colors = sortedStats.map(() => randomColor());
|
|
|
|
if (this.chart) {
|
|
this.chart.destroy();
|
|
}
|
|
this.chart = new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: headers,
|
|
datasets: [
|
|
{
|
|
data: values,
|
|
backgroundColor: colors,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
canvas {
|
|
width: 70vmin;
|
|
}
|
|
.stats-container {
|
|
align-content: center;
|
|
}
|
|
</style>
|