mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-12-02 18: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,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="zbcjwnqg">
|
<div class="zbcjwnqg">
|
||||||
|
<div class="main">
|
||||||
|
<div class="body">
|
||||||
<div class="selects" style="display: flex;">
|
<div class="selects" style="display: flex;">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||||
<optgroup :label="$ts.federation">
|
<optgroup :label="$ts.federation">
|
||||||
|
@ -30,52 +32,157 @@
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
</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,
|
||||||
},
|
});
|
||||||
detailed: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
const chartSpan = $ref<'hour' | 'day'>('hour');
|
||||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
const chartSrc = $ref('active-users');
|
||||||
const chartSrc = ref('active-users');
|
let subDoughnutEl = $ref<HTMLCanvasElement>();
|
||||||
|
let pubDoughnutEl = $ref<HTMLCanvasElement>();
|
||||||
|
|
||||||
return {
|
const { handler: externalTooltipHandler1 } = useChartTooltip();
|
||||||
chartSrc,
|
const { handler: externalTooltipHandler2 } = useChartTooltip();
|
||||||
chartSpan,
|
|
||||||
};
|
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),
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</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 {
|
> .chart {
|
||||||
padding: 8px 0 0 0;
|
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>
|
</style>
|
||||||
|
|
|
@ -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 v-else class="v"><XValue :value="value[k]"/></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue