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">&times;</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>