diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 11752d15be..1f727056ab 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -558,6 +558,9 @@ userSaysSomething: "{name}が何かを言いました" makeActive: "アクティブにする" display: "表示" copy: "コピー" +metrics: "メトリクス" +overview: "概要" +logs: "ログ" _sidebar: full: "フル" diff --git a/src/client/app.vue b/src/client/app.vue index 32777e21b1..f81e7e44ad 100644 --- a/src/client/app.vue +++ b/src/client/app.vue @@ -33,7 +33,7 @@ <x-sidebar ref="nav" @change-view-mode="calcHeaderWidth"/> - <div class="contents" ref="contents" :class="{ wallpaper }"> + <div class="contents" ref="contents" :class="{ wallpaper, full: $store.state.fullView }"> <main ref="main"> <div class="content"> <transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition"> @@ -547,6 +547,18 @@ export default Vue.extend({ backdrop-filter: blur(4px); } + &.full { + width: 100%; + + > main { + width: 100%; + } + + > .widgets { + display: none; + } + } + > main { width: $main-width; min-width: 0; diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue index 552e3523f7..f863dfd95e 100644 --- a/src/client/components/instance-stats.vue +++ b/src/client/components/instance-stats.vue @@ -1,5 +1,5 @@ <template> -<div class="zbcjwnqg"> +<div class="zbcjwnqg" v-size="{ max: [550, 1200] }"> <div class="stats" v-if="info"> <div class="_panel"> <div> @@ -127,7 +127,6 @@ import { faChartBar, faUser, faPencilAlt } from '@fortawesome/free-solid-svg-ico import Chart from 'chart.js'; import MkSelect from './ui/select.vue'; -const chartLimit = 90; const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); const negate = arr => arr.map(x => -x); const alpha = (hex, a) => { @@ -143,6 +142,19 @@ export default Vue.extend({ MkSelect }, + props: { + chartLimit: { + type: Number, + required: false, + default: 90 + }, + detailed: { + type: Boolean, + required: false, + default: false + }, + }, + data() { return { info: null, @@ -209,17 +221,17 @@ export default Vue.extend({ this.now = new Date(); const [perHour, perDay] = await Promise.all([Promise.all([ - this.$root.api('charts/federation', { limit: chartLimit, span: 'hour' }), - this.$root.api('charts/users', { limit: chartLimit, span: 'hour' }), - this.$root.api('charts/active-users', { limit: chartLimit, span: 'hour' }), - this.$root.api('charts/notes', { limit: chartLimit, span: 'hour' }), - this.$root.api('charts/drive', { limit: chartLimit, span: 'hour' }), + this.$root.api('charts/federation', { limit: this.chartLimit, span: 'hour' }), + this.$root.api('charts/users', { limit: this.chartLimit, span: 'hour' }), + this.$root.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }), + this.$root.api('charts/notes', { limit: this.chartLimit, span: 'hour' }), + this.$root.api('charts/drive', { limit: this.chartLimit, span: 'hour' }), ]), Promise.all([ - this.$root.api('charts/federation', { limit: chartLimit, span: 'day' }), - this.$root.api('charts/users', { limit: chartLimit, span: 'day' }), - this.$root.api('charts/active-users', { limit: chartLimit, span: 'day' }), - this.$root.api('charts/notes', { limit: chartLimit, span: 'day' }), - this.$root.api('charts/drive', { limit: chartLimit, span: 'day' }), + this.$root.api('charts/federation', { limit: this.chartLimit, span: 'day' }), + this.$root.api('charts/users', { limit: this.chartLimit, span: 'day' }), + this.$root.api('charts/active-users', { limit: this.chartLimit, span: 'day' }), + this.$root.api('charts/notes', { limit: this.chartLimit, span: 'day' }), + this.$root.api('charts/drive', { limit: this.chartLimit, span: 'day' }), ])]); const chart = { @@ -259,11 +271,14 @@ export default Vue.extend({ this.chartInstance.destroy(); } + // TODO: var(--panel)の色が暗いか明るいかで判定する + const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); this.chartInstance = new Chart(this.$refs.chart, { type: 'line', data: { - labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), + labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), datasets: this.data.series.map(x => ({ label: x.name, data: x.data.slice().reverse(), @@ -271,6 +286,7 @@ export default Vue.extend({ lineTension: 0, borderWidth: 2, borderColor: x.color, + borderDash: x.borderDash || [], backgroundColor: alpha(x.color, 0.1), hidden: !!x.hidden })) @@ -293,17 +309,28 @@ export default Vue.extend({ }, scales: { xAxes: [{ + type: 'time', + time: { + stepSize: 1, + unit: this.chartSpan == 'day' ? 'month' : 'day', + }, gridLines: { - display: false + display: this.detailed, + color: gridColor, + zeroLineColor: gridColor, }, ticks: { - display: false + display: this.detailed } }], yAxes: [{ - position: 'right', + position: 'left', + gridLines: { + color: gridColor, + zeroLineColor: gridColor, + }, ticks: { - display: false + display: this.detailed } }] }, @@ -325,7 +352,11 @@ export default Vue.extend({ }, format(arr) { - return arr; + const now = Date.now(); + return arr.map((v, i) => ({ + x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)), + y: v + })); }, federationInstancesChart(total: boolean): any { @@ -347,6 +378,7 @@ export default Vue.extend({ name: 'All', type: 'line', color: '#008FFB', + borderDash: [5, 5], data: this.format(type == 'combined' ? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec)) : sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec)) @@ -586,17 +618,30 @@ export default Vue.extend({ <style lang="scss" scoped> .zbcjwnqg { + &.max-width_1200px { + > .stats { + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + } + } + + &.max-width_550px { + > .stats { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr 1fr 1fr; + } + } + > .stats { - display: flex; - justify-content: space-between; - flex-wrap: wrap; - margin: calc(0px - var(--margin) / 2); - margin-bottom: calc(var(--margin) / 2); + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr; + gap: var(--margin); + margin-bottom: var(--margin); + font-size: 90%; > div { display: flex; - flex: 1 0 213px; - margin: calc(var(--margin) / 2); box-sizing: border-box; padding: 16px 20px; diff --git a/src/client/components/note.sub.vue b/src/client/components/note.sub.vue index fcdb06bd9e..329fb62d53 100644 --- a/src/client/components/note.sub.vue +++ b/src/client/components/note.sub.vue @@ -1,5 +1,5 @@ <template> -<div class="wrpstxzv" :class="{ children }" v-size="[{ max: 450 }]"> +<div class="wrpstxzv" :class="{ children }" v-size="{ max: [450] }"> <div class="main"> <mk-avatar class="avatar" :user="note.user"/> <div class="body"> diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 322b634218..99a088b3e0 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -6,7 +6,7 @@ :tabindex="!isDeleted ? '-1' : null" :class="{ renote: isRenote }" v-hotkey="keymap" - v-size="[{ max: 500 }, { max: 450 }, { max: 350 }, { max: 300 }]" + v-size="{ max: [500, 450, 350, 300] }" > <x-sub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/> <x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/> diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue index 9e4806f05f..71ac963a58 100644 --- a/src/client/components/notification.vue +++ b/src/client/components/notification.vue @@ -1,5 +1,5 @@ <template> -<div class="qglefbjs" :class="notification.type" v-size="[{ max: 500 }, { max: 600 }]"> +<div class="qglefbjs" :class="notification.type" v-size="{ max: [500, 600] }"> <div class="head"> <mk-avatar v-if="notification.user" class="icon" :user="notification.user"/> <img v-else class="icon" :src="notification.icon" alt=""/> diff --git a/src/client/components/tab.vue b/src/client/components/tab.vue index 824f150840..a10c7fdbf3 100644 --- a/src/client/components/tab.vue +++ b/src/client/components/tab.vue @@ -1,5 +1,5 @@ <template> -<div class="pxhvhrfw" v-size="[{ max: 500 }]"> +<div class="pxhvhrfw" v-size="{ max: [500] }"> <button v-for="item in items" class="_button" @click="$emit('input', item.value)" :class="{ active: value === item.value }" :key="item.value"><fa v-if="item.icon" :icon="item.icon" class="icon"/>{{ item.label }}</button> </div> </template> diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue index e5b11770af..2487ffd424 100644 --- a/src/client/components/ui/container.vue +++ b/src/client/components/ui/container.vue @@ -1,5 +1,5 @@ <template> -<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader, scrollable }" v-size="[{ max: 500 }]"> +<div class="ukygtjoj _panel" :class="{ naked, hideHeader: !showHeader, scrollable }" v-size="{ max: [380], el: resizeBaseEl }"> <header v-if="showHeader"> <div class="title"><slot name="header"></slot></div> <slot name="func"></slot> @@ -52,6 +52,9 @@ export default Vue.extend({ required: false, default: false }, + resizeBaseEl: { + required: false, + }, }, data() { return { @@ -103,10 +106,6 @@ export default Vue.extend({ position: relative; overflow: hidden; - & + .ukygtjoj { - margin-top: var(--margin); - } - &.naked { background: transparent !important; box-shadow: none !important; @@ -152,12 +151,28 @@ export default Vue.extend({ } } - &.max-width_500px { + > div { + > ::v-deep ._content { + padding: 24px; + + & + ._content { + border-top: solid 1px var(--divider); + } + } + } + + &.max-width_380px { > header { > .title { padding: 8px 10px; } } + + > div { + > ::v-deep ._content { + padding: 16px; + } + } } } diff --git a/src/client/components/ui/folder.vue b/src/client/components/ui/folder.vue new file mode 100644 index 0000000000..0b489fe9ad --- /dev/null +++ b/src/client/components/ui/folder.vue @@ -0,0 +1,126 @@ +<template> +<div class="ssazuxis" v-size="{ max: [500] }"> + <header @click="() => showBody = !showBody" class="_button"> + <div class="title"><slot name="header"></slot></div> + <div class="divider"></div> + <button class="_button"> + <template v-if="showBody"><fa :icon="faAngleUp"/></template> + <template v-else><fa :icon="faAngleDown"/></template> + </button> + </header> + <transition name="folder-toggle" + @enter="enter" + @after-enter="afterEnter" + @leave="leave" + @after-leave="afterLeave" + > + <div v-show="showBody"> + <slot></slot> + </div> + </transition> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + props: { + expanded: { + type: Boolean, + required: false, + default: true + }, + }, + data() { + return { + showBody: this.expanded, + faAngleUp, faAngleDown + }; + }, + methods: { + toggleContent(show: boolean) { + this.showBody = show; + }, + + enter(el) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = 0; + el.offsetHeight; // reflow + el.style.height = elementHeight + 'px'; + }, + afterEnter(el) { + el.style.height = null; + }, + leave(el) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = elementHeight + 'px'; + el.offsetHeight; // reflow + el.style.height = 0; + }, + afterLeave(el) { + el.style.height = null; + }, + } +}); +</script> + +<style lang="scss" scoped> +.folder-toggle-enter-active, .folder-toggle-leave-active { + overflow-y: hidden; + transition: opacity 0.5s, height 0.5s !important; +} +.folder-toggle-enter { + opacity: 0; +} +.folder-toggle-leave-to { + opacity: 0; +} + +.ssazuxis { + position: relative; + + > header { + display: flex; + position: relative; + z-index: 2; + // TODO + // position: sticky; + // top: var(--stickyTopOffset); + // backdrop-filter: blur(20px); + + > .title { + margin: 0; + padding: 12px 16px 12px 8px; + + > [data-icon] { + margin-right: 6px; + } + + &:empty { + display: none; + } + } + + > .divider { + flex: 1; + margin: auto; + height: 1px; + background: var(--divider); + } + + > button { + width: 42px; + } + } + + &.max-width_500px { + > header { + > .title { + padding: 8px 10px; + } + } + } +} +</style> diff --git a/src/client/components/url-preview.vue b/src/client/components/url-preview.vue index 78afe6a65c..a5052d6132 100644 --- a/src/client/components/url-preview.vue +++ b/src/client/components/url-preview.vue @@ -6,7 +6,7 @@ <div v-else-if="tweetId && tweetExpanded" class="twitter" ref="twitter"> <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', left: `${tweetLeft}px`, width: `${tweetLeft < 0 ? 'auto' : '100%'}`, height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.device.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe> </div> -<div v-else class="mk-url-preview" v-size="[{ max: 400 }, { max: 350 }]"> +<div v-else class="mk-url-preview" v-size="{ max: [400, 350] }"> <transition name="zoom" mode="out-in"> <component :is="self ? 'router-link' : 'a'" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url" v-if="!fetching"> <div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`"> diff --git a/src/client/directives/size.ts b/src/client/directives/size.ts index 2b589662ff..d00b2b5b38 100644 --- a/src/client/directives/size.ts +++ b/src/client/directives/size.ts @@ -1,7 +1,11 @@ export default { - inserted(el, binding, vn) { + inserted(src, binding, vn) { const query = binding.value; + // TODO: 要素をもらうというよりはカスタム幅算出関数をもらうようにしてcalcで都度呼び出して計算するようにした方が柔軟そう + // その場合はunbindの方も改修することを忘れずに + const el = query.el ? query.el() : src; + /* const addClassRecursive = (el: Element, cls: string) => { el.classList.add(cls); @@ -32,19 +36,21 @@ export default { const calc = () => { const width = el.clientWidth; - for (const q of query) { - if (q.max) { - if (width <= q.max) { - addClass(el, 'max-width_' + q.max + 'px'); + if (query.max) { + for (const v of query.max) { + if (width <= v) { + addClass(src, 'max-width_' + v + 'px'); } else { - removeClass(el, 'max-width_' + q.max + 'px'); + removeClass(src, 'max-width_' + v + 'px'); } } - if (q.min) { - if (width >= q.min) { - addClass(el, 'min-width_' + q.min + 'px'); + } + if (query.min) { + for (const v of query.min) { + if (width >= v) { + addClass(src, 'min-width_' + v + 'px'); } else { - removeClass(el, 'min-width_' + q.min + 'px'); + removeClass(src, 'min-width_' + v + 'px'); } } } @@ -63,7 +69,11 @@ export default { el._ro_ = ro; }, - unbind(el, binding, vn) { + unbind(src, binding, vn) { + const query = binding.value; + + const el = query.el ? query.el() : src; + el._ro_.unobserve(el); } }; diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue index 26e238b128..25897ea7d9 100644 --- a/src/client/pages/instance/emojis.vue +++ b/src/client/pages/instance/emojis.vue @@ -3,7 +3,7 @@ <portal to="icon"><fa :icon="faLaugh"/></portal> <portal to="title">{{ $t('customEmojis') }}</portal> - <section class="_card local"> + <section class="_card _vMargin local"> <div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojis') }}</div> <div class="_content"> <mk-pagination :pagination="pagination" class="emojis" ref="emojis"> @@ -33,7 +33,7 @@ <mk-button inline primary @click="add"><fa :icon="faPlus"/> {{ $t('addEmoji') }}</mk-button> </div> </section> - <section class="_card remote"> + <section class="_card _vMargin remote"> <div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojisOfRemote') }}</div> <div class="_content"> <mk-input v-model="host" :debounce="true"><span>{{ $t('host') }}</span></mk-input> diff --git a/src/client/pages/instance/federation.instance.vue b/src/client/pages/instance/federation.instance.vue index 556ed5f41c..80b379c6b1 100644 --- a/src/client/pages/instance/federation.instance.vue +++ b/src/client/pages/instance/federation.instance.vue @@ -2,69 +2,69 @@ <x-window @closed="() => { $emit('closed'); destroyDom(); }" :no-padding="true" :width="520" :height="500"> <template #header>{{ instance.host }}</template> <div class="mk-instance-info"> - <div class="table info"> - <div class="row"> - <div class="cell"> - <div class="label">{{ $t('software') }}</div> - <div class="data">{{ instance.softwareName || '?' }}</div> + <div class="_table"> + <div class="_row"> + <div class="_cell"> + <div class="_label">{{ $t('software') }}</div> + <div class="_data">{{ instance.softwareName || '?' }}</div> </div> - <div class="cell"> - <div class="label">{{ $t('version') }}</div> - <div class="data">{{ instance.softwareVersion || '?' }}</div> + <div class="_cell"> + <div class="_label">{{ $t('version') }}</div> + <div class="_data">{{ instance.softwareVersion || '?' }}</div> </div> </div> </div> - <div class="table data"> - <div class="row"> - <div class="cell"> - <div class="label"><fa :icon="faCrosshairs" fixed-width class="icon"/>{{ $t('registeredAt') }}</div> - <div class="data">{{ new Date(instance.caughtAt).toLocaleString() }} (<mk-time :time="instance.caughtAt"/>)</div> + <div class="_table data"> + <div class="_row"> + <div class="_cell"> + <div class="_label"><fa :icon="faCrosshairs" fixed-width class="icon"/>{{ $t('registeredAt') }}</div> + <div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<mk-time :time="instance.caughtAt"/>)</div> </div> </div> - <div class="row"> - <div class="cell"> + <div class="_row"> + <div class="_cell"> <div class="label"><fa :icon="faCloudDownloadAlt" fixed-width class="icon"/>{{ $t('following') }}</div> - <div class="data clickable" @click="showFollowing()">{{ instance.followingCount | number }}</div> + <button class="_data _textButton" @click="showFollowing()">{{ instance.followingCount | number }}</button> </div> - <div class="cell"> - <div class="label"><fa :icon="faCloudUploadAlt" fixed-width class="icon"/>{{ $t('followers') }}</div> - <div class="data clickable" @click="showFollowers()">{{ instance.followersCount | number }}</div> + <div class="_cell"> + <div class="_label"><fa :icon="faCloudUploadAlt" fixed-width class="icon"/>{{ $t('followers') }}</div> + <button class="_data _textButton" @click="showFollowers()">{{ instance.followersCount | number }}</button> </div> </div> - <div class="row"> - <div class="cell"> - <div class="label"><fa :icon="faUsers" fixed-width class="icon"/>{{ $t('users') }}</div> - <div class="data clickable" @click="showUsers()">{{ instance.usersCount | number }}</div> + <div class="_row"> + <div class="_cell"> + <div class="_label"><fa :icon="faUsers" fixed-width class="icon"/>{{ $t('users') }}</div> + <button class="_data _textButton" @click="showUsers()">{{ instance.usersCount | number }}</button> </div> - <div class="cell"> - <div class="label"><fa :icon="faPencilAlt" fixed-width class="icon"/>{{ $t('notes') }}</div> - <div class="data">{{ instance.notesCount | number }}</div> + <div class="_cell"> + <div class="_label"><fa :icon="faPencilAlt" fixed-width class="icon"/>{{ $t('notes') }}</div> + <div class="_data">{{ instance.notesCount | number }}</div> </div> </div> - <div class="row"> - <div class="cell"> - <div class="label"><fa :icon="faFileImage" fixed-width class="icon"/>{{ $t('files') }}</div> - <div class="data">{{ instance.driveFiles | number }}</div> + <div class="_row"> + <div class="_cell"> + <div class="_label"><fa :icon="faFileImage" fixed-width class="icon"/>{{ $t('files') }}</div> + <div class="_data">{{ instance.driveFiles | number }}</div> </div> - <div class="cell"> - <div class="label"><fa :icon="faDatabase" fixed-width class="icon"/>{{ $t('storageUsage') }}</div> - <div class="data">{{ instance.driveUsage | bytes }}</div> + <div class="_cell"> + <div class="_label"><fa :icon="faDatabase" fixed-width class="icon"/>{{ $t('storageUsage') }}</div> + <div class="_data">{{ instance.driveUsage | bytes }}</div> </div> </div> - <div class="row"> - <div class="cell"> - <div class="label"><fa :icon="faLongArrowAltUp" fixed-width class="icon"/>{{ $t('latestRequestSentAt') }}</div> - <div class="data"><mk-time v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div> + <div class="_row"> + <div class="_cell"> + <div class="_label"><fa :icon="faLongArrowAltUp" fixed-width class="icon"/>{{ $t('latestRequestSentAt') }}</div> + <div class="_data"><mk-time v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div> </div> - <div class="cell"> - <div class="label"><fa :icon="faTrafficLight" fixed-width class="icon"/>{{ $t('latestStatus') }}</div> - <div class="data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div> + <div class="_cell"> + <div class="_label"><fa :icon="faTrafficLight" fixed-width class="icon"/>{{ $t('latestStatus') }}</div> + <div class="_data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div> </div> </div> - <div class="row"> - <div class="cell"> - <div class="label"><fa :icon="faLongArrowAltDown" fixed-width class="icon"/>{{ $t('latestRequestReceivedAt') }}</div> - <div class="data"><mk-time v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div> + <div class="_row"> + <div class="_cell"> + <div class="_label"><fa :icon="faLongArrowAltDown" fixed-width class="icon"/>{{ $t('latestRequestReceivedAt') }}</div> + <div class="_data"><mk-time v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div> </div> </div> </div> @@ -483,39 +483,12 @@ export default Vue.extend({ .mk-instance-info { overflow: auto; - > .table { + > ._table { padding: 0 32px; @media (max-width: 500px) { padding: 0 16px; } - - > .row { - display: flex; - - &:not(:last-child) { - margin-bottom: 8px; - } - - > .cell { - flex: 1; - - > .label { - font-size: 80%; - opacity: 0.7; - - > .icon { - margin-right: 4px; - display: none; - } - } - - > .data.clickable { - color: var(--accent); - cursor: pointer; - } - } - } } > .data { diff --git a/src/client/pages/instance/index.queue-chart.vue b/src/client/pages/instance/index.queue-chart.vue new file mode 100644 index 0000000000..760d111d3a --- /dev/null +++ b/src/client/pages/instance/index.queue-chart.vue @@ -0,0 +1,183 @@ +<template> +<mk-container :body-togglable="false"> + <template #header><slot name="title"></slot></template> + <div class="_content _table"> + <div class="_row"> + <div class="_cell"><div class="_label">Process</div>{{ activeSincePrevTick | number }}</div> + <div class="_cell"><div class="_label">Active</div>{{ active | number }}</div> + <div class="_cell"><div class="_label">Waiting</div>{{ waiting | number }}</div> + <div class="_cell"><div class="_label">Delayed</div>{{ delayed | number }}</div> + </div> + </div> + <div class="_content" style="margin-bottom: -8px;"> + <canvas ref="chart"></canvas> + </div> +</mk-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Chart from 'chart.js'; +import MkContainer from '../../components/ui/container.vue'; + +const alpha = (hex, a) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; + const r = parseInt(result[1], 16); + const g = parseInt(result[2], 16); + const b = parseInt(result[3], 16); + return `rgba(${r}, ${g}, ${b}, ${a})`; +}; + +export default Vue.extend({ + components: { + MkContainer, + }, + + props: { + domain: { + required: true + }, + connection: { + required: true + }, + }, + + data() { + return { + chart: null, + activeSincePrevTick: 0, + active: 0, + waiting: 0, + delayed: 0, + } + }, + + mounted() { + // TODO: var(--panel)の色が暗いか明るいかで判定する + const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + + Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); + + this.chart = new Chart(this.$refs.chart, { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Process', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#00E396', + backgroundColor: alpha('#00E396', 0.1), + data: [] + }, { + label: 'Active', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#00BCD4', + backgroundColor: alpha('#00BCD4', 0.1), + data: [] + }, { + label: 'Waiting', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#FFB300', + backgroundColor: alpha('#FFB300', 0.1), + data: [] + }, { + label: 'Delayed', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#E53935', + borderDash: [5, 5], + fill: false, + data: [] + }] + }, + options: { + aspectRatio: 3, + layout: { + padding: { + left: 0, + right: 0, + top: 8, + bottom: 0 + } + }, + legend: { + position: 'bottom', + labels: { + boxWidth: 16, + } + }, + scales: { + xAxes: [{ + gridLines: { + display: false, + color: gridColor, + zeroLineColor: gridColor, + }, + ticks: { + display: false + } + }], + yAxes: [{ + position: 'right', + gridLines: { + display: true, + color: gridColor, + zeroLineColor: gridColor, + }, + ticks: { + display: false, + } + }] + }, + tooltips: { + intersect: false, + mode: 'index', + } + } + }); + + this.connection.on('stats', this.onStats); + this.connection.on('statsLog', this.onStatsLog); + }, + + beforeDestroy() { + this.connection.off('stats', this.onStats); + this.connection.off('statsLog', this.onStatsLog); + }, + + methods: { + onStats(stats) { + this.activeSincePrevTick = stats[this.domain].activeSincePrevTick; + this.active = stats[this.domain].active; + this.waiting = stats[this.domain].waiting; + this.delayed = stats[this.domain].delayed; + this.chart.data.labels.push(''); + this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick); + this.chart.data.datasets[1].data.push(stats[this.domain].active); + this.chart.data.datasets[2].data.push(stats[this.domain].waiting); + this.chart.data.datasets[3].data.push(stats[this.domain].delayed); + if (this.chart.data.datasets[0].data.length > 200) { + this.chart.data.labels.shift(); + this.chart.data.datasets[0].data.shift(); + this.chart.data.datasets[1].data.shift(); + this.chart.data.datasets[2].data.shift(); + this.chart.data.datasets[3].data.shift(); + } + this.chart.update(); + }, + + onStatsLog(statsLog) { + for (const stats of [...statsLog].reverse()) { + this.onStats(stats); + } + }, + } +}); +</script> diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue index 3aedcb65af..e824f1ecfb 100644 --- a/src/client/pages/instance/index.vue +++ b/src/client/pages/instance/index.vue @@ -1,112 +1,178 @@ <template> -<div v-if="meta" class="xhexznfu"> +<div v-if="meta" class="xhexznfu" v-size="{ min: [1600] }"> <portal to="icon"><fa :icon="faServer"/></portal> <portal to="title">{{ $t('instance') }}</portal> - <mk-instance-stats style="margin-bottom: var(--margin);"/> + <mk-folder> + <template #header><fa :icon="faTachometerAlt"/> {{ $t('overview') }}</template> - <section class="_card logs"> - <div class="_title"><fa :icon="faStream"/> {{ $t('serverLogs') }}</div> - <div class="_content"> - <div class="_inputs"> - <mk-input v-model="logDomain" :debounce="true"> - <span>{{ $t('domain') }}</span> - </mk-input> - <mk-select v-model="logLevel"> - <template #label>{{ $t('level') }}</template> - <option value="all">{{ $t('levels.all') }}</option> - <option value="info">{{ $t('levels.info') }}</option> - <option value="success">{{ $t('levels.success') }}</option> - <option value="warning">{{ $t('levels.warning') }}</option> - <option value="error">{{ $t('levels.error') }}</option> - <option value="debug">{{ $t('levels.debug') }}</option> - </mk-select> - </div> + <div class="sboqnrfi"> + <mk-instance-stats :chart-limit="300" :detailed="true"/> - <div class="logs"> - <code v-for="log in logs" :key="log.id" :class="log.level"> - <details> - <summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary> - <vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty> - </details> - </code> - </div> - </div> - <div class="_footer"> - <mk-button @click="deleteAllLogs()" primary><fa :icon="faTrashAlt"/> {{ $t('deleteAll') }}</mk-button> - </div> - </section> + <div class="column"> + <mk-container :body-togglable="false" :resize-base-el="() => $el"> + <template #header><fa :icon="faInfoCircle"/>{{ $t('instanceInfo') }}</template> - <section class="_card chart"> - <div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div> - <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> - <canvas ref="cpumem"></canvas> - </div> - <div class="_content" v-if="serverInfo"> - <div class="table"> - <div class="row"> - <div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div> - </div> - <div class="row"> - <div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div> - <div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div> - <div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div> - </div> - </div> - </div> - </section> - <section class="_card chart"> - <div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div> - <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> - <canvas ref="disk"></canvas> - </div> - <div class="_content" v-if="serverInfo"> - <div class="table"> - <div class="row"> - <div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div> - <div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div> - <div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div> - </div> - </div> - </div> - </section> - <section class="_card chart"> - <div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div> - <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> - <canvas ref="net"></canvas> - </div> - <div class="_content" v-if="serverInfo"> - <div class="table"> - <div class="row"> - <div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div> - </div> - </div> - </div> - </section> + <div class="_content"> + <div class="_keyValue"><b>Misskey</b><span>v{{ version }}</span></div> + </div> + <div class="_content" v-if="serverInfo"> + <div class="_keyValue"><b>Node.js</b><span>{{ serverInfo.node }}</span></div> + <div class="_keyValue"><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div> + <div class="_keyValue"><b>Redis</b><span>v{{ serverInfo.redis }}</span></div> + </div> + </mk-container> - <section class="_card info"> - <div class="_content table"> - <div><b>Misskey</b><span>v{{ version }}</span></div> + <mkw-federation/> + </div> </div> - <div class="_content table" v-if="serverInfo"> - <div><b>Node.js</b><span>{{ serverInfo.node }}</span></div> - <div><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div> - <div><b>Redis</b><span>v{{ serverInfo.redis }}</span></div> + </mk-folder> + + <mk-folder style="margin: var(--margin) 0;"> + <template #header><fa :icon="faHeartbeat"/> {{ $t('metrics') }}</template> + + <div class="segusily"> + <mk-container :body-togglable="false" :resize-base-el="() => $el"> + <template #header><fa :icon="faMicrochip"/>{{ $t('cpuAndMemory') }}</template> + + <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> + <canvas ref="cpumem"></canvas> + </div> + <div class="_content" v-if="serverInfo"> + <div class="_table"> + <!-- + <div class="_row"> + <div class="_cell"><div class="_label">CPU</div>{{ serverInfo.cpu.model }}</div> + </div> + --> + <div class="_row"> + <div class="_cell"><div class="_label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div> + <div class="_cell"><div class="_label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div> + <div class="_cell"><div class="_label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div> + </div> + </div> + </div> + </mk-container> + <mk-container :body-togglable="false" :resize-base-el="() => $el"> + <template #header><fa :icon="faHdd"/> {{ $t('disk') }}</template> + + <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> + <canvas ref="disk"></canvas> + </div> + <div class="_content" v-if="serverInfo"> + <div class="_table"> + <div class="_row"> + <div class="_cell"><div class="_label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div> + <div class="_cell"><div class="_label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div> + <div class="_cell"><div class="_label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div> + </div> + </div> + </div> + </mk-container> + <mk-container :body-togglable="false" :resize-base-el="() => $el"> + <template #header><fa :icon="faExchangeAlt"/> {{ $t('network') }}</template> + + <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> + <canvas ref="net"></canvas> + </div> + <div class="_content" v-if="serverInfo"> + <div class="_table"> + <div class="_row"> + <div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div> + </div> + </div> + </div> + </mk-container> </div> - </section> + </mk-folder> + + <mk-folder> + <template #header><fa :icon="faClipboardList"/> {{ $t('jobQueue') }}</template> + + <div class="vkyrmkwb"> + <mk-container :body-togglable="false" :resize-base-el="() => $el"> + <template #header><fa :icon="faExclamationTriangle"/> {{ $t('delayed') }}</template> + + <div class="_content"> + <div class="_keyValue" v-for="job in jobs" :key="job[0]"> + <div>{{ job[0] }}</div> + <div>{{ job[1] | number }} jobs</div> + </div> + </div> + </mk-container> + <x-queue :connection="queueConnection" domain="inbox"> + <template #title><fa :icon="faExchangeAlt"/> In</template> + </x-queue> + <x-queue :connection="queueConnection" domain="deliver"> + <template #title><fa :icon="faExchangeAlt"/> Out</template> + </x-queue> + </div> + </mk-folder> + + <mk-folder> + <template #header><fa :icon="faStream"/> {{ $t('logs') }}</template> + + <div class="uwuemslx"> + <mk-container :body-togglable="false" :resize-base-el="() => $el"> + <template #header><fa :icon="faInfoCircle"/>{{ $t('') }}</template> + + <div class="_content"> + <div class="_keyValue" v-for="log in modLogs"> + <b>{{ log.type }}</b><span>by {{ log.user.username }}</span><mk-time :time="log.createdAt" style="opacity: 0.7;"/> + </div> + </div> + </mk-container> + + <section class="_card logs"> + <div class="_title"><fa :icon="faStream"/> {{ $t('serverLogs') }}</div> + <div class="_content"> + <div class="_inputs"> + <mk-input v-model="logDomain" :debounce="true"> + <span>{{ $t('domain') }}</span> + </mk-input> + <mk-select v-model="logLevel"> + <template #label>{{ $t('level') }}</template> + <option value="all">{{ $t('levels.all') }}</option> + <option value="info">{{ $t('levels.info') }}</option> + <option value="success">{{ $t('levels.success') }}</option> + <option value="warning">{{ $t('levels.warning') }}</option> + <option value="error">{{ $t('levels.error') }}</option> + <option value="debug">{{ $t('levels.debug') }}</option> + </mk-select> + </div> + + <div class="logs"> + <code v-for="log in logs" :key="log.id" :class="log.level"> + <details> + <summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary> + <vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty> + </details> + </code> + </div> + </div> + <div class="_footer"> + <mk-button @click="deleteAllLogs()" primary><fa :icon="faTrashAlt"/> {{ $t('deleteAll') }}</mk-button> + </div> + </section> + </div> + </mk-folder> </div> </template> <script lang="ts"> import Vue from 'vue'; -import { faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import { faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList } from '@fortawesome/free-solid-svg-icons'; import Chart from 'chart.js'; import VueJsonPretty from 'vue-json-pretty'; import MkInstanceStats from '../../components/instance-stats.vue'; import MkButton from '../../components/ui/button.vue'; import MkSelect from '../../components/ui/select.vue'; import MkInput from '../../components/ui/input.vue'; +import MkContainer from '../../components/ui/container.vue'; +import MkFolder from '../../components/ui/folder.vue'; +import MkwFederation from '../../widgets/federation.vue'; import { version, url } from '../../config'; +import XQueue from './index.queue-chart.vue'; const alpha = (hex, a) => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; @@ -128,7 +194,11 @@ export default Vue.extend({ MkButton, MkSelect, MkInput, - VueJsonPretty + MkContainer, + MkFolder, + MkwFederation, + XQueue, + VueJsonPretty, }, data() { @@ -138,13 +208,16 @@ export default Vue.extend({ stats: null, serverInfo: null, connection: null, + queueConnection: this.$root.stream.useSharedConnection('queueStats'), memUsage: 0, chartCpuMem: null, chartNet: null, + jobs: [], logs: [], logLevel: 'all', logDomain: '', - faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt + modLogs: [], + faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList, } }, @@ -165,8 +238,17 @@ export default Vue.extend({ } }, + created() { + this.$store.commit('setFullView', true); + }, + mounted() { this.fetchLogs(); + this.fetchJobs(); + this.fetchModLogs(); + + // TODO: var(--panel)の色が暗いか明るいかで判定する + const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); @@ -220,14 +302,21 @@ export default Vue.extend({ scales: { xAxes: [{ gridLines: { - display: false + display: false, + color: gridColor, + zeroLineColor: gridColor, }, ticks: { - display: false + display: false, } }], yAxes: [{ position: 'right', + gridLines: { + display: true, + color: gridColor, + zeroLineColor: gridColor, + }, ticks: { display: false, max: 100 @@ -282,7 +371,9 @@ export default Vue.extend({ scales: { xAxes: [{ gridLines: { - display: false + display: false, + color: gridColor, + zeroLineColor: gridColor, }, ticks: { display: false @@ -290,6 +381,11 @@ export default Vue.extend({ }], yAxes: [{ position: 'right', + gridLines: { + display: true, + color: gridColor, + zeroLineColor: gridColor, + }, ticks: { display: false, } @@ -343,7 +439,9 @@ export default Vue.extend({ scales: { xAxes: [{ gridLines: { - display: false + display: false, + color: gridColor, + zeroLineColor: gridColor, }, ticks: { display: false @@ -351,6 +449,11 @@ export default Vue.extend({ }], yAxes: [{ position: 'right', + gridLines: { + display: true, + color: gridColor, + zeroLineColor: gridColor, + }, ticks: { display: false, } @@ -373,6 +476,13 @@ export default Vue.extend({ id: Math.random().toString().substr(2, 8), length: 150 }); + + this.$nextTick(() => { + this.queueConnection.send('requestLog', { + id: Math.random().toString().substr(2, 8), + length: 200 + }); + }); }); }, @@ -380,6 +490,8 @@ export default Vue.extend({ this.connection.off('stats', this.onStats); this.connection.off('statsLog', this.onStatsLog); this.connection.dispose(); + this.queueConnection.dispose(); + this.$store.commit('setFullView', false); }, methods: { @@ -393,6 +505,18 @@ export default Vue.extend({ }); }, + fetchJobs() { + this.$root.api('admin/queue/deliver-delayed', {}).then(jobs => { + this.jobs = jobs; + }); + }, + + fetchModLogs() { + this.$root.api('admin/show-moderation-logs', {}).then(logs => { + this.modLogs = logs; + }); + }, + deleteAllLogs() { this.$root.api('admin/delete-logs').then(() => { this.$root.dialog({ @@ -446,6 +570,50 @@ export default Vue.extend({ <style lang="scss" scoped> .xhexznfu { + &.min-width_1600px { + .sboqnrfi { + display: grid; + grid-template-columns: 3.2fr 1fr; + grid-template-rows: 1fr; + gap: 16px 16px; + + > .column { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + gap: 16px 16px; + } + } + + .segusily { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr; + gap: 16px 16px; + } + + .vkyrmkwb { + display: grid; + grid-template-columns: 0.5fr 1fr 1fr; + grid-template-rows: 1fr; + gap: 16px 16px; + } + + .uwuemslx { + display: grid; + grid-template-columns: 2fr 3fr; + grid-template-rows: 1fr; + gap: 16px 16px; + height: 400px; + } + } + + .vkyrmkwb { + > * { + margin-bottom: var(--margin); + } + } + > .stats { display: flex; justify-content: space-between; @@ -491,49 +659,5 @@ export default Vue.extend({ } } } - - > .chart { - > ._content { - > .table { - > .row { - display: flex; - - &:not(:last-child) { - margin-bottom: 16px; - - @media (max-width: 500px) { - margin-bottom: 8px; - } - } - - > .cell { - flex: 1; - - > .label { - font-size: 80%; - opacity: 0.7; - - > .icon { - margin-right: 4px; - display: none; - } - } - } - } - } - } - } - - > .info { - > .table { - > div { - display: flex; - - > * { - flex: 1; - } - } - } - } } </style> diff --git a/src/client/pages/instance/queue.queue.vue b/src/client/pages/instance/queue.chart.vue similarity index 83% rename from src/client/pages/instance/queue.queue.vue rename to src/client/pages/instance/queue.chart.vue index c2aa545fc0..8f66c8e486 100644 --- a/src/client/pages/instance/queue.queue.vue +++ b/src/client/pages/instance/queue.chart.vue @@ -1,11 +1,13 @@ <template> -<section class="_card mk-queue-queue"> +<section class="_card"> <div class="_title"><slot name="title"></slot></div> - <div class="_content status"> - <div class="cell"><div class="label">Process</div>{{ activeSincePrevTick | number }}</div> - <div class="cell"><div class="label">Active</div>{{ active | number }}</div> - <div class="cell"><div class="label">Waiting</div>{{ waiting | number }}</div> - <div class="cell"><div class="label">Delayed</div>{{ delayed | number }}</div> + <div class="_content _table"> + <div class="_row"> + <div class="_cell"><div class="_label">Process</div>{{ activeSincePrevTick | number }}</div> + <div class="_cell"><div class="_label">Active</div>{{ active | number }}</div> + <div class="_cell"><div class="_label">Waiting</div>{{ waiting | number }}</div> + <div class="_cell"><div class="_label">Delayed</div>{{ delayed | number }}</div> + </div> </div> <div class="_content" style="margin-bottom: -8px;"> <canvas ref="chart"></canvas> @@ -58,6 +60,9 @@ export default Vue.extend({ mounted() { this.fetchJobs(); + // TODO: var(--panel)の色が暗いか明るいかで判定する + const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); this.chart = new Chart(this.$refs.chart, { @@ -118,7 +123,9 @@ export default Vue.extend({ scales: { xAxes: [{ gridLines: { - display: false + display: false, + color: gridColor, + zeroLineColor: gridColor, }, ticks: { display: false @@ -126,6 +133,11 @@ export default Vue.extend({ }], yAxes: [{ position: 'right', + gridLines: { + display: true, + color: gridColor, + zeroLineColor: gridColor, + }, ticks: { display: false, } @@ -182,20 +194,3 @@ export default Vue.extend({ } }); </script> - -<style lang="scss" scoped> -.mk-queue-queue { - > .status { - display: flex; - - > .cell { - flex: 1; - - > .label { - font-size: 80%; - opacity: 0.7; - } - } - } -} -</style> diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue index 7a2204e519..d9f12577e4 100644 --- a/src/client/pages/instance/queue.vue +++ b/src/client/pages/instance/queue.vue @@ -22,7 +22,7 @@ import Vue from 'vue'; import { faExchangeAlt } from '@fortawesome/free-solid-svg-icons'; import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; import MkButton from '../../components/ui/button.vue'; -import XQueue from './queue.queue.vue'; +import XQueue from './queue.chart.vue'; export default Vue.extend({ metaInfo() { diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/instance/relays.vue index dd18867b6a..eaf6c0b682 100644 --- a/src/client/pages/instance/relays.vue +++ b/src/client/pages/instance/relays.vue @@ -3,7 +3,7 @@ <portal to="icon"><fa :icon="faProjectDiagram"/></portal> <portal to="title">{{ $t('relays') }}</portal> - <section class="_card add"> + <section class="_card _vMargin add"> <div class="_title"><fa :icon="faPlus"/> {{ $t('addRelay') }}</div> <div class="_content"> <mk-input v-model="inbox"> @@ -13,7 +13,7 @@ </div> </section> - <section class="_card relays"> + <section class="_card _vMargin relays"> <div class="_title"><fa :icon="faProjectDiagram"/> {{ $t('addedRelays') }}</div> <div class="_content relay" v-for="relay in relays" :key="relay.inbox"> <div>{{ relay.inbox }}</div> diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue index dfd6cc6d4f..8318807d43 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -3,7 +3,7 @@ <portal to="icon"><fa :icon="faCog"/></portal> <portal to="title">{{ $t('settings') }}</portal> - <section class="_card info"> + <section class="_card _vMargin info"> <div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div> <div class="_content"> <mk-input v-model="name">{{ $t('instanceName') }}</mk-input> @@ -19,7 +19,7 @@ </div> </section> - <section class="_card info"> + <section class="_card _vMargin info"> <div class="_content"> <mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input> </div> @@ -33,7 +33,7 @@ </div> </section> - <section class="_card info"> + <section class="_card _vMargin info"> <div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div> <div class="_content"> <mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch> @@ -41,7 +41,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div> <div class="_content"> <mk-switch v-model="enableHcaptcha" ref="enableHcaptcha">{{ $t('enableHcaptcha') }}</mk-switch> @@ -59,7 +59,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div> <div class="_content"> <mk-switch v-model="enableRecaptcha" ref="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch> @@ -77,7 +77,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div> <div class="_content"> <mk-switch v-model="enableEmail" @change="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></mk-switch> @@ -100,7 +100,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div> <div class="_content"> <mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch> @@ -116,7 +116,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div> <div class="_content"> <mk-textarea v-model="pinnedUsers"> @@ -128,7 +128,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div> <div class="_content"> <mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch> @@ -141,7 +141,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faCloud"/> {{ $t('objectStorage') }}</div> <div class="_content"> <mk-switch v-model="useObjectStorage">{{ $t('useObjectStorage') }}</mk-switch> @@ -168,7 +168,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div> <div class="_content"> <mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input> @@ -176,7 +176,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div> <div class="_content"> <mk-textarea v-model="blockedHosts"> @@ -188,7 +188,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div> <div class="_content"> <header><fa :icon="faTwitter"/> Twitter</header> @@ -221,7 +221,8 @@ <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> </div> </section> - <section class="_card"> + + <section class="_card _vMargin"> <div class="_title"><fa :icon="faArchway" /> Summaly Proxy</div> <div class="_content"> <mk-input v-model="summalyProxy">URL</mk-input> diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue index b209ab68cf..cf3786c965 100644 --- a/src/client/pages/instance/users.vue +++ b/src/client/pages/instance/users.vue @@ -3,7 +3,7 @@ <portal to="icon"><fa :icon="faUsers"/></portal> <portal to="title">{{ $t('users') }}</portal> - <section class="_card lookup"> + <section class="_card _vMargin lookup"> <div class="_title"><fa :icon="faSearch"/> {{ $t('lookup') }}</div> <div class="_content"> <mk-input class="target" v-model="target" type="text" @enter="showUser()"> @@ -16,7 +16,7 @@ </div> </section> - <section class="_card users"> + <section class="_card _vMargin users"> <div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</div> <div class="_content"> <div class="inputs" style="display: flex;"> diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue index f8e3e0db67..049d918595 100644 --- a/src/client/pages/messaging/index.vue +++ b/src/client/pages/messaging/index.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-messaging" v-size="[{ max: 400 }]"> +<div class="mk-messaging" v-size="{ max: [400] }"> <portal to="icon"><fa :icon="faComments"/></portal> <portal to="title">{{ $t('messaging') }}</portal> diff --git a/src/client/pages/miauth.vue b/src/client/pages/miauth.vue index 15cde8bc25..25170725da 100644 --- a/src/client/pages/miauth.vue +++ b/src/client/pages/miauth.vue @@ -1,22 +1,22 @@ <template> <div v-if="$store.getters.isSignedIn"> - <div class="waiting _card" v-if="state == 'waiting'"> + <div class="waiting _card _vMargin" v-if="state == 'waiting'"> <div class="_content"> <mk-loading/> </div> </div> - <div class="denied _card" v-if="state == 'denied'"> + <div class="denied _card _vMargin" v-if="state == 'denied'"> <div class="_content"> <p>{{ $t('_auth.denied') }}</p> </div> </div> - <div class="accepted _card" v-else-if="state == 'accepted'"> + <div class="accepted _card _vMargin" v-else-if="state == 'accepted'"> <div class="_content"> <p v-if="callback">{{ $t('_auth.callback') }}<mk-ellipsis/></p> <p v-else>{{ $t('_auth.pleaseGoBack') }}</p> </div> </div> - <div class="_card" v-else> + <div class="_card _vMargin" v-else> <div class="_title" v-if="name">{{ $t('_auth.shareAccess', { name: name }) }}</div> <div class="_title" v-else>{{ $t('_auth.shareAccessAsk') }}</div> <div class="_content"> diff --git a/src/client/pages/my-groups/group.vue b/src/client/pages/my-groups/group.vue index 0132bc2c33..5ac6db8e98 100644 --- a/src/client/pages/my-groups/group.vue +++ b/src/client/pages/my-groups/group.vue @@ -4,7 +4,7 @@ <portal to="title">{{ group.name }}</portal> <transition name="zoom" mode="out-in"> - <div v-if="group" class="_card"> + <div v-if="group" class="_card _vMargin"> <div class="_content"> <mk-button inline @click="renameGroup()">{{ $t('rename') }}</mk-button> <mk-button inline @click="transfer()">{{ $t('transfer') }}</mk-button> @@ -14,7 +14,7 @@ </transition> <transition name="zoom" mode="out-in"> - <div v-if="group" class="_card members"> + <div v-if="group" class="_card members _vMargin"> <div class="_title">{{ $t('members') }}</div> <div class="_content"> <div class="users"> diff --git a/src/client/pages/my-lists/list.vue b/src/client/pages/my-lists/list.vue index 7052c55160..a418bdded5 100644 --- a/src/client/pages/my-lists/list.vue +++ b/src/client/pages/my-lists/list.vue @@ -4,7 +4,7 @@ <portal to="title">{{ list.name }}</portal> <transition name="zoom" mode="out-in"> - <div v-if="list" class="_card"> + <div v-if="list" class="_card _vMargin"> <div class="_content"> <mk-button inline @click="renameList()">{{ $t('rename') }}</mk-button> <mk-button inline @click="deleteList()">{{ $t('delete') }}</mk-button> @@ -13,7 +13,7 @@ </transition> <transition name="zoom" mode="out-in"> - <div v-if="list" class="_card members"> + <div v-if="list" class="_card members _vMargin"> <div class="_title">{{ $t('members') }}</div> <div class="_content"> <div class="users"> diff --git a/src/client/pages/preferences/index.vue b/src/client/pages/preferences/index.vue index c2226235b0..a2a5ab80f4 100644 --- a/src/client/pages/preferences/index.vue +++ b/src/client/pages/preferences/index.vue @@ -11,7 +11,7 @@ <x-plugins/> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faMusic"/> {{ $t('sounds') }}</div> <div class="_content"> <mk-range v-model="sfxVolume" :min="0" :max="1" :step="0.1"> @@ -53,7 +53,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faColumns"/> {{ $t('deck') }}</div> <div class="_content"> <mk-switch v-model="deckAlwaysShowMainColumn"> @@ -67,7 +67,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faCog"/> {{ $t('appearance') }}</div> <div class="_content"> <mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch> @@ -87,7 +87,7 @@ </div> </section> - <section class="_card"> + <section class="_card _vMargin"> <div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div> <div class="_content"> <mk-switch v-model="autoReload"> diff --git a/src/client/pages/room/room.vue b/src/client/pages/room/room.vue index 05b93c04e8..e20b9e2002 100644 --- a/src/client/pages/room/room.vue +++ b/src/client/pages/room/room.vue @@ -8,7 +8,7 @@ /> </portal> - <div class="controller _card" v-if="objectSelected"> + <div class="controller _card _vMargin" v-if="objectSelected"> <div class="_content"> <p class="name">{{ selectedFurnitureName }}</p> <x-preview ref="preview"/> @@ -34,7 +34,7 @@ </div> </div> - <div class="menu _card" v-if="isMyRoom"> + <div class="menu _card _vMargin" v-if="isMyRoom"> <div class="_content"> <mk-button @click="add()"><fa :icon="faBoxOpen"/> {{ $t('_rooms.addFurniture') }}</mk-button> </div> diff --git a/src/client/pages/user/index.timeline.vue b/src/client/pages/user/index.timeline.vue index f03c4adf8d..13ed49ea07 100644 --- a/src/client/pages/user/index.timeline.vue +++ b/src/client/pages/user/index.timeline.vue @@ -1,5 +1,5 @@ <template> -<div class="kjeftjfm" v-size="[{ max: 500 }]"> +<div class="kjeftjfm" v-size="{ max: [500] }"> <div class="with"> <button class="_button" @click="with_ = null" :class="{ active: with_ === null }">{{ $t('notes') }}</button> <button class="_button" @click="with_ = 'replies'" :class="{ active: with_ === 'replies' }">{{ $t('notesAndReplies') }}</button> diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue index e2f3d67caa..21aa7bece0 100644 --- a/src/client/pages/user/index.vue +++ b/src/client/pages/user/index.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-user-page" v-if="user" v-size="[{ max: 500 }]"> +<div class="mk-user-page" v-if="user" v-size="{ max: [500] }"> <portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal> <portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal> diff --git a/src/client/store.ts b/src/client/store.ts index 93a28182d2..f1d4770530 100644 --- a/src/client/store.ts +++ b/src/client/store.ts @@ -106,6 +106,7 @@ export default () => new Vuex.Store({ i: null, pendingApiRequestsCount: 0, spinner: null, + fullView: false, // Plugin pluginContexts: new Map<string, AiScript>(), @@ -248,6 +249,10 @@ export default () => new Vuex.Store({ state.i[key] = value; }, + setFullView(state, v) { + state.fullView = v; + }, + initPlugin(state, { plugin, aiscript }) { state.pluginContexts.set(plugin.id, aiscript); }, diff --git a/src/client/style.scss b/src/client/style.scss index 430e056516..2d23f81213 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -329,10 +329,6 @@ hr { ._card { @extend ._panel; - & + ._card { - margin-top: var(--margin); - } - > ._title { margin: 0; padding: 22px 32px; @@ -389,6 +385,40 @@ hr { } } +._vMargin { + & + ._vMargin { + margin-top: var(--margin); + } +} + +._table { + > ._row { + display: flex; + + &:not(:last-child) { + margin-bottom: 16px; + + @media (max-width: 500px) { + margin-bottom: 8px; + } + } + + > ._cell { + flex: 1; + + > ._label { + font-size: 80%; + opacity: 0.7; + + > ._icon { + margin-right: 4px; + display: none; + } + } + } + } +} + ._fullinfo { padding: 64px 32px; text-align: center; @@ -404,7 +434,7 @@ hr { ._keyValue { display: flex; - > div { + > * { flex: 1; } } diff --git a/src/client/widgets/define.ts b/src/client/widgets/define.ts index 107045bf4b..50c9b10e81 100644 --- a/src/client/widgets/define.ts +++ b/src/client/widgets/define.ts @@ -8,7 +8,8 @@ export default function <T extends Form>(data: { return Vue.extend({ props: { widget: { - type: Object + type: Object, + required: false }, isCustomizeMode: { type: Boolean, @@ -16,19 +17,13 @@ export default function <T extends Form>(data: { } }, - data() { - return { - bakedOldProps: null - }; - }, - computed: { id(): string { - return this.widget.id; + return this.widget ? this.widget.id : null; }, props(): Record<string, any> { - return this.widget.data; + return this.widget ? this.widget.data : {}; } }, @@ -67,7 +62,9 @@ export default function <T extends Form>(data: { }, save() { - this.$store.commit('deviceUser/updateWidget', this.widget); + if (this.widget) { + this.$store.commit('deviceUser/updateWidget', this.widget); + } } } });