mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-30 13:03:09 +02:00
chore(client): tweak client
This commit is contained in:
parent
8648308823
commit
4fd386c3dc
5 changed files with 250 additions and 104 deletions
|
@ -1,81 +1,188 @@
|
|||
<template>
|
||||
<div class="zbcjwnqg">
|
||||
<div class="selects" style="display: flex;">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<optgroup :label="$ts.federation">
|
||||
<option value="federation">{{ $ts._charts.federation }}</option>
|
||||
<option value="ap-request">{{ $ts._charts.apRequest }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.users">
|
||||
<option value="users">{{ $ts._charts.usersIncDec }}</option>
|
||||
<option value="users-total">{{ $ts._charts.usersTotal }}</option>
|
||||
<option value="active-users">{{ $ts._charts.activeUsers }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.notes">
|
||||
<option value="notes">{{ $ts._charts.notesIncDec }}</option>
|
||||
<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
|
||||
<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
|
||||
<option value="notes-total">{{ $ts._charts.notesTotal }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.drive">
|
||||
<option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
|
||||
<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
||||
<option value="hour">{{ $ts.perHour }}</option>
|
||||
<option value="day">{{ $ts.perDay }}</option>
|
||||
</MkSelect>
|
||||
<div class="main">
|
||||
<div class="body">
|
||||
<div class="selects" style="display: flex;">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<optgroup :label="$ts.federation">
|
||||
<option value="federation">{{ $ts._charts.federation }}</option>
|
||||
<option value="ap-request">{{ $ts._charts.apRequest }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.users">
|
||||
<option value="users">{{ $ts._charts.usersIncDec }}</option>
|
||||
<option value="users-total">{{ $ts._charts.usersTotal }}</option>
|
||||
<option value="active-users">{{ $ts._charts.activeUsers }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.notes">
|
||||
<option value="notes">{{ $ts._charts.notesIncDec }}</option>
|
||||
<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
|
||||
<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
|
||||
<option value="notes-total">{{ $ts._charts.notesTotal }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$ts.drive">
|
||||
<option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
|
||||
<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
||||
<option value="hour">{{ $ts.perHour }}</option>
|
||||
<option value="day">{{ $ts.perDay }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
||||
<div class="subpub">
|
||||
<div class="sub">
|
||||
<div class="title">Sub</div>
|
||||
<canvas ref="subDoughnutEl"></canvas>
|
||||
</div>
|
||||
<div class="pub">
|
||||
<div class="title">Pub</div>
|
||||
<canvas ref="pubDoughnutEl"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import {
|
||||
Chart,
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
DoughnutController,
|
||||
} from 'chart.js';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkChart from '@/components/chart.vue';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkSelect,
|
||||
MkChart,
|
||||
},
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
DoughnutController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
);
|
||||
|
||||
props: {
|
||||
chartLimit: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 90
|
||||
const props = withDefaults(defineProps<{
|
||||
chartLimit?: number;
|
||||
detailed?: boolean;
|
||||
}>(), {
|
||||
chartLimit: 90,
|
||||
});
|
||||
|
||||
const chartSpan = $ref<'hour' | 'day'>('hour');
|
||||
const chartSrc = $ref('active-users');
|
||||
let subDoughnutEl = $ref<HTMLCanvasElement>();
|
||||
let pubDoughnutEl = $ref<HTMLCanvasElement>();
|
||||
|
||||
const { handler: externalTooltipHandler1 } = useChartTooltip();
|
||||
const { handler: externalTooltipHandler2 } = useChartTooltip();
|
||||
|
||||
function createDoughnut(chartEl, tooltip, data) {
|
||||
return new Chart(chartEl, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: data.map(x => x.name),
|
||||
datasets: [{
|
||||
backgroundColor: data.map(x => x.color),
|
||||
data: data.map(x => x.value),
|
||||
}],
|
||||
},
|
||||
detailed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
options: {
|
||||
layout: {
|
||||
padding: {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: tooltip,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setup() {
|
||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
||||
const chartSrc = ref('active-users');
|
||||
|
||||
return {
|
||||
chartSrc,
|
||||
chartSpan,
|
||||
};
|
||||
},
|
||||
onMounted(() => {
|
||||
os.apiGet('federation/stats').then(fedStats => {
|
||||
createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followersCount })).concat([{ name: '(other)', color: '#808080', value: fedStats.otherFollowersCount }]));
|
||||
createDoughnut(pubDoughnutEl, externalTooltipHandler1, fedStats.topPubInstances.map(x => ({ name: x.host, color: x.themeColor, value: x.followingCount })).concat([{ name: '(other)', color: '#808080', value: fedStats.otherFollowingCount }]));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zbcjwnqg {
|
||||
> .selects {
|
||||
> .main {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
> .body {
|
||||
> .chart {
|
||||
padding: 8px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .chart {
|
||||
padding: 8px 0 0 0;
|
||||
> .subpub {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
> .sub, > .pub {
|
||||
position: relative;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
|
||||
> .title {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,31 +1,35 @@
|
|||
<template>
|
||||
<div class="igpposuu _monospace">
|
||||
<div v-if="value === null" class="null">null</div>
|
||||
<div v-else-if="typeof value === 'boolean'" class="boolean">{{ value ? 'true' : 'false' }}</div>
|
||||
<div v-else-if="typeof value === 'boolean'" class="boolean" :class="{ true: value, false: !value }">{{ value ? 'true' : 'false' }}</div>
|
||||
<div v-else-if="typeof value === 'string'" class="string">"{{ value }}"</div>
|
||||
<div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div>
|
||||
<div v-else-if="Array.isArray(value)" class="array">
|
||||
<button @click="collapsed_ = !collapsed_">[ {{ collapsed_ ? '+' : '-' }} ]</button>
|
||||
<template v-if="!collapsed_">
|
||||
<div v-for="i in value.length" class="element">
|
||||
{{ i }}: <XValue :value="value[i - 1]" collapsed/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="isArray(value) && isEmpty(value)" class="array empty">[]</div>
|
||||
<div v-else-if="isArray(value)" class="array">
|
||||
<div v-for="i in value.length" class="element">
|
||||
{{ i }}: <XValue :value="value[i - 1]" collapsed/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="typeof value === 'object'" class="object">
|
||||
<button @click="collapsed_ = !collapsed_">{ {{ collapsed_ ? '+' : '-' }} }</button>
|
||||
<template v-if="!collapsed_">
|
||||
<div v-for="k in Object.keys(value)" class="kv">
|
||||
<div class="k">{{ k }}:</div>
|
||||
<div class="v"><XValue :value="value[k]" collapsed/></div>
|
||||
<div v-else-if="isObject(value) && isEmpty(value)" class="object empty">{}</div>
|
||||
<div v-else-if="isObject(value)" class="object">
|
||||
<div v-for="k in Object.keys(value)" class="kv">
|
||||
<button class="toggle _button" :class="{ visible: collapsable(value[k]) }" @click="collapsed[k] = !collapsed[k]">{{ collapsed[k] ? '+' : '-' }}</button>
|
||||
<div class="k">{{ k }}:</div>
|
||||
<div v-if="collapsed[k]" class="v">
|
||||
<button class="_button" @click="collapsed[k] = !collapsed[k]">
|
||||
<template v-if="typeof value[k] === 'string'">"..."</template>
|
||||
<template v-else-if="isArray(value[k])">[...]</template>
|
||||
<template v-else-if="isObject(value[k])">{...}</template>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="v"><XValue :value="value[k]"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import { computed, defineComponent, reactive, ref } from 'vue';
|
||||
import number from '@/filters/number';
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -33,24 +37,44 @@ export default defineComponent({
|
|||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const collapsed_ = ref(props.collapsed);
|
||||
const collapsed = reactive({});
|
||||
|
||||
if (isObject(props.value)) {
|
||||
for (const key in props.value) {
|
||||
collapsed[key] = collapsable(props.value[key]);
|
||||
}
|
||||
}
|
||||
|
||||
function isObject(v): boolean {
|
||||
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
||||
}
|
||||
|
||||
function isArray(v): boolean {
|
||||
return Array.isArray(v);
|
||||
}
|
||||
|
||||
function isEmpty(v): boolean {
|
||||
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
||||
}
|
||||
|
||||
function collapsable(v): boolean {
|
||||
return (isObject(v) || isArray(v)) && !isEmpty(v);
|
||||
}
|
||||
|
||||
return {
|
||||
number,
|
||||
collapsed_,
|
||||
collapsed,
|
||||
isObject,
|
||||
isArray,
|
||||
isEmpty,
|
||||
collapsable,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -66,6 +90,14 @@ export default defineComponent({
|
|||
> .boolean {
|
||||
display: inline;
|
||||
color: var(--codeBoolean);
|
||||
|
||||
&.true {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.false {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
> .string {
|
||||
|
@ -78,7 +110,12 @@ export default defineComponent({
|
|||
color: var(--codeNumber);
|
||||
}
|
||||
|
||||
> .array {
|
||||
> .array.empty {
|
||||
display: inline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .array:not(.empty) {
|
||||
display: inline;
|
||||
|
||||
> .element {
|
||||
|
@ -87,13 +124,28 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
> .object {
|
||||
> .object.empty {
|
||||
display: inline;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .object:not(.empty) {
|
||||
display: inline;
|
||||
|
||||
> .kv {
|
||||
display: block;
|
||||
padding-left: 16px;
|
||||
|
||||
> .toggle {
|
||||
width: 16px;
|
||||
color: var(--accent);
|
||||
visibility: hidden;
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
> .k {
|
||||
display: inline;
|
||||
margin-right: 8px;
|
||||
|
|
|
@ -4,26 +4,13 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XValue from './object-view.value.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XValue
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
|
||||
}
|
||||
});
|
||||
const props = defineProps<{
|
||||
value: Record<string, unknown>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
<MkSpacer v-else-if="tab === 'federation'" :content-max="1000" :margin-min="20">
|
||||
<XFederation/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
|
||||
<MkSpacer v-else-if="tab === 'charts'" :content-max="1000" :margin-min="20">
|
||||
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
|
|
|
@ -294,7 +294,7 @@ const headerTabs = $computed(() => [{
|
|||
icon: 'fas fa-share-alt',
|
||||
}, {
|
||||
key: 'raw',
|
||||
title: 'Raw data',
|
||||
title: 'Raw',
|
||||
icon: 'fas fa-code',
|
||||
}].filter(x => x != null));
|
||||
|
||||
|
|
Loading…
Reference in a new issue