From 4fd386c3dc5346576d52c9baaa29574d07dc6d86 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jun 2022 15:41:06 +0900
Subject: [PATCH] chore(client): tweak client

---
 .../client/src/components/instance-stats.vue  | 221 +++++++++++++-----
 .../src/components/object-view.value.vue      | 106 ++++++---
 .../client/src/components/object-view.vue     |  23 +-
 packages/client/src/pages/about.vue           |   2 +-
 packages/client/src/pages/user-info.vue       |   2 +-
 5 files changed, 250 insertions(+), 104 deletions(-)

diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue
index f386a8de9a..9a1769a3a1 100644
--- a/packages/client/src/components/instance-stats.vue
+++ b/packages/client/src/components/instance-stats.vue
@@ -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>
diff --git a/packages/client/src/components/object-view.value.vue b/packages/client/src/components/object-view.value.vue
index 6f388636dd..0c7230d783 100644
--- a/packages/client/src/components/object-view.value.vue
+++ b/packages/client/src/components/object-view.value.vue
@@ -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;
diff --git a/packages/client/src/components/object-view.vue b/packages/client/src/components/object-view.vue
index e9db96de8c..db66049fce 100644
--- a/packages/client/src/components/object-view.vue
+++ b/packages/client/src/components/object-view.vue
@@ -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>
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index bacfab771f..de89e3593c 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -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>
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index 9dfb2d87a0..76b772ece2 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -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));
 
-- 
GitLab