chore(client): tweak client

This commit is contained in:
syuilo 2022-06-29 15:41:06 +09:00
parent ee71296eb7
commit a40862106c
5 changed files with 250 additions and 104 deletions

View file

@ -1,81 +1,188 @@
<template> <template>
<div class="zbcjwnqg"> <div class="zbcjwnqg">
<div class="selects" style="display: flex;"> <div class="main">
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> <div class="body">
<optgroup :label="$ts.federation"> <div class="selects" style="display: flex;">
<option value="federation">{{ $ts._charts.federation }}</option> <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
<option value="ap-request">{{ $ts._charts.apRequest }}</option> <optgroup :label="$ts.federation">
</optgroup> <option value="federation">{{ $ts._charts.federation }}</option>
<optgroup :label="$ts.users"> <option value="ap-request">{{ $ts._charts.apRequest }}</option>
<option value="users">{{ $ts._charts.usersIncDec }}</option> </optgroup>
<option value="users-total">{{ $ts._charts.usersTotal }}</option> <optgroup :label="$ts.users">
<option value="active-users">{{ $ts._charts.activeUsers }}</option> <option value="users">{{ $ts._charts.usersIncDec }}</option>
</optgroup> <option value="users-total">{{ $ts._charts.usersTotal }}</option>
<optgroup :label="$ts.notes"> <option value="active-users">{{ $ts._charts.activeUsers }}</option>
<option value="notes">{{ $ts._charts.notesIncDec }}</option> </optgroup>
<option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option> <optgroup :label="$ts.notes">
<option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option> <option value="notes">{{ $ts._charts.notesIncDec }}</option>
<option value="notes-total">{{ $ts._charts.notesTotal }}</option> <option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
</optgroup> <option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
<optgroup :label="$ts.drive"> <option value="notes-total">{{ $ts._charts.notesTotal }}</option>
<option value="drive-files">{{ $ts._charts.filesIncDec }}</option> </optgroup>
<option value="drive">{{ $ts._charts.storageUsageIncDec }}</option> <optgroup :label="$ts.drive">
</optgroup> <option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
</MkSelect> <option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;"> </optgroup>
<option value="hour">{{ $ts.perHour }}</option> </MkSelect>
<option value="day">{{ $ts.perDay }}</option> <MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
</MkSelect> <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>
<div class="chart"> <div class="subpub">
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart> <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>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref } from 'vue'; 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 MkSelect from '@/components/form/select.vue';
import MkChart from '@/components/chart.vue'; import MkChart from '@/components/chart.vue';
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import * as os from '@/os';
export default defineComponent({ Chart.register(
components: { ArcElement,
MkSelect, LineElement,
MkChart, BarElement,
}, PointElement,
BarController,
LineController,
DoughnutController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
);
props: { const props = withDefaults(defineProps<{
chartLimit: { chartLimit?: number;
type: Number, detailed?: boolean;
required: false, }>(), {
default: 90 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: { options: {
type: Boolean, layout: {
required: false, padding: {
default: false 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() { onMounted(() => {
const chartSpan = ref<'hour' | 'day'>('hour'); os.apiGet('federation/stats').then(fedStats => {
const chartSrc = ref('active-users'); 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 }]));
return { });
chartSrc,
chartSpan,
};
},
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.zbcjwnqg { .zbcjwnqg {
> .selects { > .main {
background: var(--panel);
border-radius: var(--radius);
padding: 24px;
margin-bottom: 16px;
> .body {
> .chart {
padding: 8px 0 0 0;
}
}
} }
> .chart { > .subpub {
padding: 8px 0 0 0; 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> </style>

View file

@ -1,31 +1,35 @@
<template> <template>
<div class="igpposuu _monospace"> <div class="igpposuu _monospace">
<div v-if="value === null" class="null">null</div> <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 === 'string'" class="string">"{{ value }}"</div>
<div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div> <div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div>
<div v-else-if="Array.isArray(value)" class="array"> <div v-else-if="isArray(value) && isEmpty(value)" class="array empty">[]</div>
<button @click="collapsed_ = !collapsed_">[ {{ collapsed_ ? '+' : '-' }} ]</button> <div v-else-if="isArray(value)" class="array">
<template v-if="!collapsed_"> <div v-for="i in value.length" class="element">
<div v-for="i in value.length" class="element"> {{ i }}: <XValue :value="value[i - 1]" collapsed/>
{{ i }}: <XValue :value="value[i - 1]" collapsed/> </div>
</div>
</template>
</div> </div>
<div v-else-if="typeof value === 'object'" class="object"> <div v-else-if="isObject(value) && isEmpty(value)" class="object empty">{}</div>
<button @click="collapsed_ = !collapsed_">{ {{ collapsed_ ? '+' : '-' }} }</button> <div v-else-if="isObject(value)" class="object">
<template v-if="!collapsed_"> <div v-for="k in Object.keys(value)" class="kv">
<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 class="k">{{ k }}:</div>
<div class="v"><XValue :value="value[k]" collapsed/></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> </div>
</template> <div v-else class="v"><XValue :value="value[k]"/></div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, ref } from 'vue'; import { computed, defineComponent, reactive, ref } from 'vue';
import number from '@/filters/number'; import number from '@/filters/number';
export default defineComponent({ export default defineComponent({
@ -33,24 +37,44 @@ export default defineComponent({
props: { props: {
value: { value: {
type: Object,
required: true, required: true,
}, },
collapsed: {
type: Boolean,
required: false,
default: false,
},
}, },
setup(props) { 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 { return {
number, number,
collapsed_, collapsed,
isObject,
isArray,
isEmpty,
collapsable,
}; };
} },
}); });
</script> </script>
@ -66,6 +90,14 @@ export default defineComponent({
> .boolean { > .boolean {
display: inline; display: inline;
color: var(--codeBoolean); color: var(--codeBoolean);
&.true {
font-weight: bold;
}
&.false {
opacity: 0.7;
}
} }
> .string { > .string {
@ -78,7 +110,12 @@ export default defineComponent({
color: var(--codeNumber); color: var(--codeNumber);
} }
> .array { > .array.empty {
display: inline;
opacity: 0.7;
}
> .array:not(.empty) {
display: inline; display: inline;
> .element { > .element {
@ -87,13 +124,28 @@ export default defineComponent({
} }
} }
> .object { > .object.empty {
display: inline;
opacity: 0.7;
}
> .object:not(.empty) {
display: inline; display: inline;
> .kv { > .kv {
display: block; display: block;
padding-left: 16px; padding-left: 16px;
> .toggle {
width: 16px;
color: var(--accent);
visibility: hidden;
&.visible {
visibility: visible;
}
}
> .k { > .k {
display: inline; display: inline;
margin-right: 8px; margin-right: 8px;

View file

@ -4,26 +4,13 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent } from 'vue'; import { } from 'vue';
import XValue from './object-view.value.vue'; import XValue from './object-view.value.vue';
export default defineComponent({ const props = defineProps<{
components: { value: Record<string, unknown>;
XValue }>();
},
props: {
value: {
type: Object,
required: true,
},
},
setup(props) {
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -73,7 +73,7 @@
<MkSpacer v-else-if="tab === 'federation'" :content-max="1000" :margin-min="20"> <MkSpacer v-else-if="tab === 'federation'" :content-max="1000" :margin-min="20">
<XFederation/> <XFederation/>
</MkSpacer> </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"/> <MkInstanceStats :chart-limit="500" :detailed="true"/>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>

View file

@ -294,7 +294,7 @@ const headerTabs = $computed(() => [{
icon: 'fas fa-share-alt', icon: 'fas fa-share-alt',
}, { }, {
key: 'raw', key: 'raw',
title: 'Raw data', title: 'Raw',
icon: 'fas fa-code', icon: 'fas fa-code',
}].filter(x => x != null)); }].filter(x => x != null));