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