summaryrefslogtreecommitdiff
path: root/packages/client/src/pages/admin
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-12-27 14:36:33 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-12-27 14:36:33 +0900
commit9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch)
treece5959571a981b9c4047da3c7b3fd080aa44222c /packages/client/src/pages/admin
parentwip: retention for dashboard (diff)
downloadmisskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.gz
misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.bz2
misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.zip
rename: client -> frontend
Diffstat (limited to 'packages/client/src/pages/admin')
-rw-r--r--packages/client/src/pages/admin/_header_.vue292
-rw-r--r--packages/client/src/pages/admin/abuses.vue97
-rw-r--r--packages/client/src/pages/admin/ads.vue132
-rw-r--r--packages/client/src/pages/admin/announcements.vue112
-rw-r--r--packages/client/src/pages/admin/bot-protection.vue109
-rw-r--r--packages/client/src/pages/admin/database.vue35
-rw-r--r--packages/client/src/pages/admin/email-settings.vue126
-rw-r--r--packages/client/src/pages/admin/emoji-edit-dialog.vue106
-rw-r--r--packages/client/src/pages/admin/emojis.vue398
-rw-r--r--packages/client/src/pages/admin/files.vue120
-rw-r--r--packages/client/src/pages/admin/index.vue316
-rw-r--r--packages/client/src/pages/admin/instance-block.vue51
-rw-r--r--packages/client/src/pages/admin/integrations.discord.vue60
-rw-r--r--packages/client/src/pages/admin/integrations.github.vue60
-rw-r--r--packages/client/src/pages/admin/integrations.twitter.vue60
-rw-r--r--packages/client/src/pages/admin/integrations.vue57
-rw-r--r--packages/client/src/pages/admin/metrics.vue472
-rw-r--r--packages/client/src/pages/admin/object-storage.vue148
-rw-r--r--packages/client/src/pages/admin/other-settings.vue44
-rw-r--r--packages/client/src/pages/admin/overview.active-users.vue217
-rw-r--r--packages/client/src/pages/admin/overview.ap-requests.vue346
-rw-r--r--packages/client/src/pages/admin/overview.federation.vue185
-rw-r--r--packages/client/src/pages/admin/overview.heatmap.vue15
-rw-r--r--packages/client/src/pages/admin/overview.instances.vue50
-rw-r--r--packages/client/src/pages/admin/overview.moderators.vue55
-rw-r--r--packages/client/src/pages/admin/overview.pie.vue110
-rw-r--r--packages/client/src/pages/admin/overview.queue.chart.vue186
-rw-r--r--packages/client/src/pages/admin/overview.queue.vue127
-rw-r--r--packages/client/src/pages/admin/overview.retention.vue49
-rw-r--r--packages/client/src/pages/admin/overview.stats.vue155
-rw-r--r--packages/client/src/pages/admin/overview.users.vue57
-rw-r--r--packages/client/src/pages/admin/overview.vue190
-rw-r--r--packages/client/src/pages/admin/proxy-account.vue62
-rw-r--r--packages/client/src/pages/admin/queue.chart.chart.vue186
-rw-r--r--packages/client/src/pages/admin/queue.chart.vue149
-rw-r--r--packages/client/src/pages/admin/queue.vue56
-rw-r--r--packages/client/src/pages/admin/relays.vue103
-rw-r--r--packages/client/src/pages/admin/security.vue179
-rw-r--r--packages/client/src/pages/admin/settings.vue262
-rw-r--r--packages/client/src/pages/admin/users.vue170
40 files changed, 0 insertions, 5704 deletions
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue
deleted file mode 100644
index bdb41b2d2c..0000000000
--- a/packages/client/src/pages/admin/_header_.vue
+++ /dev/null
@@ -1,292 +0,0 @@
-<template>
-<div ref="el" class="fdidabkc" :style="{ background: bg }" @click="onClick">
- <template v-if="metadata">
- <div class="titleContainer" @click="showTabsPopup">
- <i v-if="metadata.icon" class="icon" :class="metadata.icon"></i>
-
- <div class="title">
- <div class="title">{{ metadata.title }}</div>
- </div>
- </div>
- <div class="tabs">
- <button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
- <i v-if="tab.icon" class="icon" :class="tab.icon"></i>
- <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
- </button>
- <div ref="tabHighlightEl" class="highlight"></div>
- </div>
- </template>
- <div class="buttons right">
- <template v-if="actions">
- <template v-for="action in actions">
- <MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
- <button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
- </template>
- </template>
- </div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { computed, onMounted, onUnmounted, ref, inject, watch, nextTick } from 'vue';
-import tinycolor from 'tinycolor2';
-import { popupMenu } from '@/os';
-import { url } from '@/config';
-import { scrollToTop } from '@/scripts/scroll';
-import MkButton from '@/components/MkButton.vue';
-import { i18n } from '@/i18n';
-import { globalEvents } from '@/events';
-import { injectPageMetadata } from '@/scripts/page-metadata';
-
-type Tab = {
- key?: string | null;
- title: string;
- icon?: string;
- iconOnly?: boolean;
- onClick?: (ev: MouseEvent) => void;
-};
-
-const props = defineProps<{
- tabs?: Tab[];
- tab?: string;
- actions?: {
- text: string;
- icon: string;
- asFullButton?: boolean;
- handler: (ev: MouseEvent) => void;
- }[];
- thin?: boolean;
-}>();
-
-const emit = defineEmits<{
- (ev: 'update:tab', key: string);
-}>();
-
-const metadata = injectPageMetadata();
-
-const el = ref<HTMLElement>(null);
-const tabRefs = {};
-const tabHighlightEl = $ref<HTMLElement | null>(null);
-const bg = ref(null);
-const height = ref(0);
-const hasTabs = computed(() => {
- return props.tabs && props.tabs.length > 0;
-});
-
-const showTabsPopup = (ev: MouseEvent) => {
- if (!hasTabs.value) return;
- ev.preventDefault();
- ev.stopPropagation();
- const menu = props.tabs.map(tab => ({
- text: tab.title,
- icon: tab.icon,
- active: tab.key != null && tab.key === props.tab,
- action: (ev) => {
- onTabClick(tab, ev);
- },
- }));
- popupMenu(menu, ev.currentTarget ?? ev.target);
-};
-
-const preventDrag = (ev: TouchEvent) => {
- ev.stopPropagation();
-};
-
-const onClick = () => {
- scrollToTop(el.value, { behavior: 'smooth' });
-};
-
-function onTabMousedown(tab: Tab, ev: MouseEvent): void {
- // ユーザビリティの観点からmousedown時にはonClickは呼ばない
- if (tab.key) {
- emit('update:tab', tab.key);
- }
-}
-
-function onTabClick(tab: Tab, ev: MouseEvent): void {
- if (tab.onClick) {
- ev.preventDefault();
- ev.stopPropagation();
- tab.onClick(ev);
- }
- if (tab.key) {
- emit('update:tab', tab.key);
- }
-}
-
-const calcBg = () => {
- const rawBg = metadata?.bg || 'var(--bg)';
- const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
- tinyBg.setAlpha(0.85);
- bg.value = tinyBg.toRgbString();
-};
-
-onMounted(() => {
- calcBg();
- globalEvents.on('themeChanged', calcBg);
-
- watch(() => [props.tab, props.tabs], () => {
- nextTick(() => {
- const tabEl = tabRefs[props.tab];
- if (tabEl && tabHighlightEl) {
- // offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
- // https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
- const parentRect = tabEl.parentElement.getBoundingClientRect();
- const rect = tabEl.getBoundingClientRect();
- tabHighlightEl.style.width = rect.width + 'px';
- tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
- }
- });
- }, {
- immediate: true,
- });
-});
-
-onUnmounted(() => {
- globalEvents.off('themeChanged', calcBg);
-});
-</script>
-
-<style lang="scss" scoped>
-.fdidabkc {
- --height: 60px;
- display: flex;
- width: 100%;
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
-
- > .buttons {
- --margin: 8px;
- display: flex;
- align-items: center;
- height: var(--height);
- margin: 0 var(--margin);
-
- &.right {
- margin-left: auto;
- }
-
- &:empty {
- width: var(--height);
- }
-
- > .button {
- display: flex;
- align-items: center;
- justify-content: center;
- height: calc(var(--height) - (var(--margin) * 2));
- width: calc(var(--height) - (var(--margin) * 2));
- box-sizing: border-box;
- position: relative;
- border-radius: 5px;
-
- &:hover {
- background: rgba(0, 0, 0, 0.05);
- }
-
- &.highlighted {
- color: var(--accent);
- }
- }
-
- > .fullButton {
- & + .fullButton {
- margin-left: 12px;
- }
- }
- }
-
- > .titleContainer {
- display: flex;
- align-items: center;
- max-width: 400px;
- overflow: auto;
- white-space: nowrap;
- text-align: left;
- font-weight: bold;
- flex-shrink: 0;
- margin-left: 24px;
-
- > .avatar {
- $size: 32px;
- display: inline-block;
- width: $size;
- height: $size;
- vertical-align: bottom;
- margin: 0 8px;
- pointer-events: none;
- }
-
- > .icon {
- margin-right: 8px;
- width: 16px;
- text-align: center;
- }
-
- > .title {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- line-height: 1.1;
-
- > .subtitle {
- opacity: 0.6;
- font-size: 0.8em;
- font-weight: normal;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-
- &.activeTab {
- text-align: center;
-
- > .chevron {
- display: inline-block;
- margin-left: 6px;
- }
- }
- }
- }
- }
-
- > .tabs {
- position: relative;
- margin-left: 16px;
- font-size: 0.8em;
- overflow: auto;
- white-space: nowrap;
-
- > .tab {
- display: inline-block;
- position: relative;
- padding: 0 10px;
- height: 100%;
- font-weight: normal;
- opacity: 0.7;
-
- &:hover {
- opacity: 1;
- }
-
- &.active {
- opacity: 1;
- }
-
- > .icon + .title {
- margin-left: 8px;
- }
- }
-
- > .highlight {
- position: absolute;
- bottom: 0;
- height: 3px;
- background: var(--accent);
- border-radius: 999px;
- transition: all 0.2s ease;
- pointer-events: none;
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
deleted file mode 100644
index 973ec871ab..0000000000
--- a/packages/client/src/pages/admin/abuses.vue
+++ /dev/null
@@ -1,97 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="900">
- <div class="lcixvhis">
- <div class="_section reports">
- <div class="_content">
- <div class="inputs" style="display: flex;">
- <MkSelect v-model="state" style="margin: 0; flex: 1;">
- <template #label>{{ i18n.ts.state }}</template>
- <option value="all">{{ i18n.ts.all }}</option>
- <option value="unresolved">{{ i18n.ts.unresolved }}</option>
- <option value="resolved">{{ i18n.ts.resolved }}</option>
- </MkSelect>
- <MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;">
- <template #label>{{ i18n.ts.reporteeOrigin }}</template>
- <option value="combined">{{ i18n.ts.all }}</option>
- <option value="local">{{ i18n.ts.local }}</option>
- <option value="remote">{{ i18n.ts.remote }}</option>
- </MkSelect>
- <MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;">
- <template #label>{{ i18n.ts.reporterOrigin }}</template>
- <option value="combined">{{ i18n.ts.all }}</option>
- <option value="local">{{ i18n.ts.local }}</option>
- <option value="remote">{{ i18n.ts.remote }}</option>
- </MkSelect>
- </div>
- <!-- TODO
- <div class="inputs" style="display: flex; padding-top: 1.2em;">
- <MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" :spellcheck="false">
- <span>{{ i18n.ts.username }}</span>
- </MkInput>
- <MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params().origin === 'local'">
- <span>{{ i18n.ts.host }}</span>
- </MkInput>
- </div>
- -->
-
- <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
- <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
- </MkPagination>
- </div>
- </div>
- </div>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { computed } from 'vue';
-
-import XHeader from './_header_.vue';
-import MkInput from '@/components/form/input.vue';
-import MkSelect from '@/components/form/select.vue';
-import MkPagination from '@/components/MkPagination.vue';
-import XAbuseReport from '@/components/MkAbuseReport.vue';
-import * as os from '@/os';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let reports = $ref<InstanceType<typeof MkPagination>>();
-
-let state = $ref('unresolved');
-let reporterOrigin = $ref('combined');
-let targetUserOrigin = $ref('combined');
-let searchUsername = $ref('');
-let searchHost = $ref('');
-
-const pagination = {
- endpoint: 'admin/abuse-user-reports' as const,
- limit: 10,
- params: computed(() => ({
- state,
- reporterOrigin,
- targetUserOrigin,
- })),
-};
-
-function resolved(reportId) {
- reports.removeItem(item => item.id === reportId);
-}
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.abuseReports,
- icon: 'ti ti-exclamation-circle',
-});
-</script>
-
-<style lang="scss" scoped>
-.lcixvhis {
- margin: var(--margin);
-}
-</style>
diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue
deleted file mode 100644
index 2ec926c65c..0000000000
--- a/packages/client/src/pages/admin/ads.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="900">
- <div class="uqshojas">
- <div v-for="ad in ads" class="_panel _formRoot ad">
- <MkAd v-if="ad.url" :specify="ad"/>
- <MkInput v-model="ad.url" type="url" class="_formBlock">
- <template #label>URL</template>
- </MkInput>
- <MkInput v-model="ad.imageUrl" class="_formBlock">
- <template #label>{{ i18n.ts.imageUrl }}</template>
- </MkInput>
- <FormRadios v-model="ad.place" class="_formBlock">
- <template #label>Form</template>
- <option value="square">square</option>
- <option value="horizontal">horizontal</option>
- <option value="horizontal-big">horizontal-big</option>
- </FormRadios>
- <!--
- <div style="margin: 32px 0;">
- {{ i18n.ts.priority }}
- <MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
- <MkRadio v-model="ad.priority" value="middle">{{ i18n.ts.middle }}</MkRadio>
- <MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
- </div>
- -->
- <FormSplit>
- <MkInput v-model="ad.ratio" type="number">
- <template #label>{{ i18n.ts.ratio }}</template>
- </MkInput>
- <MkInput v-model="ad.expiresAt" type="date">
- <template #label>{{ i18n.ts.expiration }}</template>
- </MkInput>
- </FormSplit>
- <MkTextarea v-model="ad.memo" class="_formBlock">
- <template #label>{{ i18n.ts.memo }}</template>
- </MkTextarea>
- <div class="buttons _formBlock">
- <MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
- <MkButton class="button" inline danger @click="remove(ad)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
- </div>
- </div>
- </div>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XHeader from './_header_.vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/form/input.vue';
-import MkTextarea from '@/components/form/textarea.vue';
-import FormRadios from '@/components/form/radios.vue';
-import FormSplit from '@/components/form/split.vue';
-import * as os from '@/os';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let ads: any[] = $ref([]);
-
-os.api('admin/ad/list').then(adsResponse => {
- ads = adsResponse;
-});
-
-function add() {
- ads.unshift({
- id: null,
- memo: '',
- place: 'square',
- priority: 'middle',
- ratio: 1,
- url: '',
- imageUrl: null,
- expiresAt: null,
- });
-}
-
-function remove(ad) {
- os.confirm({
- type: 'warning',
- text: i18n.t('removeAreYouSure', { x: ad.url }),
- }).then(({ canceled }) => {
- if (canceled) return;
- ads = ads.filter(x => x !== ad);
- os.apiWithDialog('admin/ad/delete', {
- id: ad.id,
- });
- });
-}
-
-function save(ad) {
- if (ad.id == null) {
- os.apiWithDialog('admin/ad/create', {
- ...ad,
- expiresAt: new Date(ad.expiresAt).getTime(),
- });
- } else {
- os.apiWithDialog('admin/ad/update', {
- ...ad,
- expiresAt: new Date(ad.expiresAt).getTime(),
- });
- }
-}
-
-const headerActions = $computed(() => [{
- asFullButton: true,
- icon: 'ti ti-plus',
- text: i18n.ts.add,
- handler: add,
-}]);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.ads,
- icon: 'ti ti-ad',
-});
-</script>
-
-<style lang="scss" scoped>
-.uqshojas {
- > .ad {
- padding: 32px;
-
- &:not(:last-child) {
- margin-bottom: var(--margin);
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue
deleted file mode 100644
index 607ad8aa02..0000000000
--- a/packages/client/src/pages/admin/announcements.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="900">
- <div class="ztgjmzrw">
- <section v-for="announcement in announcements" class="_card _gap announcements">
- <div class="_content announcement">
- <MkInput v-model="announcement.title">
- <template #label>{{ i18n.ts.title }}</template>
- </MkInput>
- <MkTextarea v-model="announcement.text">
- <template #label>{{ i18n.ts.text }}</template>
- </MkTextarea>
- <MkInput v-model="announcement.imageUrl">
- <template #label>{{ i18n.ts.imageUrl }}</template>
- </MkInput>
- <p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
- <div class="buttons">
- <MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
- <MkButton class="button" inline @click="remove(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
- </div>
- </div>
- </section>
- </div>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XHeader from './_header_.vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/form/input.vue';
-import MkTextarea from '@/components/form/textarea.vue';
-import * as os from '@/os';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let announcements: any[] = $ref([]);
-
-os.api('admin/announcements/list').then(announcementResponse => {
- announcements = announcementResponse;
-});
-
-function add() {
- announcements.unshift({
- id: null,
- title: '',
- text: '',
- imageUrl: null,
- });
-}
-
-function remove(announcement) {
- os.confirm({
- type: 'warning',
- text: i18n.t('removeAreYouSure', { x: announcement.title }),
- }).then(({ canceled }) => {
- if (canceled) return;
- announcements = announcements.filter(x => x !== announcement);
- os.api('admin/announcements/delete', announcement);
- });
-}
-
-function save(announcement) {
- if (announcement.id == null) {
- os.api('admin/announcements/create', announcement).then(() => {
- os.alert({
- type: 'success',
- text: i18n.ts.saved,
- });
- }).catch(err => {
- os.alert({
- type: 'error',
- text: err,
- });
- });
- } else {
- os.api('admin/announcements/update', announcement).then(() => {
- os.alert({
- type: 'success',
- text: i18n.ts.saved,
- });
- }).catch(err => {
- os.alert({
- type: 'error',
- text: err,
- });
- });
- }
-}
-
-const headerActions = $computed(() => [{
- asFullButton: true,
- icon: 'ti ti-plus',
- text: i18n.ts.add,
- handler: add,
-}]);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.announcements,
- icon: 'ti ti-speakerphone',
-});
-</script>
-
-<style lang="scss" scoped>
-.ztgjmzrw {
- margin: var(--margin);
-}
-</style>
diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue
deleted file mode 100644
index d03961cf95..0000000000
--- a/packages/client/src/pages/admin/bot-protection.vue
+++ /dev/null
@@ -1,109 +0,0 @@
-<template>
-<div>
- <FormSuspense :p="init">
- <div class="_formRoot">
- <FormRadios v-model="provider" class="_formBlock">
- <option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
- <option value="hcaptcha">hCaptcha</option>
- <option value="recaptcha">reCAPTCHA</option>
- <option value="turnstile">Turnstile</option>
- </FormRadios>
-
- <template v-if="provider === 'hcaptcha'">
- <FormInput v-model="hcaptchaSiteKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
- </FormInput>
- <FormInput v-model="hcaptchaSecretKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
- </FormInput>
- <FormSlot class="_formBlock">
- <template #label>{{ i18n.ts.preview }}</template>
- <MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
- </FormSlot>
- </template>
- <template v-else-if="provider === 'recaptcha'">
- <FormInput v-model="recaptchaSiteKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
- </FormInput>
- <FormInput v-model="recaptchaSecretKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
- </FormInput>
- <FormSlot v-if="recaptchaSiteKey" class="_formBlock">
- <template #label>{{ i18n.ts.preview }}</template>
- <MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
- </FormSlot>
- </template>
- <template v-else-if="provider === 'turnstile'">
- <FormInput v-model="turnstileSiteKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.turnstileSiteKey }}</template>
- </FormInput>
- <FormInput v-model="turnstileSecretKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>{{ i18n.ts.turnstileSecretKey }}</template>
- </FormInput>
- <FormSlot class="_formBlock">
- <template #label>{{ i18n.ts.preview }}</template>
- <MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/>
- </FormSlot>
- </template>
-
- <FormButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
- </div>
- </FormSuspense>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
-import FormRadios from '@/components/form/radios.vue';
-import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import FormSlot from '@/components/form/slot.vue';
-import * as os from '@/os';
-import { fetchInstance } from '@/instance';
-import { i18n } from '@/i18n';
-
-const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
-
-let provider = $ref(null);
-let hcaptchaSiteKey: string | null = $ref(null);
-let hcaptchaSecretKey: string | null = $ref(null);
-let recaptchaSiteKey: string | null = $ref(null);
-let recaptchaSecretKey: string | null = $ref(null);
-let turnstileSiteKey: string | null = $ref(null);
-let turnstileSecretKey: string | null = $ref(null);
-
-async function init() {
- const meta = await os.api('admin/meta');
- hcaptchaSiteKey = meta.hcaptchaSiteKey;
- hcaptchaSecretKey = meta.hcaptchaSecretKey;
- recaptchaSiteKey = meta.recaptchaSiteKey;
- recaptchaSecretKey = meta.recaptchaSecretKey;
- turnstileSiteKey = meta.turnstileSiteKey;
- turnstileSecretKey = meta.turnstileSecretKey;
-
- provider = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : meta.enableTurnstile ? 'turnstile' : null;
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta', {
- enableHcaptcha: provider === 'hcaptcha',
- hcaptchaSiteKey,
- hcaptchaSecretKey,
- enableRecaptcha: provider === 'recaptcha',
- recaptchaSiteKey,
- recaptchaSecretKey,
- enableTurnstile: provider === 'turnstile',
- turnstileSiteKey,
- turnstileSecretKey,
- }).then(() => {
- fetchInstance();
- });
-}
-</script>
diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue
deleted file mode 100644
index 5a0d3d5e51..0000000000
--- a/packages/client/src/pages/admin/database.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
- <FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
- <MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;">
- <template #key>{{ table[0] }}</template>
- <template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template>
- </MkKeyValue>
- </FormSuspense>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import MkKeyValue from '@/components/MkKeyValue.vue';
-import * as os from '@/os';
-import bytes from '@/filters/bytes';
-import number from '@/filters/number';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.database,
- icon: 'ti ti-database',
-});
-</script>
diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue
deleted file mode 100644
index 6c9dee1704..0000000000
--- a/packages/client/src/pages/admin/email-settings.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <div class="_formRoot">
- <FormSwitch v-model="enableEmail" class="_formBlock">
- <template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template>
- <template #caption>{{ i18n.ts.emailConfigInfo }}</template>
- </FormSwitch>
-
- <template v-if="enableEmail">
- <FormInput v-model="email" type="email" class="_formBlock">
- <template #label>{{ i18n.ts.emailAddress }}</template>
- </FormInput>
-
- <FormSection>
- <template #label>{{ i18n.ts.smtpConfig }}</template>
- <FormSplit :min-width="280">
- <FormInput v-model="smtpHost" class="_formBlock">
- <template #label>{{ i18n.ts.smtpHost }}</template>
- </FormInput>
- <FormInput v-model="smtpPort" type="number" class="_formBlock">
- <template #label>{{ i18n.ts.smtpPort }}</template>
- </FormInput>
- </FormSplit>
- <FormSplit :min-width="280">
- <FormInput v-model="smtpUser" class="_formBlock">
- <template #label>{{ i18n.ts.smtpUser }}</template>
- </FormInput>
- <FormInput v-model="smtpPass" type="password" class="_formBlock">
- <template #label>{{ i18n.ts.smtpPass }}</template>
- </FormInput>
- </FormSplit>
- <FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
- <FormSwitch v-model="smtpSecure" class="_formBlock">
- <template #label>{{ i18n.ts.smtpSecure }}</template>
- <template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
- </FormSwitch>
- </FormSection>
- </template>
- </div>
- </FormSuspense>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XHeader from './_header_.vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormInput from '@/components/form/input.vue';
-import FormInfo from '@/components/MkInfo.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import FormSplit from '@/components/form/split.vue';
-import FormSection from '@/components/form/section.vue';
-import * as os from '@/os';
-import { fetchInstance, instance } from '@/instance';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let enableEmail: boolean = $ref(false);
-let email: any = $ref(null);
-let smtpSecure: boolean = $ref(false);
-let smtpHost: string = $ref('');
-let smtpPort: number = $ref(0);
-let smtpUser: string = $ref('');
-let smtpPass: string = $ref('');
-
-async function init() {
- const meta = await os.api('admin/meta');
- enableEmail = meta.enableEmail;
- email = meta.email;
- smtpSecure = meta.smtpSecure;
- smtpHost = meta.smtpHost;
- smtpPort = meta.smtpPort;
- smtpUser = meta.smtpUser;
- smtpPass = meta.smtpPass;
-}
-
-async function testEmail() {
- const { canceled, result: destination } = await os.inputText({
- title: i18n.ts.destination,
- type: 'email',
- placeholder: instance.maintainerEmail,
- });
- if (canceled) return;
- os.apiWithDialog('admin/send-email', {
- to: destination,
- subject: 'Test email',
- text: 'Yo',
- });
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta', {
- enableEmail,
- email,
- smtpSecure,
- smtpHost,
- smtpPort,
- smtpUser,
- smtpPass,
- }).then(() => {
- fetchInstance();
- });
-}
-
-const headerActions = $computed(() => [{
- asFullButton: true,
- text: i18n.ts.testEmail,
- handler: testEmail,
-}, {
- asFullButton: true,
- icon: 'ti ti-check',
- text: i18n.ts.save,
- handler: save,
-}]);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.emailServer,
- icon: 'ti ti-mail',
-});
-</script>
diff --git a/packages/client/src/pages/admin/emoji-edit-dialog.vue b/packages/client/src/pages/admin/emoji-edit-dialog.vue
deleted file mode 100644
index bd601cb1de..0000000000
--- a/packages/client/src/pages/admin/emoji-edit-dialog.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<template>
-<XModalWindow
- ref="dialog"
- :width="370"
- :with-ok-button="true"
- @close="$refs.dialog.close()"
- @closed="$emit('closed')"
- @ok="ok()"
->
- <template #header>:{{ emoji.name }}:</template>
-
- <div class="_monolithic_">
- <div class="yigymqpb _section">
- <img :src="emoji.url" class="img"/>
- <MkInput v-model="name" class="_formBlock">
- <template #label>{{ i18n.ts.name }}</template>
- </MkInput>
- <MkInput v-model="category" class="_formBlock" :datalist="categories">
- <template #label>{{ i18n.ts.category }}</template>
- </MkInput>
- <MkInput v-model="aliases" class="_formBlock">
- <template #label>{{ i18n.ts.tags }}</template>
- <template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
- </MkInput>
- <MkButton danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
- </div>
- </div>
-</XModalWindow>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XModalWindow from '@/components/MkModalWindow.vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/form/input.vue';
-import * as os from '@/os';
-import { unique } from '@/scripts/array';
-import { i18n } from '@/i18n';
-import { emojiCategories } from '@/instance';
-
-const props = defineProps<{
- emoji: any,
-}>();
-
-let dialog = $ref(null);
-let name: string = $ref(props.emoji.name);
-let category: string = $ref(props.emoji.category);
-let aliases: string = $ref(props.emoji.aliases.join(' '));
-let categories: string[] = $ref(emojiCategories);
-
-const emit = defineEmits<{
- (ev: 'done', v: { deleted?: boolean, updated?: any }): void,
- (ev: 'closed'): void
-}>();
-
-function ok() {
- update();
-}
-
-async function update() {
- await os.apiWithDialog('admin/emoji/update', {
- id: props.emoji.id,
- name,
- category,
- aliases: aliases.split(' '),
- });
-
- emit('done', {
- updated: {
- id: props.emoji.id,
- name,
- category,
- aliases: aliases.split(' '),
- },
- });
-
- dialog.close();
-}
-
-async function del() {
- const { canceled } = await os.confirm({
- type: 'warning',
- text: i18n.t('removeAreYouSure', { x: name }),
- });
- if (canceled) return;
-
- os.api('admin/emoji/delete', {
- id: props.emoji.id,
- }).then(() => {
- emit('done', {
- deleted: true,
- });
- dialog.close();
- });
-}
-</script>
-
-<style lang="scss" scoped>
-.yigymqpb {
- > .img {
- display: block;
- height: 64px;
- margin: 0 auto;
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
deleted file mode 100644
index 14c8466d73..0000000000
--- a/packages/client/src/pages/admin/emojis.vue
+++ /dev/null
@@ -1,398 +0,0 @@
-<template>
-<div>
- <MkStickyContainer>
- <template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="900">
- <div class="ogwlenmc">
- <div v-if="tab === 'local'" class="local">
- <MkInput v-model="query" :debounce="true" type="search">
- <template #prefix><i class="ti ti-search"></i></template>
- <template #label>{{ i18n.ts.search }}</template>
- </MkInput>
- <MkSwitch v-model="selectMode" style="margin: 8px 0;">
- <template #label>Select mode</template>
- </MkSwitch>
- <div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
- <MkButton inline @click="selectAll">Select all</MkButton>
- <MkButton inline @click="setCategoryBulk">Set category</MkButton>
- <MkButton inline @click="addTagBulk">Add tag</MkButton>
- <MkButton inline @click="removeTagBulk">Remove tag</MkButton>
- <MkButton inline @click="setTagBulk">Set tag</MkButton>
- <MkButton inline danger @click="delBulk">Delete</MkButton>
- </div>
- <MkPagination ref="emojisPaginationComponent" :pagination="pagination">
- <template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
- <template #default="{items}">
- <div class="ldhfsamy">
- <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
- <img :src="emoji.url" class="img" :alt="emoji.name"/>
- <div class="body">
- <div class="name _monospace">{{ emoji.name }}</div>
- <div class="info">{{ emoji.category }}</div>
- </div>
- </button>
- </div>
- </template>
- </MkPagination>
- </div>
-
- <div v-else-if="tab === 'remote'" class="remote">
- <FormSplit>
- <MkInput v-model="queryRemote" :debounce="true" type="search">
- <template #prefix><i class="ti ti-search"></i></template>
- <template #label>{{ i18n.ts.search }}</template>
- </MkInput>
- <MkInput v-model="host" :debounce="true">
- <template #label>{{ i18n.ts.host }}</template>
- </MkInput>
- </FormSplit>
- <MkPagination :pagination="remotePagination">
- <template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
- <template #default="{items}">
- <div class="ldhfsamy">
- <div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
- <img :src="emoji.url" class="img" :alt="emoji.name"/>
- <div class="body">
- <div class="name _monospace">{{ emoji.name }}</div>
- <div class="info">{{ emoji.host }}</div>
- </div>
- </div>
- </div>
- </template>
- </MkPagination>
- </div>
- </div>
- </MkSpacer>
- </MkStickyContainer>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue';
-import XHeader from './_header_.vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/form/input.vue';
-import MkPagination from '@/components/MkPagination.vue';
-import MkTab from '@/components/MkTab.vue';
-import MkSwitch from '@/components/form/switch.vue';
-import FormSplit from '@/components/form/split.vue';
-import { selectFile, selectFiles } from '@/scripts/select-file';
-import * as os from '@/os';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
-
-const tab = ref('local');
-const query = ref(null);
-const queryRemote = ref(null);
-const host = ref(null);
-const selectMode = ref(false);
-const selectedEmojis = ref<string[]>([]);
-
-const pagination = {
- endpoint: 'admin/emoji/list' as const,
- limit: 30,
- params: computed(() => ({
- query: (query.value && query.value !== '') ? query.value : null,
- })),
-};
-
-const remotePagination = {
- endpoint: 'admin/emoji/list-remote' as const,
- limit: 30,
- params: computed(() => ({
- query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null,
- host: (host.value && host.value !== '') ? host.value : null,
- })),
-};
-
-const selectAll = () => {
- if (selectedEmojis.value.length > 0) {
- selectedEmojis.value = [];
- } else {
- selectedEmojis.value = emojisPaginationComponent.value.items.map(item => item.id);
- }
-};
-
-const toggleSelect = (emoji) => {
- if (selectedEmojis.value.includes(emoji.id)) {
- selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id);
- } else {
- selectedEmojis.value.push(emoji.id);
- }
-};
-
-const add = async (ev: MouseEvent) => {
- const files = await selectFiles(ev.currentTarget ?? ev.target, null);
-
- const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
- fileId: file.id,
- })));
- promise.then(() => {
- emojisPaginationComponent.value.reload();
- });
- os.promiseDialog(promise);
-};
-
-const edit = (emoji) => {
- os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
- emoji: emoji,
- }, {
- done: result => {
- if (result.updated) {
- emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
- ...oldEmoji,
- ...result.updated,
- }));
- } else if (result.deleted) {
- emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
- }
- },
- }, 'closed');
-};
-
-const im = (emoji) => {
- os.apiWithDialog('admin/emoji/copy', {
- emojiId: emoji.id,
- });
-};
-
-const remoteMenu = (emoji, ev: MouseEvent) => {
- os.popupMenu([{
- type: 'label',
- text: ':' + emoji.name + ':',
- }, {
- text: i18n.ts.import,
- icon: 'ti ti-plus',
- action: () => { im(emoji); },
- }], ev.currentTarget ?? ev.target);
-};
-
-const menu = (ev: MouseEvent) => {
- os.popupMenu([{
- icon: 'ti ti-download',
- text: i18n.ts.export,
- action: async () => {
- os.api('export-custom-emojis', {
- })
- .then(() => {
- os.alert({
- type: 'info',
- text: i18n.ts.exportRequested,
- });
- }).catch((err) => {
- os.alert({
- type: 'error',
- text: err.message,
- });
- });
- },
- }, {
- icon: 'ti ti-upload',
- text: i18n.ts.import,
- action: async () => {
- const file = await selectFile(ev.currentTarget ?? ev.target);
- os.api('admin/emoji/import-zip', {
- fileId: file.id,
- })
- .then(() => {
- os.alert({
- type: 'info',
- text: i18n.ts.importRequested,
- });
- }).catch((err) => {
- os.alert({
- type: 'error',
- text: err.message,
- });
- });
- },
- }], ev.currentTarget ?? ev.target);
-};
-
-const setCategoryBulk = async () => {
- const { canceled, result } = await os.inputText({
- title: 'Category',
- });
- if (canceled) return;
- await os.apiWithDialog('admin/emoji/set-category-bulk', {
- ids: selectedEmojis.value,
- category: result,
- });
- emojisPaginationComponent.value.reload();
-};
-
-const addTagBulk = async () => {
- const { canceled, result } = await os.inputText({
- title: 'Tag',
- });
- if (canceled) return;
- await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
- ids: selectedEmojis.value,
- aliases: result.split(' '),
- });
- emojisPaginationComponent.value.reload();
-};
-
-const removeTagBulk = async () => {
- const { canceled, result } = await os.inputText({
- title: 'Tag',
- });
- if (canceled) return;
- await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
- ids: selectedEmojis.value,
- aliases: result.split(' '),
- });
- emojisPaginationComponent.value.reload();
-};
-
-const setTagBulk = async () => {
- const { canceled, result } = await os.inputText({
- title: 'Tag',
- });
- if (canceled) return;
- await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
- ids: selectedEmojis.value,
- aliases: result.split(' '),
- });
- emojisPaginationComponent.value.reload();
-};
-
-const delBulk = async () => {
- const { canceled } = await os.confirm({
- type: 'warning',
- text: i18n.ts.deleteConfirm,
- });
- if (canceled) return;
- await os.apiWithDialog('admin/emoji/delete-bulk', {
- ids: selectedEmojis.value,
- });
- emojisPaginationComponent.value.reload();
-};
-
-const headerActions = $computed(() => [{
- asFullButton: true,
- icon: 'ti ti-plus',
- text: i18n.ts.addEmoji,
- handler: add,
-}, {
- icon: 'ti ti-dots',
- handler: menu,
-}]);
-
-const headerTabs = $computed(() => [{
- key: 'local',
- title: i18n.ts.local,
-}, {
- key: 'remote',
- title: i18n.ts.remote,
-}]);
-
-definePageMetadata(computed(() => ({
- title: i18n.ts.customEmojis,
- icon: 'ti ti-mood-happy',
-})));
-</script>
-
-<style lang="scss" scoped>
-.ogwlenmc {
- > .local {
- .empty {
- margin: var(--margin);
- }
-
- .ldhfsamy {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
- grid-gap: 12px;
- margin: var(--margin) 0;
-
- > .emoji {
- display: flex;
- align-items: center;
- padding: 11px;
- text-align: left;
- border: solid 1px var(--panel);
-
- &:hover {
- border-color: var(--inputBorderHover);
- }
-
- &.selected {
- border-color: var(--accent);
- }
-
- > .img {
- width: 42px;
- height: 42px;
- }
-
- > .body {
- padding: 0 0 0 8px;
- white-space: nowrap;
- overflow: hidden;
-
- > .name {
- text-overflow: ellipsis;
- overflow: hidden;
- }
-
- > .info {
- opacity: 0.5;
- text-overflow: ellipsis;
- overflow: hidden;
- }
- }
- }
- }
- }
-
- > .remote {
- .empty {
- margin: var(--margin);
- }
-
- .ldhfsamy {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
- grid-gap: 12px;
- margin: var(--margin) 0;
-
- > .emoji {
- display: flex;
- align-items: center;
- padding: 12px;
- text-align: left;
-
- &:hover {
- color: var(--accent);
- }
-
- > .img {
- width: 32px;
- height: 32px;
- }
-
- > .body {
- padding: 0 0 0 8px;
- white-space: nowrap;
- overflow: hidden;
-
- > .name {
- text-overflow: ellipsis;
- overflow: hidden;
- }
-
- > .info {
- opacity: 0.5;
- font-size: 90%;
- text-overflow: ellipsis;
- overflow: hidden;
- }
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue
deleted file mode 100644
index 8ad6bd4fc0..0000000000
--- a/packages/client/src/pages/admin/files.vue
+++ /dev/null
@@ -1,120 +0,0 @@
-<template>
-<div>
- <MkStickyContainer>
- <template #header><XHeader :actions="headerActions"/></template>
- <MkSpacer :content-max="900">
- <div class="xrmjdkdw">
- <div>
- <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
- <MkSelect v-model="origin" style="margin: 0; flex: 1;">
- <template #label>{{ i18n.ts.instance }}</template>
- <option value="combined">{{ i18n.ts.all }}</option>
- <option value="local">{{ i18n.ts.local }}</option>
- <option value="remote">{{ i18n.ts.remote }}</option>
- </MkSelect>
- <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
- <template #label>{{ i18n.ts.host }}</template>
- </MkInput>
- </div>
- <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap; padding-top: 1.2em;">
- <MkInput v-model="userId" :debounce="true" type="search" style="margin: 0; flex: 1;">
- <template #label>User ID</template>
- </MkInput>
- <MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
- <template #label>MIME type</template>
- </MkInput>
- </div>
- <MkFileListForAdmin :pagination="pagination" :view-mode="viewMode"/>
- </div>
- </div>
- </MkSpacer>
- </MkStickyContainer>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { computed, defineAsyncComponent } from 'vue';
-import * as Acct from 'misskey-js/built/acct';
-import XHeader from './_header_.vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/form/input.vue';
-import MkSelect from '@/components/form/select.vue';
-import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
-import bytes from '@/filters/bytes';
-import * as os from '@/os';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let origin = $ref('local');
-let type = $ref(null);
-let searchHost = $ref('');
-let userId = $ref('');
-let viewMode = $ref('grid');
-const pagination = {
- endpoint: 'admin/drive/files' as const,
- limit: 10,
- params: computed(() => ({
- type: (type && type !== '') ? type : null,
- userId: (userId && userId !== '') ? userId : null,
- origin: origin,
- hostname: (searchHost && searchHost !== '') ? searchHost : null,
- })),
-};
-
-function clear() {
- os.confirm({
- type: 'warning',
- text: i18n.ts.clearCachedFilesConfirm,
- }).then(({ canceled }) => {
- if (canceled) return;
-
- os.apiWithDialog('admin/drive/clean-remote-files', {});
- });
-}
-
-function show(file) {
- os.pageWindow(`/admin/file/${file.id}`);
-}
-
-async function find() {
- const { canceled, result: q } = await os.inputText({
- title: i18n.ts.fileIdOrUrl,
- allowEmpty: false,
- });
- if (canceled) return;
-
- os.api('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
- show(file);
- }).catch(err => {
- if (err.code === 'NO_SUCH_FILE') {
- os.alert({
- type: 'error',
- text: i18n.ts.notFound,
- });
- }
- });
-}
-
-const headerActions = $computed(() => [{
- text: i18n.ts.lookup,
- icon: 'ti ti-search',
- handler: find,
-}, {
- text: i18n.ts.clearCachedFiles,
- icon: 'ti ti-trash',
- handler: clear,
-}]);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata(computed(() => ({
- title: i18n.ts.files,
- icon: 'ti ti-cloud',
-})));
-</script>
-
-<style lang="scss" scoped>
-.xrmjdkdw {
- margin: var(--margin);
-}
-</style>
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
deleted file mode 100644
index 6c07a87eeb..0000000000
--- a/packages/client/src/pages/admin/index.vue
+++ /dev/null
@@ -1,316 +0,0 @@
-<template>
-<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
- <div v-if="!narrow || currentPage?.route.name == null" class="nav">
- <MkSpacer :content-max="700" :margin-min="16">
- <div class="lxpfedzu">
- <div class="banner">
- <img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
- </div>
-
- <MkInfo v-if="thereIsUnresolvedAbuseReport" warn class="info">{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
- <MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
- <MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
- <MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
-
- <MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu>
- </div>
- </MkSpacer>
- </div>
- <div v-if="!(narrow && currentPage?.route.name == null)" class="main">
- <RouterView/>
- </div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
-import { i18n } from '@/i18n';
-import MkSuperMenu from '@/components/MkSuperMenu.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import { scroll } from '@/scripts/scroll';
-import { instance } from '@/instance';
-import * as os from '@/os';
-import { lookupUser } from '@/scripts/lookup-user';
-import { useRouter } from '@/router';
-import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
-
-const isEmpty = (x: string | null) => x == null || x === '';
-
-const router = useRouter();
-
-const indexInfo = {
- title: i18n.ts.controlPanel,
- icon: 'ti ti-settings',
- hideHeader: true,
-};
-
-provide('shouldOmitHeaderTitle', false);
-
-let INFO = $ref(indexInfo);
-let childInfo = $ref(null);
-let narrow = $ref(false);
-let view = $ref(null);
-let el = $ref(null);
-let pageProps = $ref({});
-let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
-let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile;
-let noEmailServer = !instance.enableEmail;
-let thereIsUnresolvedAbuseReport = $ref(false);
-let currentPage = $computed(() => router.currentRef.value.child);
-
-os.api('admin/abuse-user-reports', {
- state: 'unresolved',
- limit: 1,
-}).then(reports => {
- if (reports.length > 0) thereIsUnresolvedAbuseReport = true;
-});
-
-const NARROW_THRESHOLD = 600;
-const ro = new ResizeObserver((entries, observer) => {
- if (entries.length === 0) return;
- narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
-});
-
-const menuDef = $computed(() => [{
- title: i18n.ts.quickAction,
- items: [{
- type: 'button',
- icon: 'ti ti-search',
- text: i18n.ts.lookup,
- action: lookup,
- }, ...(instance.disableRegistration ? [{
- type: 'button',
- icon: 'ti ti-user',
- text: i18n.ts.invite,
- action: invite,
- }] : [])],
-}, {
- title: i18n.ts.administration,
- items: [{
- icon: 'ti ti-dashboard',
- text: i18n.ts.dashboard,
- to: '/admin/overview',
- active: currentPage?.route.name === 'overview',
- }, {
- icon: 'ti ti-users',
- text: i18n.ts.users,
- to: '/admin/users',
- active: currentPage?.route.name === 'users',
- }, {
- icon: 'ti ti-mood-happy',
- text: i18n.ts.customEmojis,
- to: '/admin/emojis',
- active: currentPage?.route.name === 'emojis',
- }, {
- icon: 'ti ti-whirl',
- text: i18n.ts.federation,
- to: '/about#federation',
- active: currentPage?.route.name === 'federation',
- }, {
- icon: 'ti ti-clock-play',
- text: i18n.ts.jobQueue,
- to: '/admin/queue',
- active: currentPage?.route.name === 'queue',
- }, {
- icon: 'ti ti-cloud',
- text: i18n.ts.files,
- to: '/admin/files',
- active: currentPage?.route.name === 'files',
- }, {
- icon: 'ti ti-speakerphone',
- text: i18n.ts.announcements,
- to: '/admin/announcements',
- active: currentPage?.route.name === 'announcements',
- }, {
- icon: 'ti ti-ad',
- text: i18n.ts.ads,
- to: '/admin/ads',
- active: currentPage?.route.name === 'ads',
- }, {
- icon: 'ti ti-exclamation-circle',
- text: i18n.ts.abuseReports,
- to: '/admin/abuses',
- active: currentPage?.route.name === 'abuses',
- }],
-}, {
- title: i18n.ts.settings,
- items: [{
- icon: 'ti ti-settings',
- text: i18n.ts.general,
- to: '/admin/settings',
- active: currentPage?.route.name === 'settings',
- }, {
- icon: 'ti ti-mail',
- text: i18n.ts.emailServer,
- to: '/admin/email-settings',
- active: currentPage?.route.name === 'email-settings',
- }, {
- icon: 'ti ti-cloud',
- text: i18n.ts.objectStorage,
- to: '/admin/object-storage',
- active: currentPage?.route.name === 'object-storage',
- }, {
- icon: 'ti ti-lock',
- text: i18n.ts.security,
- to: '/admin/security',
- active: currentPage?.route.name === 'security',
- }, {
- icon: 'ti ti-planet',
- text: i18n.ts.relays,
- to: '/admin/relays',
- active: currentPage?.route.name === 'relays',
- }, {
- icon: 'ti ti-share',
- text: i18n.ts.integration,
- to: '/admin/integrations',
- active: currentPage?.route.name === 'integrations',
- }, {
- icon: 'ti ti-ban',
- text: i18n.ts.instanceBlocking,
- to: '/admin/instance-block',
- active: currentPage?.route.name === 'instance-block',
- }, {
- icon: 'ti ti-ghost',
- text: i18n.ts.proxyAccount,
- to: '/admin/proxy-account',
- active: currentPage?.route.name === 'proxy-account',
- }, {
- icon: 'ti ti-adjustments',
- text: i18n.ts.other,
- to: '/admin/other-settings',
- active: currentPage?.route.name === 'other-settings',
- }],
-}, {
- title: i18n.ts.info,
- items: [{
- icon: 'ti ti-database',
- text: i18n.ts.database,
- to: '/admin/database',
- active: currentPage?.route.name === 'database',
- }],
-}]);
-
-watch(narrow, () => {
- if (currentPage?.route.name == null && !narrow) {
- router.push('/admin/overview');
- }
-});
-
-onMounted(() => {
- ro.observe(el);
-
- narrow = el.offsetWidth < NARROW_THRESHOLD;
- if (currentPage?.route.name == null && !narrow) {
- router.push('/admin/overview');
- }
-});
-
-onUnmounted(() => {
- ro.disconnect();
-});
-
-provideMetadataReceiver((info) => {
- if (info == null) {
- childInfo = null;
- } else {
- childInfo = info;
- }
-});
-
-const invite = () => {
- os.api('admin/invite').then(x => {
- os.alert({
- type: 'info',
- text: x.code,
- });
- }).catch(err => {
- os.alert({
- type: 'error',
- text: err,
- });
- });
-};
-
-const lookup = (ev) => {
- os.popupMenu([{
- text: i18n.ts.user,
- icon: 'ti ti-user',
- action: () => {
- lookupUser();
- },
- }, {
- text: i18n.ts.note,
- icon: 'ti ti-pencil',
- action: () => {
- alert('TODO');
- },
- }, {
- text: i18n.ts.file,
- icon: 'ti ti-cloud',
- action: () => {
- alert('TODO');
- },
- }, {
- text: i18n.ts.instance,
- icon: 'ti ti-planet',
- action: () => {
- alert('TODO');
- },
- }], ev.currentTarget ?? ev.target);
-};
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata(INFO);
-
-defineExpose({
- header: {
- title: i18n.ts.controlPanel,
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.hiyeyicy {
- &.wide {
- display: flex;
- margin: 0 auto;
- height: 100%;
-
- > .nav {
- width: 32%;
- max-width: 280px;
- box-sizing: border-box;
- border-right: solid 0.5px var(--divider);
- overflow: auto;
- height: 100%;
- }
-
- > .main {
- flex: 1;
- min-width: 0;
- }
- }
-
- > .nav {
- .lxpfedzu {
- > .info {
- margin: 16px 0;
- }
-
- > .banner {
- margin: 16px;
-
- > .icon {
- display: block;
- margin: auto;
- height: 42px;
- border-radius: 8px;
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue
deleted file mode 100644
index 1bdd174de4..0000000000
--- a/packages/client/src/pages/admin/instance-block.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <FormTextarea v-model="blockedHosts" class="_formBlock">
- <span>{{ i18n.ts.blockedInstances }}</span>
- <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
- </FormTextarea>
-
- <FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
- </FormSuspense>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XHeader from './_header_.vue';
-import FormButton from '@/components/MkButton.vue';
-import FormTextarea from '@/components/form/textarea.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import * as os from '@/os';
-import { fetchInstance } from '@/instance';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let blockedHosts: string = $ref('');
-
-async function init() {
- const meta = await os.api('admin/meta');
- blockedHosts = meta.blockedHosts.join('\n');
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta', {
- blockedHosts: blockedHosts.split('\n') || [],
- }).then(() => {
- fetchInstance();
- });
-}
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.instanceBlocking,
- icon: 'ti ti-ban',
-});
-</script>
diff --git a/packages/client/src/pages/admin/integrations.discord.vue b/packages/client/src/pages/admin/integrations.discord.vue
deleted file mode 100644
index 0a69c44c93..0000000000
--- a/packages/client/src/pages/admin/integrations.discord.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-<FormSuspense :p="init">
- <div class="_formRoot">
- <FormSwitch v-model="enableDiscordIntegration" class="_formBlock">
- <template #label>{{ i18n.ts.enable }}</template>
- </FormSwitch>
-
- <template v-if="enableDiscordIntegration">
- <FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
-
- <FormInput v-model="discordClientId" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Client ID</template>
- </FormInput>
-
- <FormInput v-model="discordClientSecret" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Client Secret</template>
- </FormInput>
- </template>
-
- <FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
- </div>
-</FormSuspense>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
-import FormInfo from '@/components/MkInfo.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import * as os from '@/os';
-import { fetchInstance } from '@/instance';
-import { i18n } from '@/i18n';
-
-let uri: string = $ref('');
-let enableDiscordIntegration: boolean = $ref(false);
-let discordClientId: string | null = $ref(null);
-let discordClientSecret: string | null = $ref(null);
-
-async function init() {
- const meta = await os.api('admin/meta');
- uri = meta.uri;
- enableDiscordIntegration = meta.enableDiscordIntegration;
- discordClientId = meta.discordClientId;
- discordClientSecret = meta.discordClientSecret;
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta', {
- enableDiscordIntegration,
- discordClientId,
- discordClientSecret,
- }).then(() => {
- fetchInstance();
- });
-}
-</script>
diff --git a/packages/client/src/pages/admin/integrations.github.vue b/packages/client/src/pages/admin/integrations.github.vue
deleted file mode 100644
index 66419d5891..0000000000
--- a/packages/client/src/pages/admin/integrations.github.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-<FormSuspense :p="init">
- <div class="_formRoot">
- <FormSwitch v-model="enableGithubIntegration" class="_formBlock">
- <template #label>{{ i18n.ts.enable }}</template>
- </FormSwitch>
-
- <template v-if="enableGithubIntegration">
- <FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo>
-
- <FormInput v-model="githubClientId" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Client ID</template>
- </FormInput>
-
- <FormInput v-model="githubClientSecret" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Client Secret</template>
- </FormInput>
- </template>
-
- <FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
- </div>
-</FormSuspense>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
-import FormInfo from '@/components/MkInfo.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import * as os from '@/os';
-import { fetchInstance } from '@/instance';
-import { i18n } from '@/i18n';
-
-let uri: string = $ref('');
-let enableGithubIntegration: boolean = $ref(false);
-let githubClientId: string | null = $ref(null);
-let githubClientSecret: string | null = $ref(null);
-
-async function init() {
- const meta = await os.api('admin/meta');
- uri = meta.uri;
- enableGithubIntegration = meta.enableGithubIntegration;
- githubClientId = meta.githubClientId;
- githubClientSecret = meta.githubClientSecret;
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta', {
- enableGithubIntegration,
- githubClientId,
- githubClientSecret,
- }).then(() => {
- fetchInstance();
- });
-}
-</script>
diff --git a/packages/client/src/pages/admin/integrations.twitter.vue b/packages/client/src/pages/admin/integrations.twitter.vue
deleted file mode 100644
index 1e8d882b9c..0000000000
--- a/packages/client/src/pages/admin/integrations.twitter.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-<FormSuspense :p="init">
- <div class="_formRoot">
- <FormSwitch v-model="enableTwitterIntegration" class="_formBlock">
- <template #label>{{ i18n.ts.enable }}</template>
- </FormSwitch>
-
- <template v-if="enableTwitterIntegration">
- <FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo>
-
- <FormInput v-model="twitterConsumerKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Consumer Key</template>
- </FormInput>
-
- <FormInput v-model="twitterConsumerSecret" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Consumer Secret</template>
- </FormInput>
- </template>
-
- <FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
- </div>
-</FormSuspense>
-</template>
-
-<script lang="ts" setup>
-import { defineComponent } from 'vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
-import FormInfo from '@/components/MkInfo.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import * as os from '@/os';
-import { fetchInstance } from '@/instance';
-import { i18n } from '@/i18n';
-
-let uri: string = $ref('');
-let enableTwitterIntegration: boolean = $ref(false);
-let twitterConsumerKey: string | null = $ref(null);
-let twitterConsumerSecret: string | null = $ref(null);
-
-async function init() {
- const meta = await os.api('admin/meta');
- uri = meta.uri;
- enableTwitterIntegration = meta.enableTwitterIntegration;
- twitterConsumerKey = meta.twitterConsumerKey;
- twitterConsumerSecret = meta.twitterConsumerSecret;
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta', {
- enableTwitterIntegration,
- twitterConsumerKey,
- twitterConsumerSecret,
- }).then(() => {
- fetchInstance();
- });
-}
-</script>
diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue
deleted file mode 100644
index 9cc35baefd..0000000000
--- a/packages/client/src/pages/admin/integrations.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<template><MkStickyContainer>
- <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <FormFolder class="_formBlock">
- <template #icon><i class="ti ti-brand-twitter"></i></template>
- <template #label>Twitter</template>
- <template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
- <XTwitter/>
- </FormFolder>
- <FormFolder class="_formBlock">
- <template #icon><i class="ti ti-brand-github"></i></template>
- <template #label>GitHub</template>
- <template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
- <XGithub/>
- </FormFolder>
- <FormFolder class="_formBlock">
- <template #icon><i class="ti ti-brand-discord"></i></template>
- <template #label>Discord</template>
- <template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
- <XDiscord/>
- </FormFolder>
- </FormSuspense>
-</MkSpacer></MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XTwitter from './integrations.twitter.vue';
-import XGithub from './integrations.github.vue';
-import XDiscord from './integrations.discord.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import FormFolder from '@/components/form/folder.vue';
-import * as os from '@/os';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let enableTwitterIntegration: boolean = $ref(false);
-let enableGithubIntegration: boolean = $ref(false);
-let enableDiscordIntegration: boolean = $ref(false);
-
-async function init() {
- const meta = await os.api('admin/meta');
- enableTwitterIntegration = meta.enableTwitterIntegration;
- enableGithubIntegration = meta.enableGithubIntegration;
- enableDiscordIntegration = meta.enableDiscordIntegration;
-}
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.integration,
- icon: 'ti ti-share',
-});
-</script>
diff --git a/packages/client/src/pages/admin/metrics.vue b/packages/client/src/pages/admin/metrics.vue
deleted file mode 100644
index db8e448639..0000000000
--- a/packages/client/src/pages/admin/metrics.vue
+++ /dev/null
@@ -1,472 +0,0 @@
-<template>
-<div class="_debobigegoItem">
- <div class="_debobigegoLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div>
- <div class="_debobigegoPanel xhexznfu">
- <div>
- <canvas :ref="cpumem"></canvas>
- </div>
- <div v-if="serverInfo">
- <div class="_table">
- <div class="_row">
- <div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
- <div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
- <div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
- </div>
- </div>
- </div>
- </div>
-</div>
-<div class="_debobigegoItem">
- <div class="_debobigegoLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div>
- <div class="_debobigegoPanel xhexznfu">
- <div>
- <canvas :ref="disk"></canvas>
- </div>
- <div v-if="serverInfo">
- <div class="_table">
- <div class="_row">
- <div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
- <div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
- <div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
- </div>
- </div>
- </div>
- </div>
-</div>
-<div class="_debobigegoItem">
- <div class="_debobigegoLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div>
- <div class="_debobigegoPanel xhexznfu">
- <div>
- <canvas :ref="net"></canvas>
- </div>
- <div v-if="serverInfo">
- <div class="_table">
- <div class="_row">
- <div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
- </div>
- </div>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import {
- Chart,
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
-} from 'chart.js';
-import MkButton from '@/components/MkButton.vue';
-import MkSelect from '@/components/form/select.vue';
-import MkInput from '@/components/form/input.vue';
-import MkContainer from '@/components/MkContainer.vue';
-import MkFolder from '@/components/MkFolder.vue';
-import MkwFederation from '../../widgets/federation.vue';
-import { version, url } from '@/config';
-import bytes from '@/filters/bytes';
-import number from '@/filters/number';
-
-Chart.register(
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
-);
-
-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})`;
-};
-import * as os from '@/os';
-import { stream } from '@/stream';
-
-export default defineComponent({
- components: {
- MkButton,
- MkSelect,
- MkInput,
- MkContainer,
- MkFolder,
- MkwFederation,
- },
-
- data() {
- return {
- version,
- url,
- stats: null,
- serverInfo: null,
- connection: null,
- queueConnection: markRaw(stream.useChannel('queueStats')),
- memUsage: 0,
- chartCpuMem: null,
- chartNet: null,
- jobs: [],
- logs: [],
- logLevel: 'all',
- logDomain: '',
- modLogs: [],
- dbInfo: null,
- overviewHeight: '1fr',
- queueHeight: '1fr',
- paused: false,
- };
- },
-
- computed: {
- gridColor() {
- // TODO: var(--panel)の色が暗いか明るいかで判定する
- return this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
- },
- },
-
- mounted() {
- this.fetchJobs();
-
- Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
- os.api('admin/server-info', {}).then(res => {
- this.serverInfo = res;
-
- this.connection = markRaw(stream.useChannel('serverStats'));
- this.connection.on('stats', this.onStats);
- this.connection.on('statsLog', this.onStatsLog);
- this.connection.send('requestLog', {
- id: Math.random().toString().substr(2, 8),
- length: 150,
- });
-
- this.$nextTick(() => {
- this.queueConnection.send('requestLog', {
- id: Math.random().toString().substr(2, 8),
- length: 200,
- });
- });
- });
- },
-
- beforeUnmount() {
- if (this.connection) {
- this.connection.off('stats', this.onStats);
- this.connection.off('statsLog', this.onStatsLog);
- this.connection.dispose();
- }
- this.queueConnection.dispose();
- },
-
- methods: {
- cpumem(el) {
- if (this.chartCpuMem != null) return;
- this.chartCpuMem = markRaw(new Chart(el, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: 'CPU',
- pointRadius: 0,
- tension: 0,
- borderWidth: 2,
- borderColor: '#86b300',
- backgroundColor: alpha('#86b300', 0.1),
- data: [],
- }, {
- label: 'MEM (active)',
- pointRadius: 0,
- tension: 0,
- borderWidth: 2,
- borderColor: '#935dbf',
- backgroundColor: alpha('#935dbf', 0.02),
- data: [],
- }, {
- label: 'MEM (used)',
- pointRadius: 0,
- tension: 0,
- borderWidth: 2,
- borderColor: '#935dbf',
- borderDash: [5, 5],
- fill: false,
- data: [],
- }],
- },
- options: {
- aspectRatio: 3,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 0,
- },
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- },
- },
- scales: {
- x: {
- gridLines: {
- display: false,
- color: this.gridColor,
- zeroLineColor: this.gridColor,
- },
- ticks: {
- display: false,
- },
- },
- y: {
- position: 'right',
- gridLines: {
- display: true,
- color: this.gridColor,
- zeroLineColor: this.gridColor,
- },
- ticks: {
- display: false,
- max: 100,
- },
- },
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- },
- },
- }));
- },
-
- net(el) {
- if (this.chartNet != null) return;
- this.chartNet = markRaw(new Chart(el, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: 'In',
- pointRadius: 0,
- tension: 0,
- borderWidth: 2,
- borderColor: '#94a029',
- backgroundColor: alpha('#94a029', 0.1),
- data: [],
- }, {
- label: 'Out',
- pointRadius: 0,
- tension: 0,
- borderWidth: 2,
- borderColor: '#ff9156',
- backgroundColor: alpha('#ff9156', 0.1),
- data: [],
- }],
- },
- options: {
- aspectRatio: 3,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 0,
- },
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- },
- },
- scales: {
- x: {
- gridLines: {
- display: false,
- color: this.gridColor,
- zeroLineColor: this.gridColor,
- },
- ticks: {
- display: false,
- },
- },
- y: {
- position: 'right',
- gridLines: {
- display: true,
- color: this.gridColor,
- zeroLineColor: this.gridColor,
- },
- ticks: {
- display: false,
- },
- },
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- },
- },
- }));
- },
-
- disk(el) {
- if (this.chartDisk != null) return;
- this.chartDisk = markRaw(new Chart(el, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: 'Read',
- pointRadius: 0,
- tension: 0,
- borderWidth: 2,
- borderColor: '#94a029',
- backgroundColor: alpha('#94a029', 0.1),
- data: [],
- }, {
- label: 'Write',
- pointRadius: 0,
- tension: 0,
- borderWidth: 2,
- borderColor: '#ff9156',
- backgroundColor: alpha('#ff9156', 0.1),
- data: [],
- }],
- },
- options: {
- aspectRatio: 3,
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 0,
- },
- },
- legend: {
- position: 'bottom',
- labels: {
- boxWidth: 16,
- },
- },
- scales: {
- x: {
- gridLines: {
- display: false,
- color: this.gridColor,
- zeroLineColor: this.gridColor,
- },
- ticks: {
- display: false,
- },
- },
- y: {
- position: 'right',
- gridLines: {
- display: true,
- color: this.gridColor,
- zeroLineColor: this.gridColor,
- },
- ticks: {
- display: false,
- },
- },
- },
- tooltips: {
- intersect: false,
- mode: 'index',
- },
- },
- }));
- },
-
- fetchJobs() {
- os.api('admin/queue/deliver-delayed', {}).then(jobs => {
- this.jobs = jobs;
- });
- },
-
- onStats(stats) {
- if (this.paused) return;
-
- const cpu = (stats.cpu * 100).toFixed(0);
- const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
- const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
- this.memUsage = stats.mem.active;
-
- this.chartCpuMem.data.labels.push('');
- this.chartCpuMem.data.datasets[0].data.push(cpu);
- this.chartCpuMem.data.datasets[1].data.push(memActive);
- this.chartCpuMem.data.datasets[2].data.push(memUsed);
- this.chartNet.data.labels.push('');
- this.chartNet.data.datasets[0].data.push(stats.net.rx);
- this.chartNet.data.datasets[1].data.push(stats.net.tx);
- this.chartDisk.data.labels.push('');
- this.chartDisk.data.datasets[0].data.push(stats.fs.r);
- this.chartDisk.data.datasets[1].data.push(stats.fs.w);
- if (this.chartCpuMem.data.datasets[0].data.length > 150) {
- this.chartCpuMem.data.labels.shift();
- this.chartCpuMem.data.datasets[0].data.shift();
- this.chartCpuMem.data.datasets[1].data.shift();
- this.chartCpuMem.data.datasets[2].data.shift();
- this.chartNet.data.labels.shift();
- this.chartNet.data.datasets[0].data.shift();
- this.chartNet.data.datasets[1].data.shift();
- this.chartDisk.data.labels.shift();
- this.chartDisk.data.datasets[0].data.shift();
- this.chartDisk.data.datasets[1].data.shift();
- }
- this.chartCpuMem.update();
- this.chartNet.update();
- this.chartDisk.update();
- },
-
- onStatsLog(statsLog) {
- for (const stats of [...statsLog].reverse()) {
- this.onStats(stats);
- }
- },
-
- bytes,
-
- number,
-
- pause() {
- this.paused = true;
- },
-
- resume() {
- this.paused = false;
- },
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.xhexznfu {
- > div:nth-child(2) {
- padding: 16px;
- border-top: solid 0.5px var(--divider);
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue
deleted file mode 100644
index f2ab30eaa5..0000000000
--- a/packages/client/src/pages/admin/object-storage.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <div class="_formRoot">
- <FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
-
- <template v-if="useObjectStorage">
- <FormInput v-model="objectStorageBaseUrl" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
- <template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
- </FormInput>
-
- <FormInput v-model="objectStorageBucket" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageBucket }}</template>
- <template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
- </FormInput>
-
- <FormInput v-model="objectStoragePrefix" class="_formBlock">
- <template #label>{{ i18n.ts.objectStoragePrefix }}</template>
- <template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
- </FormInput>
-
- <FormInput v-model="objectStorageEndpoint" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
- <template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
- </FormInput>
-
- <FormInput v-model="objectStorageRegion" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageRegion }}</template>
- <template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
- </FormInput>
-
- <FormSplit :min-width="280">
- <FormInput v-model="objectStorageAccessKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Access key</template>
- </FormInput>
-
- <FormInput v-model="objectStorageSecretKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Secret key</template>
- </FormInput>
- </FormSplit>
-
- <FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
- <template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
- </FormSwitch>
-
- <FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
- <template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
- </FormSwitch>
-
- <FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
- <template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
- </FormSwitch>
-
- <FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
- <template #label>s3ForcePathStyle</template>
- </FormSwitch>
- </template>
- </div>
- </FormSuspense>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XHeader from './_header_.vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormInput from '@/components/form/input.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import FormSplit from '@/components/form/split.vue';
-import FormSection from '@/components/form/section.vue';
-import * as os from '@/os';
-import { fetchInstance } from '@/instance';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let useObjectStorage: boolean = $ref(false);
-let objectStorageBaseUrl: string | null = $ref(null);
-let objectStorageBucket: string | null = $ref(null);
-let objectStoragePrefix: string | null = $ref(null);
-let objectStorageEndpoint: string | null = $ref(null);
-let objectStorageRegion: string | null = $ref(null);
-let objectStoragePort: number | null = $ref(null);
-let objectStorageAccessKey: string | null = $ref(null);
-let objectStorageSecretKey: string | null = $ref(null);
-let objectStorageUseSSL: boolean = $ref(false);
-let objectStorageUseProxy: boolean = $ref(false);
-let objectStorageSetPublicRead: boolean = $ref(false);
-let objectStorageS3ForcePathStyle: boolean = $ref(true);
-
-async function init() {
- const meta = await os.api('admin/meta');
- useObjectStorage = meta.useObjectStorage;
- objectStorageBaseUrl = meta.objectStorageBaseUrl;
- objectStorageBucket = meta.objectStorageBucket;
- objectStoragePrefix = meta.objectStoragePrefix;
- objectStorageEndpoint = meta.objectStorageEndpoint;
- objectStorageRegion = meta.objectStorageRegion;
- objectStoragePort = meta.objectStoragePort;
- objectStorageAccessKey = meta.objectStorageAccessKey;
- objectStorageSecretKey = meta.objectStorageSecretKey;
- objectStorageUseSSL = meta.objectStorageUseSSL;
- objectStorageUseProxy = meta.objectStorageUseProxy;
- objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
- objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta', {
- useObjectStorage,
- objectStorageBaseUrl,
- objectStorageBucket,
- objectStoragePrefix,
- objectStorageEndpoint,
- objectStorageRegion,
- objectStoragePort,
- objectStorageAccessKey,
- objectStorageSecretKey,
- objectStorageUseSSL,
- objectStorageUseProxy,
- objectStorageSetPublicRead,
- objectStorageS3ForcePathStyle,
- }).then(() => {
- fetchInstance();
- });
-}
-
-const headerActions = $computed(() => [{
- asFullButton: true,
- icon: 'ti ti-check',
- text: i18n.ts.save,
- handler: save,
-}]);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.objectStorage,
- icon: 'ti ti-cloud',
-});
-</script>
diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue
deleted file mode 100644
index 62dff6ce7f..0000000000
--- a/packages/client/src/pages/admin/other-settings.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- none
- </FormSuspense>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XHeader from './_header_.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import * as os from '@/os';
-import { fetchInstance } from '@/instance';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-async function init() {
- await os.api('admin/meta');
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta').then(() => {
- fetchInstance();
- });
-}
-
-const headerActions = $computed(() => [{
- asFullButton: true,
- icon: 'ti ti-check',
- text: i18n.ts.save,
- handler: save,
-}]);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.other,
- icon: 'ti ti-adjustments',
-});
-</script>
diff --git a/packages/client/src/pages/admin/overview.active-users.vue b/packages/client/src/pages/admin/overview.active-users.vue
deleted file mode 100644
index c3ce5ac901..0000000000
--- a/packages/client/src/pages/admin/overview.active-users.vue
+++ /dev/null
@@ -1,217 +0,0 @@
-<template>
-<div>
- <MkLoading v-if="fetching"/>
- <div v-show="!fetching" :class="$style.root" class="_panel">
- <canvas ref="chartEl"></canvas>
- </div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
-import {
- Chart,
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
-} from 'chart.js';
-import { enUS } from 'date-fns/locale';
-import tinycolor from 'tinycolor2';
-import * as os from '@/os';
-import 'chartjs-adapter-date-fns';
-import { defaultStore } from '@/store';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip';
-import gradient from 'chartjs-plugin-gradient';
-import { chartVLine } from '@/scripts/chart-vline';
-
-Chart.register(
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
- gradient,
-);
-
-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})`;
-};
-
-const chartEl = $ref<HTMLCanvasElement>(null);
-const now = new Date();
-let chartInstance: Chart = null;
-const chartLimit = 7;
-let fetching = $ref(true);
-
-const { handler: externalTooltipHandler } = useChartTooltip();
-
-async function renderChart() {
- if (chartInstance) {
- chartInstance.destroy();
- }
-
- const getDate = (ago: number) => {
- const y = now.getFullYear();
- const m = now.getMonth();
- const d = now.getDate();
-
- return new Date(y, m, d - ago);
- };
-
- const format = (arr) => {
- return arr.map((v, i) => ({
- x: getDate(i).getTime(),
- y: v,
- }));
- };
-
- const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' });
-
- const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
- const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
-
- // フォントカラー
- Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
- const colorRead = '#3498db';
- const colorWrite = '#2ecc71';
-
- const max = Math.max(...raw.read);
-
- chartInstance = new Chart(chartEl, {
- type: 'bar',
- data: {
- datasets: [{
- parsing: false,
- label: 'Read',
- data: format(raw.read).slice().reverse(),
- pointRadius: 0,
- borderWidth: 0,
- borderJoinStyle: 'round',
- borderRadius: 4,
- backgroundColor: colorRead,
- barPercentage: 0.7,
- categoryPercentage: 0.5,
- fill: true,
- }, {
- parsing: false,
- label: 'Write',
- data: format(raw.write).slice().reverse(),
- pointRadius: 0,
- borderWidth: 0,
- borderJoinStyle: 'round',
- borderRadius: 4,
- backgroundColor: colorWrite,
- barPercentage: 0.7,
- categoryPercentage: 0.5,
- fill: true,
- }],
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 0,
- right: 8,
- top: 0,
- bottom: 0,
- },
- },
- scales: {
- x: {
- type: 'time',
- offset: true,
- time: {
- stepSize: 1,
- unit: 'day',
- },
- grid: {
- display: false,
- color: gridColor,
- borderColor: 'rgb(0, 0, 0, 0)',
- },
- ticks: {
- display: true,
- maxRotation: 0,
- autoSkipPadding: 8,
- },
- adapters: {
- date: {
- locale: enUS,
- },
- },
- },
- y: {
- position: 'left',
- suggestedMax: 10,
- grid: {
- display: true,
- color: gridColor,
- borderColor: 'rgb(0, 0, 0, 0)',
- },
- ticks: {
- display: true,
- //mirror: true,
- },
- },
- },
- interaction: {
- intersect: false,
- mode: 'index',
- },
- animation: false,
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: false,
- mode: 'index',
- animation: {
- duration: 0,
- },
- external: externalTooltipHandler,
- },
- gradient,
- },
- },
- plugins: [chartVLine(vLineColor)],
- });
-
- fetching = false;
-}
-
-onMounted(async () => {
- renderChart();
-});
-</script>
-
-<style lang="scss" module>
-.root {
- padding: 20px;
-}
-</style>
diff --git a/packages/client/src/pages/admin/overview.ap-requests.vue b/packages/client/src/pages/admin/overview.ap-requests.vue
deleted file mode 100644
index 024ffdc245..0000000000
--- a/packages/client/src/pages/admin/overview.ap-requests.vue
+++ /dev/null
@@ -1,346 +0,0 @@
-<template>
-<div>
- <MkLoading v-if="fetching"/>
- <div v-show="!fetching" :class="$style.root">
- <div class="charts _panel">
- <div class="chart">
- <canvas ref="chartEl2"></canvas>
- </div>
- <div class="chart">
- <canvas ref="chartEl"></canvas>
- </div>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, ref } from 'vue';
-import {
- Chart,
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
-} from 'chart.js';
-import gradient from 'chartjs-plugin-gradient';
-import { enUS } from 'date-fns/locale';
-import tinycolor from 'tinycolor2';
-import MkMiniChart from '@/components/MkMiniChart.vue';
-import * as os from '@/os';
-import number from '@/filters/number';
-import MkNumberDiff from '@/components/MkNumberDiff.vue';
-import { i18n } from '@/i18n';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip';
-import { chartVLine } from '@/scripts/chart-vline';
-import { defaultStore } from '@/store';
-
-Chart.register(
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
- gradient,
-);
-
-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})`;
-};
-
-const chartLimit = 50;
-const chartEl = $ref<HTMLCanvasElement>();
-const chartEl2 = $ref<HTMLCanvasElement>();
-let fetching = $ref(true);
-
-const { handler: externalTooltipHandler } = useChartTooltip();
-const { handler: externalTooltipHandler2 } = useChartTooltip();
-
-onMounted(async () => {
- const now = new Date();
-
- const getDate = (ago: number) => {
- const y = now.getFullYear();
- const m = now.getMonth();
- const d = now.getDate();
-
- return new Date(y, m, d - ago);
- };
-
- const format = (arr) => {
- return arr.map((v, i) => ({
- x: getDate(i).getTime(),
- y: v,
- }));
- };
-
- const formatMinus = (arr) => {
- return arr.map((v, i) => ({
- x: getDate(i).getTime(),
- y: -v,
- }));
- };
-
- const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' });
-
- const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
- const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
- const succColor = '#87e000';
- const failColor = '#ff4400';
-
- const succMax = Math.max(...raw.deliverSucceeded);
- const failMax = Math.max(...raw.deliverFailed);
-
- // フォントカラー
- Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
- new Chart(chartEl, {
- type: 'line',
- data: {
- datasets: [{
- stack: 'a',
- parsing: false,
- label: 'Out: Succ',
- data: format(raw.deliverSucceeded).slice().reverse(),
- tension: 0.3,
- pointRadius: 0,
- borderWidth: 2,
- borderColor: succColor,
- borderJoinStyle: 'round',
- borderRadius: 4,
- backgroundColor: alpha(succColor, 0.35),
- fill: true,
- clip: 8,
- }, {
- stack: 'a',
- parsing: false,
- label: 'Out: Fail',
- data: formatMinus(raw.deliverFailed).slice().reverse(),
- tension: 0.3,
- pointRadius: 0,
- borderWidth: 2,
- borderColor: failColor,
- borderJoinStyle: 'round',
- borderRadius: 4,
- backgroundColor: alpha(failColor, 0.35),
- fill: true,
- clip: 8,
- }],
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 0,
- right: 8,
- top: 0,
- bottom: 0,
- },
- },
- scales: {
- x: {
- type: 'time',
- stacked: true,
- offset: false,
- time: {
- stepSize: 1,
- unit: 'day',
- },
- grid: {
- display: true,
- color: gridColor,
- borderColor: 'rgb(0, 0, 0, 0)',
- },
- ticks: {
- display: true,
- maxRotation: 0,
- autoSkipPadding: 16,
- },
- adapters: {
- date: {
- locale: enUS,
- },
- },
- min: getDate(chartLimit).getTime(),
- },
- y: {
- stacked: true,
- position: 'left',
- suggestedMax: 10,
- grid: {
- display: true,
- color: gridColor,
- borderColor: 'rgb(0, 0, 0, 0)',
- },
- ticks: {
- display: true,
- //mirror: true,
- callback: (value, index, values) => value < 0 ? -value : value,
- },
- },
- },
- interaction: {
- intersect: false,
- mode: 'index',
- },
- elements: {
- point: {
- hoverRadius: 5,
- hoverBorderWidth: 2,
- },
- },
- animation: false,
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: false,
- mode: 'index',
- animation: {
- duration: 0,
- },
- external: externalTooltipHandler,
- },
- gradient,
- },
- },
- plugins: [chartVLine(vLineColor)],
- });
-
- new Chart(chartEl2, {
- type: 'bar',
- data: {
- datasets: [{
- parsing: false,
- label: 'In',
- data: format(raw.inboxReceived).slice().reverse(),
- tension: 0.3,
- pointRadius: 0,
- borderWidth: 0,
- borderJoinStyle: 'round',
- borderRadius: 4,
- backgroundColor: '#0cc2d6',
- barPercentage: 0.8,
- categoryPercentage: 0.9,
- fill: true,
- clip: 8,
- }],
- },
- options: {
- aspectRatio: 5,
- layout: {
- padding: {
- left: 0,
- right: 8,
- top: 0,
- bottom: 0,
- },
- },
- scales: {
- x: {
- type: 'time',
- offset: false,
- time: {
- stepSize: 1,
- unit: 'day',
- },
- grid: {
- display: false,
- color: gridColor,
- borderColor: 'rgb(0, 0, 0, 0)',
- },
- ticks: {
- display: false,
- maxRotation: 0,
- autoSkipPadding: 16,
- },
- adapters: {
- date: {
- locale: enUS,
- },
- },
- min: getDate(chartLimit).getTime(),
- },
- y: {
- position: 'left',
- suggestedMax: 10,
- grid: {
- display: true,
- color: gridColor,
- borderColor: 'rgb(0, 0, 0, 0)',
- },
- },
- },
- interaction: {
- intersect: false,
- mode: 'index',
- },
- elements: {
- point: {
- hoverRadius: 5,
- hoverBorderWidth: 2,
- },
- },
- animation: false,
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: false,
- mode: 'index',
- animation: {
- duration: 0,
- },
- external: externalTooltipHandler2,
- },
- gradient,
- },
- },
- plugins: [chartVLine(vLineColor)],
- });
-
- fetching = false;
-});
-</script>
-
-<style lang="scss" module>
-.root {
- &:global {
- > .charts {
- > .chart {
- padding: 16px;
-
- &:first-child {
- border-bottom: solid 0.5px var(--divider);
- }
- }
- }
- }
-}
-</style>
-
diff --git a/packages/client/src/pages/admin/overview.federation.vue b/packages/client/src/pages/admin/overview.federation.vue
deleted file mode 100644
index 71f5a054b4..0000000000
--- a/packages/client/src/pages/admin/overview.federation.vue
+++ /dev/null
@@ -1,185 +0,0 @@
-<template>
-<div>
- <MkLoading v-if="fetching"/>
- <div v-show="!fetching" :class="$style.root">
- <div v-if="topSubInstancesForPie && topPubInstancesForPie" class="pies">
- <div class="pie deliver _panel">
- <div class="title">Sub</div>
- <XPie :data="topSubInstancesForPie" class="chart"/>
- <div class="subTitle">Top 10</div>
- </div>
- <div class="pie inbox _panel">
- <div class="title">Pub</div>
- <XPie :data="topPubInstancesForPie" class="chart"/>
- <div class="subTitle">Top 10</div>
- </div>
- </div>
- <div v-if="!fetching" class="items">
- <div class="item _panel sub">
- <div class="icon"><i class="ti ti-world-download"></i></div>
- <div class="body">
- <div class="value">
- {{ number(federationSubActive) }}
- <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"></MkNumberDiff>
- </div>
- <div class="label">Sub</div>
- </div>
- </div>
- <div class="item _panel pub">
- <div class="icon"><i class="ti ti-world-upload"></i></div>
- <div class="body">
- <div class="value">
- {{ number(federationPubActive) }}
- <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"></MkNumberDiff>
- </div>
- <div class="label">Pub</div>
- </div>
- </div>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, ref } from 'vue';
-import XPie from './overview.pie.vue';
-import MkMiniChart from '@/components/MkMiniChart.vue';
-import * as os from '@/os';
-import number from '@/filters/number';
-import MkNumberDiff from '@/components/MkNumberDiff.vue';
-import { i18n } from '@/i18n';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip';
-
-let topSubInstancesForPie: any = $ref(null);
-let topPubInstancesForPie: any = $ref(null);
-let federationPubActive = $ref<number | null>(null);
-let federationPubActiveDiff = $ref<number | null>(null);
-let federationSubActive = $ref<number | null>(null);
-let federationSubActiveDiff = $ref<number | null>(null);
-let fetching = $ref(true);
-
-const { handler: externalTooltipHandler } = useChartTooltip();
-
-onMounted(async () => {
- const chart = await os.apiGet('charts/federation', { limit: 2, span: 'day' });
- federationPubActive = chart.pubActive[0];
- federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1];
- federationSubActive = chart.subActive[0];
- federationSubActiveDiff = chart.subActive[0] - chart.subActive[1];
-
- os.apiGet('federation/stats', { limit: 10 }).then(res => {
- topSubInstancesForPie = res.topSubInstances.map(x => ({
- name: x.host,
- color: x.themeColor,
- value: x.followersCount,
- onClick: () => {
- os.pageWindow(`/instance-info/${x.host}`);
- },
- })).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]);
- topPubInstancesForPie = res.topPubInstances.map(x => ({
- name: x.host,
- color: x.themeColor,
- value: x.followingCount,
- onClick: () => {
- os.pageWindow(`/instance-info/${x.host}`);
- },
- })).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowingCount }]);
- });
-
- fetching = false;
-});
-</script>
-
-<style lang="scss" module>
-.root {
-
- &:global {
- > .pies {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
- grid-gap: 12px;
- margin-bottom: 12px;
-
- > .pie {
- position: relative;
- padding: 12px;
-
- > .title {
- position: absolute;
- top: 20px;
- left: 20px;
- font-size: 90%;
- }
-
- > .chart {
- max-height: 150px;
- }
-
- > .subTitle {
- position: absolute;
- bottom: 20px;
- right: 20px;
- font-size: 85%;
- }
- }
- }
-
- > .items {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
- grid-gap: 12px;
-
- > .item {
- display: flex;
- box-sizing: border-box;
- padding: 12px;
-
- > .icon {
- display: grid;
- place-items: center;
- height: 100%;
- aspect-ratio: 1;
- margin-right: 12px;
- background: var(--accentedBg);
- color: var(--accent);
- border-radius: 10px;
- }
-
- &.sub {
- > .icon {
- background: #d5ba0026;
- color: #dfc300;
- }
- }
-
- &.pub {
- > .icon {
- background: #00cf2326;
- color: #00cd5b;
- }
- }
-
- > .body {
- padding: 2px 0;
-
- > .value {
- font-size: 1.25em;
- font-weight: bold;
-
- > .diff {
- font-size: 0.65em;
- font-weight: normal;
- }
- }
-
- > .label {
- font-size: 0.8em;
- opacity: 0.5;
- }
- }
- }
- }
- }
-}
-</style>
-
diff --git a/packages/client/src/pages/admin/overview.heatmap.vue b/packages/client/src/pages/admin/overview.heatmap.vue
deleted file mode 100644
index 16d1c83b9f..0000000000
--- a/packages/client/src/pages/admin/overview.heatmap.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-<template>
-<div class="_panel" :class="$style.root">
- <MkActiveUsersHeatmap/>
-</div>
-</template>
-
-<script lang="ts" setup>
-import MkActiveUsersHeatmap from '@/components/MkActiveUsersHeatmap.vue';
-</script>
-
-<style lang="scss" module>
-.root {
- padding: 20px;
-}
-</style>
diff --git a/packages/client/src/pages/admin/overview.instances.vue b/packages/client/src/pages/admin/overview.instances.vue
deleted file mode 100644
index 29848bf03b..0000000000
--- a/packages/client/src/pages/admin/overview.instances.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-<div class="wbrkwale">
- <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
- <MkLoading v-if="fetching"/>
- <div v-else class="instances">
- <MkA v-for="(instance, i) in instances" :key="instance.id" v-tooltip.mfm.noDelay="`${instance.name}\n${instance.host}\n${instance.softwareName} ${instance.softwareVersion}`" :to="`/instance-info/${instance.host}`" class="instance">
- <MkInstanceCardMini :instance="instance"/>
- </MkA>
- </div>
- </transition>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, ref } from 'vue';
-import * as os from '@/os';
-import { useInterval } from '@/scripts/use-interval';
-import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
-
-const instances = ref([]);
-const fetching = ref(true);
-
-const fetch = async () => {
- const fetchedInstances = await os.api('federation/instances', {
- sort: '+lastCommunicatedAt',
- limit: 6,
- });
- instances.value = fetchedInstances;
- fetching.value = false;
-};
-
-useInterval(fetch, 1000 * 60, {
- immediate: true,
- afterMounted: true,
-});
-</script>
-
-<style lang="scss" scoped>
-.wbrkwale {
- > .instances {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
- grid-gap: 12px;
-
- > .instance:hover {
- text-decoration: none;
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/overview.moderators.vue b/packages/client/src/pages/admin/overview.moderators.vue
deleted file mode 100644
index a1f63c8711..0000000000
--- a/packages/client/src/pages/admin/overview.moderators.vue
+++ /dev/null
@@ -1,55 +0,0 @@
-<template>
-<div>
- <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
- <MkLoading v-if="fetching"/>
- <div v-else :class="$style.root" class="_panel">
- <MkA v-for="user in moderators" :key="user.id" class="user" :to="`/user-info/${user.id}`">
- <MkAvatar :user="user" class="avatar" :show-indicator="true" :disable-link="true"/>
- </MkA>
- </div>
- </transition>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, ref } from 'vue';
-import * as os from '@/os';
-import number from '@/filters/number';
-import { i18n } from '@/i18n';
-
-let moderators: any = $ref(null);
-let fetching = $ref(true);
-
-onMounted(async () => {
- moderators = await os.api('admin/show-users', {
- sort: '+lastActiveDate',
- state: 'adminOrModerator',
- limit: 30,
- });
-
- fetching = false;
-});
-</script>
-
-<style lang="scss" module>
-.root {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(30px, 40px));
- grid-gap: 12px;
- place-content: center;
- padding: 12px;
-
- &:global {
- > .user {
- width: 100%;
- height: 100%;
- aspect-ratio: 1;
-
- > .avatar {
- width: 100%;
- height: 100%;
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/overview.pie.vue b/packages/client/src/pages/admin/overview.pie.vue
deleted file mode 100644
index 94509cf006..0000000000
--- a/packages/client/src/pages/admin/overview.pie.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<template>
-<canvas ref="chartEl"></canvas>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, ref } from 'vue';
-import {
- Chart,
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
- DoughnutController,
-} from 'chart.js';
-import number from '@/filters/number';
-import { defaultStore } from '@/store';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip';
-
-Chart.register(
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- DoughnutController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
-);
-
-const props = defineProps<{
- data: { name: string; value: number; color: string; onClick?: () => void }[];
-}>();
-
-const chartEl = ref<HTMLCanvasElement>(null);
-
-// フォントカラー
-Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
-const { handler: externalTooltipHandler } = useChartTooltip({
- position: 'middle',
-});
-
-let chartInstance: Chart;
-
-onMounted(() => {
- chartInstance = new Chart(chartEl.value, {
- type: 'doughnut',
- data: {
- labels: props.data.map(x => x.name),
- datasets: [{
- backgroundColor: props.data.map(x => x.color),
- borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
- borderWidth: 2,
- hoverOffset: 0,
- data: props.data.map(x => x.value),
- }],
- },
- options: {
- layout: {
- padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 16,
- },
- },
- onClick: (ev) => {
- const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
- if (hit && props.data[hit.index].onClick) {
- props.data[hit.index].onClick();
- }
- },
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: false,
- mode: 'index',
- animation: {
- duration: 0,
- },
- external: externalTooltipHandler,
- },
- },
- },
- });
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/client/src/pages/admin/overview.queue.chart.vue b/packages/client/src/pages/admin/overview.queue.chart.vue
deleted file mode 100644
index 1e095bddaa..0000000000
--- a/packages/client/src/pages/admin/overview.queue.chart.vue
+++ /dev/null
@@ -1,186 +0,0 @@
-<template>
-<canvas ref="chartEl"></canvas>
-</template>
-
-<script lang="ts" setup>
-import { watch, onMounted, onUnmounted, ref } from 'vue';
-import {
- Chart,
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
-} from 'chart.js';
-import number from '@/filters/number';
-import * as os from '@/os';
-import { defaultStore } from '@/store';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip';
-import { chartVLine } from '@/scripts/chart-vline';
-
-Chart.register(
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
-);
-
-const props = defineProps<{
- type: string;
-}>();
-
-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})`;
-};
-
-const chartEl = ref<HTMLCanvasElement>(null);
-
-const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-
-// フォントカラー
-Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
-const { handler: externalTooltipHandler } = useChartTooltip();
-
-let chartInstance: Chart;
-
-function setData(values) {
- if (chartInstance == null) return;
- for (const value of values) {
- chartInstance.data.labels.push('');
- chartInstance.data.datasets[0].data.push(value);
- if (chartInstance.data.datasets[0].data.length > 100) {
- chartInstance.data.labels.shift();
- chartInstance.data.datasets[0].data.shift();
- }
- }
- chartInstance.update();
-}
-
-function pushData(value) {
- if (chartInstance == null) return;
- chartInstance.data.labels.push('');
- chartInstance.data.datasets[0].data.push(value);
- if (chartInstance.data.datasets[0].data.length > 100) {
- chartInstance.data.labels.shift();
- chartInstance.data.datasets[0].data.shift();
- }
- chartInstance.update();
-}
-
-const label =
- props.type === 'process' ? 'Process' :
- props.type === 'active' ? 'Active' :
- props.type === 'delayed' ? 'Delayed' :
- props.type === 'waiting' ? 'Waiting' :
- '?' as never;
-
-const color =
- props.type === 'process' ? '#00E396' :
- props.type === 'active' ? '#00BCD4' :
- props.type === 'delayed' ? '#E53935' :
- props.type === 'waiting' ? '#FFB300' :
- '?' as never;
-
-onMounted(() => {
- const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
-
- chartInstance = new Chart(chartEl.value, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: label,
- pointRadius: 0,
- tension: 0.3,
- borderWidth: 2,
- borderJoinStyle: 'round',
- borderColor: color,
- backgroundColor: alpha(color, 0.2),
- fill: true,
- data: [],
- }],
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 0,
- right: 0,
- top: 0,
- bottom: 0,
- },
- },
- scales: {
- x: {
- grid: {
- display: false,
- color: gridColor,
- borderColor: 'rgb(0, 0, 0, 0)',
- },
- ticks: {
- display: false,
- maxTicksLimit: 10,
- },
- },
- y: {
- min: 0,
- grid: {
- color: gridColor,
- borderColor: 'rgb(0, 0, 0, 0)',
- },
- },
- },
- interaction: {
- intersect: false,
- },
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: false,
- mode: 'index',
- animation: {
- duration: 0,
- },
- external: externalTooltipHandler,
- },
- },
- },
- plugins: [chartVLine(vLineColor)],
- });
-});
-
-defineExpose({
- setData,
- pushData,
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/client/src/pages/admin/overview.queue.vue b/packages/client/src/pages/admin/overview.queue.vue
deleted file mode 100644
index 72ebddc72f..0000000000
--- a/packages/client/src/pages/admin/overview.queue.vue
+++ /dev/null
@@ -1,127 +0,0 @@
-<template>
-<div :class="$style.root">
- <div class="_table status">
- <div class="_row">
- <div class="_cell" style="text-align: center;"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
- <div class="_cell" style="text-align: center;"><div class="_label">Active</div>{{ number(active) }}</div>
- <div class="_cell" style="text-align: center;"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
- <div class="_cell" style="text-align: center;"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
- </div>
- </div>
- <div class="charts">
- <div class="chart">
- <div class="title">Process</div>
- <XChart ref="chartProcess" type="process"/>
- </div>
- <div class="chart">
- <div class="title">Active</div>
- <XChart ref="chartActive" type="active"/>
- </div>
- <div class="chart">
- <div class="title">Delayed</div>
- <XChart ref="chartDelayed" type="delayed"/>
- </div>
- <div class="chart">
- <div class="title">Waiting</div>
- <XChart ref="chartWaiting" type="waiting"/>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { markRaw, onMounted, onUnmounted, ref } from 'vue';
-import XChart from './overview.queue.chart.vue';
-import number from '@/filters/number';
-import * as os from '@/os';
-import { stream } from '@/stream';
-import { i18n } from '@/i18n';
-
-const connection = markRaw(stream.useChannel('queueStats'));
-
-const activeSincePrevTick = ref(0);
-const active = ref(0);
-const delayed = ref(0);
-const waiting = ref(0);
-let chartProcess = $ref<InstanceType<typeof XChart>>();
-let chartActive = $ref<InstanceType<typeof XChart>>();
-let chartDelayed = $ref<InstanceType<typeof XChart>>();
-let chartWaiting = $ref<InstanceType<typeof XChart>>();
-
-const props = defineProps<{
- domain: string;
-}>();
-
-const onStats = (stats) => {
- activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
- active.value = stats[props.domain].active;
- delayed.value = stats[props.domain].delayed;
- waiting.value = stats[props.domain].waiting;
-
- chartProcess.pushData(stats[props.domain].activeSincePrevTick);
- chartActive.pushData(stats[props.domain].active);
- chartDelayed.pushData(stats[props.domain].delayed);
- chartWaiting.pushData(stats[props.domain].waiting);
-};
-
-const onStatsLog = (statsLog) => {
- const dataProcess = [];
- const dataActive = [];
- const dataDelayed = [];
- const dataWaiting = [];
-
- for (const stats of [...statsLog].reverse()) {
- dataProcess.push(stats[props.domain].activeSincePrevTick);
- dataActive.push(stats[props.domain].active);
- dataDelayed.push(stats[props.domain].delayed);
- dataWaiting.push(stats[props.domain].waiting);
- }
-
- chartProcess.setData(dataProcess);
- chartActive.setData(dataActive);
- chartDelayed.setData(dataDelayed);
- chartWaiting.setData(dataWaiting);
-};
-
-onMounted(() => {
- connection.on('stats', onStats);
- connection.on('statsLog', onStatsLog);
- connection.send('requestLog', {
- id: Math.random().toString().substr(2, 8),
- length: 100,
- });
-});
-
-onUnmounted(() => {
- connection.off('stats', onStats);
- connection.off('statsLog', onStatsLog);
- connection.dispose();
-});
-</script>
-
-<style lang="scss" module>
-.root {
- &:global {
- > .status {
- padding: 0 0 16px 0;
- }
-
- > .charts {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 12px;
-
- > .chart {
- min-width: 0;
- padding: 16px;
- background: var(--panel);
- border-radius: var(--radius);
-
- > .title {
- font-size: 0.85em;
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/overview.retention.vue b/packages/client/src/pages/admin/overview.retention.vue
deleted file mode 100644
index feac6f8118..0000000000
--- a/packages/client/src/pages/admin/overview.retention.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-<div>
- <MkLoading v-if="fetching"/>
- <div v-else :class="$style.root">
- <div v-for="row in retention" class="row">
- <div v-for="value in getValues(row)" v-tooltip="value.percentage" class="cell">
- </div>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, ref } from 'vue';
-import * as os from '@/os';
-import number from '@/filters/number';
-import { i18n } from '@/i18n';
-
-let retention: any = $ref(null);
-let fetching = $ref(true);
-
-function getValues(row) {
- const data = [];
- for (const key in row.data) {
- data.push({
- date: new Date(key),
- value: number(row.data[key]),
- percentage: `${Math.ceil(row.data[key] / row.users) * 100}%`,
- });
- }
- data.sort((a, b) => a.date > b.date);
- return data;
-}
-
-onMounted(async () => {
- retention = await os.apiGet('retention', {});
-
- fetching = false;
-});
-</script>
-
-<style lang="scss" module>
-.root {
-
- &:global {
-
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/overview.stats.vue b/packages/client/src/pages/admin/overview.stats.vue
deleted file mode 100644
index 4dcf7e751a..0000000000
--- a/packages/client/src/pages/admin/overview.stats.vue
+++ /dev/null
@@ -1,155 +0,0 @@
-<template>
-<div>
- <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
- <MkLoading v-if="fetching"/>
- <div v-else :class="$style.root">
- <div class="item _panel users">
- <div class="icon"><i class="ti ti-users"></i></div>
- <div class="body">
- <div class="value">
- {{ number(stats.originalUsersCount) }}
- <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"></MkNumberDiff>
- </div>
- <div class="label">Users</div>
- </div>
- </div>
- <div class="item _panel notes">
- <div class="icon"><i class="ti ti-pencil"></i></div>
- <div class="body">
- <div class="value">
- {{ number(stats.originalNotesCount) }}
- <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"></MkNumberDiff>
- </div>
- <div class="label">Notes</div>
- </div>
- </div>
- <div class="item _panel instances">
- <div class="icon"><i class="ti ti-planet"></i></div>
- <div class="body">
- <div class="value">
- {{ number(stats.instances) }}
- </div>
- <div class="label">Instances</div>
- </div>
- </div>
- <div class="item _panel online">
- <div class="icon"><i class="ti ti-access-point"></i></div>
- <div class="body">
- <div class="value">
- {{ number(onlineUsersCount) }}
- </div>
- <div class="label">Online</div>
- </div>
- </div>
- </div>
- </transition>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, ref } from 'vue';
-import MkMiniChart from '@/components/MkMiniChart.vue';
-import * as os from '@/os';
-import number from '@/filters/number';
-import MkNumberDiff from '@/components/MkNumberDiff.vue';
-import { i18n } from '@/i18n';
-
-let stats: any = $ref(null);
-let usersComparedToThePrevDay = $ref<number>();
-let notesComparedToThePrevDay = $ref<number>();
-let onlineUsersCount = $ref(0);
-let fetching = $ref(true);
-
-onMounted(async () => {
- const [_stats, _onlineUsersCount] = await Promise.all([
- os.api('stats', {}),
- os.api('get-online-users-count').then(res => res.count),
- ]);
- stats = _stats;
- onlineUsersCount = _onlineUsersCount;
-
- os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
- usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1];
- });
-
- os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
- notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1];
- });
-
- fetching = false;
-});
-</script>
-
-<style lang="scss" module>
-.root {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
- grid-gap: 12px;
-
- &:global {
- > .item {
- display: flex;
- box-sizing: border-box;
- padding: 12px;
-
- > .icon {
- display: grid;
- place-items: center;
- height: 100%;
- aspect-ratio: 1;
- margin-right: 12px;
- background: var(--accentedBg);
- color: var(--accent);
- border-radius: 10px;
- }
-
- &.users {
- > .icon {
- background: #0088d726;
- color: #3d96c1;
- }
- }
-
- &.notes {
- > .icon {
- background: #86b30026;
- color: #86b300;
- }
- }
-
- &.instances {
- > .icon {
- background: #e96b0026;
- color: #d76d00;
- }
- }
-
- &.online {
- > .icon {
- background: #8a00d126;
- color: #c01ac3;
- }
- }
-
- > .body {
- padding: 2px 0;
-
- > .value {
- font-size: 1.25em;
- font-weight: bold;
-
- > .diff {
- font-size: 0.65em;
- font-weight: normal;
- }
- }
-
- > .label {
- font-size: 0.8em;
- opacity: 0.5;
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/overview.users.vue b/packages/client/src/pages/admin/overview.users.vue
deleted file mode 100644
index 5d4be11742..0000000000
--- a/packages/client/src/pages/admin/overview.users.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<template>
-<div :class="$style.root">
- <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
- <MkLoading v-if="fetching"/>
- <div v-else class="users">
- <MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/user-info/${user.id}`" class="user">
- <MkUserCardMini :user="user"/>
- </MkA>
- </div>
- </transition>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, ref } from 'vue';
-import * as os from '@/os';
-import { useInterval } from '@/scripts/use-interval';
-import MkUserCardMini from '@/components/MkUserCardMini.vue';
-
-let newUsers = $ref(null);
-let fetching = $ref(true);
-
-const fetch = async () => {
- const _newUsers = await os.api('admin/show-users', {
- limit: 5,
- sort: '+createdAt',
- origin: 'local',
- });
- newUsers = _newUsers;
- fetching = false;
-};
-
-useInterval(fetch, 1000 * 60, {
- immediate: true,
- afterMounted: true,
-});
-</script>
-
-<style lang="scss" module>
-.root {
- &:global {
- > .users {
- .chart-move {
- transition: transform 1s ease;
- }
-
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- grid-gap: 12px;
-
- > .user:hover {
- text-decoration: none;
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
deleted file mode 100644
index d656e55200..0000000000
--- a/packages/client/src/pages/admin/overview.vue
+++ /dev/null
@@ -1,190 +0,0 @@
-<template>
-<MkSpacer :content-max="1000">
- <div ref="rootEl" class="edbbcaef">
- <MkFolder class="item">
- <template #header>Stats</template>
- <XStats/>
- </MkFolder>
-
- <MkFolder class="item">
- <template #header>Active users</template>
- <XActiveUsers/>
- </MkFolder>
-
- <MkFolder class="item">
- <template #header>Heatmap</template>
- <XHeatmap/>
- </MkFolder>
-
- <MkFolder class="item">
- <template #header>Retention rate</template>
- <XRetention/>
- </MkFolder>
-
- <MkFolder class="item">
- <template #header>Moderators</template>
- <XModerators/>
- </MkFolder>
-
- <MkFolder class="item">
- <template #header>Federation</template>
- <XFederation/>
- </MkFolder>
-
- <MkFolder class="item">
- <template #header>Instances</template>
- <XInstances/>
- </MkFolder>
-
- <MkFolder class="item">
- <template #header>Ap requests</template>
- <XApRequests/>
- </MkFolder>
-
- <MkFolder class="item">
- <template #header>New users</template>
- <XUsers/>
- </MkFolder>
-
- <MkFolder class="item">
- <template #header>Deliver queue</template>
- <XQueue domain="deliver"/>
- </MkFolder>
-
- <MkFolder class="item">
- <template #header>Inbox queue</template>
- <XQueue domain="inbox"/>
- </MkFolder>
- </div>
-</MkSpacer>
-</template>
-
-<script lang="ts" setup>
-import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
-import XFederation from './overview.federation.vue';
-import XInstances from './overview.instances.vue';
-import XQueue from './overview.queue.vue';
-import XApRequests from './overview.ap-requests.vue';
-import XUsers from './overview.users.vue';
-import XActiveUsers from './overview.active-users.vue';
-import XStats from './overview.stats.vue';
-import XRetention from './overview.retention.vue';
-import XModerators from './overview.moderators.vue';
-import XHeatmap from './overview.heatmap.vue';
-import MkTagCloud from '@/components/MkTagCloud.vue';
-import { version, url } from '@/config';
-import * as os from '@/os';
-import { stream } from '@/stream';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-import 'chartjs-adapter-date-fns';
-import { defaultStore } from '@/store';
-import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
-import MkFolder from '@/components/MkFolder.vue';
-
-const rootEl = $ref<HTMLElement>();
-let serverInfo: any = $ref(null);
-let topSubInstancesForPie: any = $ref(null);
-let topPubInstancesForPie: any = $ref(null);
-let federationPubActive = $ref<number | null>(null);
-let federationPubActiveDiff = $ref<number | null>(null);
-let federationSubActive = $ref<number | null>(null);
-let federationSubActiveDiff = $ref<number | null>(null);
-let newUsers = $ref(null);
-let activeInstances = $shallowRef(null);
-const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
-const now = new Date();
-const filesPagination = {
- endpoint: 'admin/drive/files' as const,
- limit: 9,
- noPaging: true,
-};
-
-function onInstanceClick(i) {
- os.pageWindow(`/instance-info/${i.host}`);
-}
-
-onMounted(async () => {
- /*
- const magicGrid = new MagicGrid({
- container: rootEl,
- static: true,
- animate: true,
- });
-
- magicGrid.listen();
- */
-
- os.apiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => {
- federationPubActive = chart.pubActive[0];
- federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1];
- federationSubActive = chart.subActive[0];
- federationSubActiveDiff = chart.subActive[0] - chart.subActive[1];
- });
-
- os.apiGet('federation/stats', { limit: 10 }).then(res => {
- topSubInstancesForPie = res.topSubInstances.map(x => ({
- name: x.host,
- color: x.themeColor,
- value: x.followersCount,
- onClick: () => {
- os.pageWindow(`/instance-info/${x.host}`);
- },
- })).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]);
- topPubInstancesForPie = res.topPubInstances.map(x => ({
- name: x.host,
- color: x.themeColor,
- value: x.followingCount,
- onClick: () => {
- os.pageWindow(`/instance-info/${x.host}`);
- },
- })).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowingCount }]);
- });
-
- os.api('admin/server-info').then(serverInfoResponse => {
- serverInfo = serverInfoResponse;
- });
-
- os.api('admin/show-users', {
- limit: 5,
- sort: '+createdAt',
- }).then(res => {
- newUsers = res;
- });
-
- os.api('federation/instances', {
- sort: '+lastCommunicatedAt',
- limit: 25,
- }).then(res => {
- activeInstances = res;
- });
-
- nextTick(() => {
- queueStatsConnection.send('requestLog', {
- id: Math.random().toString().substr(2, 8),
- length: 100,
- });
- });
-});
-
-onBeforeUnmount(() => {
- queueStatsConnection.dispose();
-});
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.dashboard,
- icon: 'ti ti-dashboard',
-});
-</script>
-
-<style lang="scss" scoped>
-.edbbcaef {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
- grid-gap: 16px;
-}
-</style>
diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue
deleted file mode 100644
index 5d0d67980e..0000000000
--- a/packages/client/src/pages/admin/proxy-account.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template><MkStickyContainer>
- <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
- <MkKeyValue class="_formBlock">
- <template #key>{{ i18n.ts.proxyAccount }}</template>
- <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
- </MkKeyValue>
-
- <FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
- </FormSuspense>
-</MkSpacer></MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import MkKeyValue from '@/components/MkKeyValue.vue';
-import FormButton from '@/components/MkButton.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import * as os from '@/os';
-import { fetchInstance } from '@/instance';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let proxyAccount: any = $ref(null);
-let proxyAccountId: any = $ref(null);
-
-async function init() {
- const meta = await os.api('admin/meta');
- proxyAccountId = meta.proxyAccountId;
- if (proxyAccountId) {
- proxyAccount = await os.api('users/show', { userId: proxyAccountId });
- }
-}
-
-function chooseProxyAccount() {
- os.selectUser().then(user => {
- proxyAccount = user;
- proxyAccountId = user.id;
- save();
- });
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta', {
- proxyAccountId: proxyAccountId,
- }).then(() => {
- fetchInstance();
- });
-}
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.proxyAccount,
- icon: 'ti ti-ghost',
-});
-</script>
diff --git a/packages/client/src/pages/admin/queue.chart.chart.vue b/packages/client/src/pages/admin/queue.chart.chart.vue
deleted file mode 100644
index 5777674ae3..0000000000
--- a/packages/client/src/pages/admin/queue.chart.chart.vue
+++ /dev/null
@@ -1,186 +0,0 @@
-<template>
-<canvas ref="chartEl"></canvas>
-</template>
-
-<script lang="ts" setup>
-import { watch, onMounted, onUnmounted, ref } from 'vue';
-import {
- Chart,
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
-} from 'chart.js';
-import number from '@/filters/number';
-import * as os from '@/os';
-import { defaultStore } from '@/store';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip';
-import { chartVLine } from '@/scripts/chart-vline';
-
-Chart.register(
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
-);
-
-const props = defineProps<{
- type: string;
-}>();
-
-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})`;
-};
-
-const chartEl = ref<HTMLCanvasElement>(null);
-
-const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
-
-// フォントカラー
-Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
-const { handler: externalTooltipHandler } = useChartTooltip();
-
-let chartInstance: Chart;
-
-function setData(values) {
- if (chartInstance == null) return;
- for (const value of values) {
- chartInstance.data.labels.push('');
- chartInstance.data.datasets[0].data.push(value);
- if (chartInstance.data.datasets[0].data.length > 200) {
- chartInstance.data.labels.shift();
- chartInstance.data.datasets[0].data.shift();
- }
- }
- chartInstance.update();
-}
-
-function pushData(value) {
- if (chartInstance == null) return;
- chartInstance.data.labels.push('');
- chartInstance.data.datasets[0].data.push(value);
- if (chartInstance.data.datasets[0].data.length > 200) {
- chartInstance.data.labels.shift();
- chartInstance.data.datasets[0].data.shift();
- }
- chartInstance.update();
-}
-
-const label =
- props.type === 'process' ? 'Process' :
- props.type === 'active' ? 'Active' :
- props.type === 'delayed' ? 'Delayed' :
- props.type === 'waiting' ? 'Waiting' :
- '?' as never;
-
-const color =
- props.type === 'process' ? '#00E396' :
- props.type === 'active' ? '#00BCD4' :
- props.type === 'delayed' ? '#E53935' :
- props.type === 'waiting' ? '#FFB300' :
- '?' as never;
-
-onMounted(() => {
- const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
-
- chartInstance = new Chart(chartEl.value, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: label,
- pointRadius: 0,
- tension: 0.3,
- borderWidth: 2,
- borderJoinStyle: 'round',
- borderColor: color,
- backgroundColor: alpha(color, 0.2),
- fill: true,
- data: [],
- }],
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 0,
- right: 0,
- top: 0,
- bottom: 0,
- },
- },
- scales: {
- x: {
- grid: {
- display: true,
- color: gridColor,
- borderColor: 'rgb(0, 0, 0, 0)',
- },
- ticks: {
- display: false,
- maxTicksLimit: 10,
- },
- },
- y: {
- min: 0,
- grid: {
- color: gridColor,
- borderColor: 'rgb(0, 0, 0, 0)',
- },
- },
- },
- interaction: {
- intersect: false,
- },
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: false,
- mode: 'index',
- animation: {
- duration: 0,
- },
- external: externalTooltipHandler,
- },
- },
- },
- plugins: [chartVLine(vLineColor)],
- });
-});
-
-defineExpose({
- setData,
- pushData,
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/client/src/pages/admin/queue.chart.vue b/packages/client/src/pages/admin/queue.chart.vue
deleted file mode 100644
index 186a22c43e..0000000000
--- a/packages/client/src/pages/admin/queue.chart.vue
+++ /dev/null
@@ -1,149 +0,0 @@
-<template>
-<div class="pumxzjhg">
- <div class="_table status">
- <div class="_row">
- <div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
- <div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
- <div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
- <div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
- </div>
- </div>
- <div class="charts">
- <div class="chart">
- <div class="title">Process</div>
- <XChart ref="chartProcess" type="process"/>
- </div>
- <div class="chart">
- <div class="title">Active</div>
- <XChart ref="chartActive" type="active"/>
- </div>
- <div class="chart">
- <div class="title">Delayed</div>
- <XChart ref="chartDelayed" type="delayed"/>
- </div>
- <div class="chart">
- <div class="title">Waiting</div>
- <XChart ref="chartWaiting" type="waiting"/>
- </div>
- </div>
- <div class="jobs">
- <div v-if="jobs.length > 0">
- <div v-for="job in jobs" :key="job[0]">
- <span>{{ job[0] }}</span>
- <span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
- </div>
- </div>
- <span v-else style="opacity: 0.5;">{{ i18n.ts.noJobs }}</span>
- </div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { markRaw, onMounted, onUnmounted, ref } from 'vue';
-import XChart from './queue.chart.chart.vue';
-import number from '@/filters/number';
-import * as os from '@/os';
-import { stream } from '@/stream';
-import { i18n } from '@/i18n';
-
-const connection = markRaw(stream.useChannel('queueStats'));
-
-const activeSincePrevTick = ref(0);
-const active = ref(0);
-const delayed = ref(0);
-const waiting = ref(0);
-const jobs = ref([]);
-let chartProcess = $ref<InstanceType<typeof XChart>>();
-let chartActive = $ref<InstanceType<typeof XChart>>();
-let chartDelayed = $ref<InstanceType<typeof XChart>>();
-let chartWaiting = $ref<InstanceType<typeof XChart>>();
-
-const props = defineProps<{
- domain: string;
-}>();
-
-const onStats = (stats) => {
- activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
- active.value = stats[props.domain].active;
- delayed.value = stats[props.domain].delayed;
- waiting.value = stats[props.domain].waiting;
-
- chartProcess.pushData(stats[props.domain].activeSincePrevTick);
- chartActive.pushData(stats[props.domain].active);
- chartDelayed.pushData(stats[props.domain].delayed);
- chartWaiting.pushData(stats[props.domain].waiting);
-};
-
-const onStatsLog = (statsLog) => {
- const dataProcess = [];
- const dataActive = [];
- const dataDelayed = [];
- const dataWaiting = [];
-
- for (const stats of [...statsLog].reverse()) {
- dataProcess.push(stats[props.domain].activeSincePrevTick);
- dataActive.push(stats[props.domain].active);
- dataDelayed.push(stats[props.domain].delayed);
- dataWaiting.push(stats[props.domain].waiting);
- }
-
- chartProcess.setData(dataProcess);
- chartActive.setData(dataActive);
- chartDelayed.setData(dataDelayed);
- chartWaiting.setData(dataWaiting);
-};
-
-onMounted(() => {
- os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
- jobs.value = result;
- });
-
- connection.on('stats', onStats);
- connection.on('statsLog', onStatsLog);
- connection.send('requestLog', {
- id: Math.random().toString().substr(2, 8),
- length: 200,
- });
-});
-
-onUnmounted(() => {
- connection.off('stats', onStats);
- connection.off('statsLog', onStatsLog);
- connection.dispose();
-});
-</script>
-
-<style lang="scss" scoped>
-.pumxzjhg {
- > .status {
- padding: 16px;
- }
-
- > .charts {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 16px;
-
- > .chart {
- min-width: 0;
- padding: 16px;
- background: var(--panel);
- border-radius: var(--radius);
-
- > .title {
- margin-bottom: 8px;
- }
- }
- }
-
- > .jobs {
- margin-top: 16px;
- padding: 16px;
- max-height: 180px;
- overflow: auto;
- background: var(--panel);
- border-radius: var(--radius);
- }
-
-}
-</style>
diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue
deleted file mode 100644
index 8d19b49fc5..0000000000
--- a/packages/client/src/pages/admin/queue.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="800">
- <XQueue v-if="tab === 'deliver'" domain="deliver"/>
- <XQueue v-else-if="tab === 'inbox'" domain="inbox"/>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
-import XQueue from './queue.chart.vue';
-import XHeader from './_header_.vue';
-import MkButton from '@/components/MkButton.vue';
-import * as os from '@/os';
-import * as config from '@/config';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let tab = $ref('deliver');
-
-function clear() {
- os.confirm({
- type: 'warning',
- title: i18n.ts.clearQueueConfirmTitle,
- text: i18n.ts.clearQueueConfirmText,
- }).then(({ canceled }) => {
- if (canceled) return;
-
- os.apiWithDialog('admin/queue/clear');
- });
-}
-
-const headerActions = $computed(() => [{
- asFullButton: true,
- icon: 'ti ti-external-link',
- text: i18n.ts.dashboard,
- handler: () => {
- window.open(config.url + '/queue', '_blank');
- },
-}]);
-
-const headerTabs = $computed(() => [{
- key: 'deliver',
- title: 'Deliver',
-}, {
- key: 'inbox',
- title: 'Inbox',
-}]);
-
-definePageMetadata({
- title: i18n.ts.jobQueue,
- icon: 'ti ti-clock-play',
-});
-</script>
diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue
deleted file mode 100644
index 4768ae67b1..0000000000
--- a/packages/client/src/pages/admin/relays.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="800">
- <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
- <div>{{ relay.inbox }}</div>
- <div class="status">
- <i v-if="relay.status === 'accepted'" class="ti ti-check icon accepted"></i>
- <i v-else-if="relay.status === 'rejected'" class="ti ti-ban icon rejected"></i>
- <i v-else class="ti ti-clock icon requesting"></i>
- <span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
- </div>
- <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
- </div>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XHeader from './_header_.vue';
-import MkButton from '@/components/MkButton.vue';
-import * as os from '@/os';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let relays: any[] = $ref([]);
-
-async function addRelay() {
- const { canceled, result: inbox } = await os.inputText({
- title: i18n.ts.addRelay,
- type: 'url',
- placeholder: i18n.ts.inboxUrl,
- });
- if (canceled) return;
- os.api('admin/relays/add', {
- inbox,
- }).then((relay: any) => {
- refresh();
- }).catch((err: any) => {
- os.alert({
- type: 'error',
- text: err.message || err,
- });
- });
-}
-
-function remove(inbox: string) {
- os.api('admin/relays/remove', {
- inbox,
- }).then(() => {
- refresh();
- }).catch((err: any) => {
- os.alert({
- type: 'error',
- text: err.message || err,
- });
- });
-}
-
-function refresh() {
- os.api('admin/relays/list').then((relayList: any) => {
- relays = relayList;
- });
-}
-
-refresh();
-
-const headerActions = $computed(() => [{
- asFullButton: true,
- icon: 'ti ti-plus',
- text: i18n.ts.addRelay,
- handler: addRelay,
-}]);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.relays,
- icon: 'ti ti-planet',
-});
-</script>
-
-<style lang="scss" scoped>
-.relaycxt {
- > .status {
- margin: 8px 0;
-
- > .icon {
- width: 1em;
- margin-right: 0.75em;
-
- &.accepted {
- color: var(--success);
- }
-
- &.rejected {
- color: var(--error);
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue
deleted file mode 100644
index 2682bda337..0000000000
--- a/packages/client/src/pages/admin/security.vue
+++ /dev/null
@@ -1,179 +0,0 @@
-<template>
-<MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <div class="_formRoot">
- <FormFolder class="_formBlock">
- <template #icon><i class="ti ti-shield"></i></template>
- <template #label>{{ i18n.ts.botProtection }}</template>
- <template v-if="enableHcaptcha" #suffix>hCaptcha</template>
- <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
- <template v-else-if="enableTurnstile" #suffix>Turnstile</template>
- <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
-
- <XBotProtection/>
- </FormFolder>
-
- <FormFolder class="_formBlock">
- <template #icon><i class="ti ti-eye-off"></i></template>
- <template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
- <template v-if="sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
- <template v-else-if="sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
- <template v-else-if="sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
- <template v-else #suffix>{{ i18n.ts.none }}</template>
-
- <div class="_formRoot">
- <span class="_formBlock">{{ i18n.ts._sensitiveMediaDetection.description }}</span>
-
- <FormRadios v-model="sensitiveMediaDetection" class="_formBlock">
- <option value="none">{{ i18n.ts.none }}</option>
- <option value="all">{{ i18n.ts.all }}</option>
- <option value="local">{{ i18n.ts.localOnly }}</option>
- <option value="remote">{{ i18n.ts.remoteOnly }}</option>
- </FormRadios>
-
- <FormRange v-model="sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :text-converter="(v) => `${v + 1}`" class="_formBlock">
- <template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
- <template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
- </FormRange>
-
- <FormSwitch v-model="enableSensitiveMediaDetectionForVideos" class="_formBlock">
- <template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
- <template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
- </FormSwitch>
-
- <FormSwitch v-model="setSensitiveFlagAutomatically" class="_formBlock">
- <template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
- <template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
- </FormSwitch>
-
- <!-- 現状 false positive が多すぎて実用に耐えない
- <FormSwitch v-model="disallowUploadWhenPredictedAsPorn" class="_formBlock">
- <template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
- </FormSwitch>
- -->
-
- <FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
- </div>
- </FormFolder>
-
- <FormFolder class="_formBlock">
- <template #label>Active Email Validation</template>
- <template v-if="enableActiveEmailValidation" #suffix>Enabled</template>
- <template v-else #suffix>Disabled</template>
-
- <div class="_formRoot">
- <span class="_formBlock">{{ i18n.ts.activeEmailValidationDescription }}</span>
- <FormSwitch v-model="enableActiveEmailValidation" class="_formBlock" @update:model-value="save">
- <template #label>Enable</template>
- </FormSwitch>
- </div>
- </FormFolder>
-
- <FormFolder class="_formBlock">
- <template #label>Log IP address</template>
- <template v-if="enableIpLogging" #suffix>Enabled</template>
- <template v-else #suffix>Disabled</template>
-
- <div class="_formRoot">
- <FormSwitch v-model="enableIpLogging" class="_formBlock" @update:model-value="save">
- <template #label>Enable</template>
- </FormSwitch>
- </div>
- </FormFolder>
-
- <FormFolder class="_formBlock">
- <template #label>Summaly Proxy</template>
-
- <div class="_formRoot">
- <FormInput v-model="summalyProxy" class="_formBlock">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>Summaly Proxy URL</template>
- </FormInput>
-
- <FormButton primary class="_formBlock" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
- </div>
- </FormFolder>
- </div>
- </FormSuspense>
- </MkSpacer>
-</MkStickyContainer>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XBotProtection from './bot-protection.vue';
-import XHeader from './_header_.vue';
-import FormFolder from '@/components/form/folder.vue';
-import FormRadios from '@/components/form/radios.vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormInfo from '@/components/MkInfo.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import FormRange from '@/components/form/range.vue';
-import FormInput from '@/components/form/input.vue';
-import FormButton from '@/components/MkButton.vue';
-import * as os from '@/os';
-import { fetchInstance } from '@/instance';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let summalyProxy: string = $ref('');
-let enableHcaptcha: boolean = $ref(false);
-let enableRecaptcha: boolean = $ref(false);
-let enableTurnstile: boolean = $ref(false);
-let sensitiveMediaDetection: string = $ref('none');
-let sensitiveMediaDetectionSensitivity: number = $ref(0);
-let setSensitiveFlagAutomatically: boolean = $ref(false);
-let enableSensitiveMediaDetectionForVideos: boolean = $ref(false);
-let enableIpLogging: boolean = $ref(false);
-let enableActiveEmailValidation: boolean = $ref(false);
-
-async function init() {
- const meta = await os.api('admin/meta');
- summalyProxy = meta.summalyProxy;
- enableHcaptcha = meta.enableHcaptcha;
- enableRecaptcha = meta.enableRecaptcha;
- enableTurnstile = meta.enableTurnstile;
- sensitiveMediaDetection = meta.sensitiveMediaDetection;
- sensitiveMediaDetectionSensitivity =
- meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 :
- meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 :
- meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 :
- meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 :
- meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0;
- setSensitiveFlagAutomatically = meta.setSensitiveFlagAutomatically;
- enableSensitiveMediaDetectionForVideos = meta.enableSensitiveMediaDetectionForVideos;
- enableIpLogging = meta.enableIpLogging;
- enableActiveEmailValidation = meta.enableActiveEmailValidation;
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta', {
- summalyProxy,
- sensitiveMediaDetection,
- sensitiveMediaDetectionSensitivity:
- sensitiveMediaDetectionSensitivity === 0 ? 'veryLow' :
- sensitiveMediaDetectionSensitivity === 1 ? 'low' :
- sensitiveMediaDetectionSensitivity === 2 ? 'medium' :
- sensitiveMediaDetectionSensitivity === 3 ? 'high' :
- sensitiveMediaDetectionSensitivity === 4 ? 'veryHigh' :
- 0,
- setSensitiveFlagAutomatically,
- enableSensitiveMediaDetectionForVideos,
- enableIpLogging,
- enableActiveEmailValidation,
- }).then(() => {
- fetchInstance();
- });
-}
-
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.security,
- icon: 'ti ti-lock',
-});
-</script>
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
deleted file mode 100644
index 460eb92694..0000000000
--- a/packages/client/src/pages/admin/settings.vue
+++ /dev/null
@@ -1,262 +0,0 @@
-<template>
-<div>
- <MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
- <FormSuspense :p="init">
- <div class="_formRoot">
- <FormInput v-model="name" class="_formBlock">
- <template #label>{{ i18n.ts.instanceName }}</template>
- </FormInput>
-
- <FormTextarea v-model="description" class="_formBlock">
- <template #label>{{ i18n.ts.instanceDescription }}</template>
- </FormTextarea>
-
- <FormInput v-model="tosUrl" class="_formBlock">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.tosUrl }}</template>
- </FormInput>
-
- <FormSplit :min-width="300">
- <FormInput v-model="maintainerName" class="_formBlock">
- <template #label>{{ i18n.ts.maintainerName }}</template>
- </FormInput>
-
- <FormInput v-model="maintainerEmail" type="email" class="_formBlock">
- <template #prefix><i class="ti ti-mail"></i></template>
- <template #label>{{ i18n.ts.maintainerEmail }}</template>
- </FormInput>
- </FormSplit>
-
- <FormTextarea v-model="pinnedUsers" class="_formBlock">
- <template #label>{{ i18n.ts.pinnedUsers }}</template>
- <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
- </FormTextarea>
-
- <FormSection>
- <FormSwitch v-model="enableRegistration" class="_formBlock">
- <template #label>{{ i18n.ts.enableRegistration }}</template>
- </FormSwitch>
-
- <FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
- <template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
- </FormSwitch>
- </FormSection>
-
- <FormSection>
- <FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
- <FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
- <FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
- </FormSection>
-
- <FormSection>
- <template #label>{{ i18n.ts.theme }}</template>
-
- <FormInput v-model="iconUrl" class="_formBlock">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.iconUrl }}</template>
- </FormInput>
-
- <FormInput v-model="bannerUrl" class="_formBlock">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.bannerUrl }}</template>
- </FormInput>
-
- <FormInput v-model="backgroundImageUrl" class="_formBlock">
- <template #prefix><i class="ti ti-link"></i></template>
- <template #label>{{ i18n.ts.backgroundImageUrl }}</template>
- </FormInput>
-
- <FormInput v-model="themeColor" class="_formBlock">
- <template #prefix><i class="ti ti-palette"></i></template>
- <template #label>{{ i18n.ts.themeColor }}</template>
- <template #caption>#RRGGBB</template>
- </FormInput>
-
- <FormTextarea v-model="defaultLightTheme" class="_formBlock">
- <template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
- <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
- </FormTextarea>
-
- <FormTextarea v-model="defaultDarkTheme" class="_formBlock">
- <template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
- <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
- </FormTextarea>
- </FormSection>
-
- <FormSection>
- <template #label>{{ i18n.ts.files }}</template>
-
- <FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
- <template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
- <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
- </FormSwitch>
-
- <FormSplit :min-width="280">
- <FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
- <template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
- <template #suffix>MB</template>
- <template #caption>{{ i18n.ts.inMb }}</template>
- </FormInput>
-
- <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
- <template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
- <template #suffix>MB</template>
- <template #caption>{{ i18n.ts.inMb }}</template>
- </FormInput>
- </FormSplit>
- </FormSection>
-
- <FormSection>
- <template #label>ServiceWorker</template>
-
- <FormSwitch v-model="enableServiceWorker" class="_formBlock">
- <template #label>{{ i18n.ts.enableServiceworker }}</template>
- <template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
- </FormSwitch>
-
- <template v-if="enableServiceWorker">
- <FormInput v-model="swPublicKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Public key</template>
- </FormInput>
-
- <FormInput v-model="swPrivateKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>Private key</template>
- </FormInput>
- </template>
- </FormSection>
-
- <FormSection>
- <template #label>DeepL Translation</template>
-
- <FormInput v-model="deeplAuthKey" class="_formBlock">
- <template #prefix><i class="ti ti-key"></i></template>
- <template #label>DeepL Auth Key</template>
- </FormInput>
- <FormSwitch v-model="deeplIsPro" class="_formBlock">
- <template #label>Pro account</template>
- </FormSwitch>
- </FormSection>
- </div>
- </FormSuspense>
- </MkSpacer>
- </MkStickyContainer>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import XHeader from './_header_.vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormInput from '@/components/form/input.vue';
-import FormTextarea from '@/components/form/textarea.vue';
-import FormInfo from '@/components/MkInfo.vue';
-import FormSection from '@/components/form/section.vue';
-import FormSplit from '@/components/form/split.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import * as os from '@/os';
-import { fetchInstance } from '@/instance';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-
-let name: string | null = $ref(null);
-let description: string | null = $ref(null);
-let tosUrl: string | null = $ref(null);
-let maintainerName: string | null = $ref(null);
-let maintainerEmail: string | null = $ref(null);
-let iconUrl: string | null = $ref(null);
-let bannerUrl: string | null = $ref(null);
-let backgroundImageUrl: string | null = $ref(null);
-let themeColor: any = $ref(null);
-let defaultLightTheme: any = $ref(null);
-let defaultDarkTheme: any = $ref(null);
-let enableLocalTimeline: boolean = $ref(false);
-let enableGlobalTimeline: boolean = $ref(false);
-let pinnedUsers: string = $ref('');
-let cacheRemoteFiles: boolean = $ref(false);
-let localDriveCapacityMb: any = $ref(0);
-let remoteDriveCapacityMb: any = $ref(0);
-let enableRegistration: boolean = $ref(false);
-let emailRequiredForSignup: boolean = $ref(false);
-let enableServiceWorker: boolean = $ref(false);
-let swPublicKey: any = $ref(null);
-let swPrivateKey: any = $ref(null);
-let deeplAuthKey: string = $ref('');
-let deeplIsPro: boolean = $ref(false);
-
-async function init() {
- const meta = await os.api('admin/meta');
- name = meta.name;
- description = meta.description;
- tosUrl = meta.tosUrl;
- iconUrl = meta.iconUrl;
- bannerUrl = meta.bannerUrl;
- backgroundImageUrl = meta.backgroundImageUrl;
- themeColor = meta.themeColor;
- defaultLightTheme = meta.defaultLightTheme;
- defaultDarkTheme = meta.defaultDarkTheme;
- maintainerName = meta.maintainerName;
- maintainerEmail = meta.maintainerEmail;
- enableLocalTimeline = !meta.disableLocalTimeline;
- enableGlobalTimeline = !meta.disableGlobalTimeline;
- pinnedUsers = meta.pinnedUsers.join('\n');
- cacheRemoteFiles = meta.cacheRemoteFiles;
- localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
- remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
- enableRegistration = !meta.disableRegistration;
- emailRequiredForSignup = meta.emailRequiredForSignup;
- enableServiceWorker = meta.enableServiceWorker;
- swPublicKey = meta.swPublickey;
- swPrivateKey = meta.swPrivateKey;
- deeplAuthKey = meta.deeplAuthKey;
- deeplIsPro = meta.deeplIsPro;
-}
-
-function save() {
- os.apiWithDialog('admin/update-meta', {
- name,
- description,
- tosUrl,
- iconUrl,
- bannerUrl,
- backgroundImageUrl,
- themeColor: themeColor === '' ? null : themeColor,
- defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme,
- defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme,
- maintainerName,
- maintainerEmail,
- disableLocalTimeline: !enableLocalTimeline,
- disableGlobalTimeline: !enableGlobalTimeline,
- pinnedUsers: pinnedUsers.split('\n'),
- cacheRemoteFiles,
- localDriveCapacityMb: parseInt(localDriveCapacityMb, 10),
- remoteDriveCapacityMb: parseInt(remoteDriveCapacityMb, 10),
- disableRegistration: !enableRegistration,
- emailRequiredForSignup,
- enableServiceWorker,
- swPublicKey,
- swPrivateKey,
- deeplAuthKey,
- deeplIsPro,
- }).then(() => {
- fetchInstance();
- });
-}
-
-const headerActions = $computed(() => [{
- asFullButton: true,
- icon: 'ti ti-check',
- text: i18n.ts.save,
- handler: save,
-}]);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata({
- title: i18n.ts.general,
- icon: 'ti ti-settings',
-});
-</script>
diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue
deleted file mode 100644
index d466e21907..0000000000
--- a/packages/client/src/pages/admin/users.vue
+++ /dev/null
@@ -1,170 +0,0 @@
-<template>
-<div>
- <MkStickyContainer>
- <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer :content-max="900">
- <div class="lknzcolw">
- <div class="users">
- <div class="inputs">
- <MkSelect v-model="sort" style="flex: 1;">
- <template #label>{{ i18n.ts.sort }}</template>
- <option value="-createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.ascendingOrder }})</option>
- <option value="+createdAt">{{ i18n.ts.registeredDate }} ({{ i18n.ts.descendingOrder }})</option>
- <option value="-updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.ascendingOrder }})</option>
- <option value="+updatedAt">{{ i18n.ts.lastUsed }} ({{ i18n.ts.descendingOrder }})</option>
- </MkSelect>
- <MkSelect v-model="state" style="flex: 1;">
- <template #label>{{ i18n.ts.state }}</template>
- <option value="all">{{ i18n.ts.all }}</option>
- <option value="available">{{ i18n.ts.normal }}</option>
- <option value="admin">{{ i18n.ts.administrator }}</option>
- <option value="moderator">{{ i18n.ts.moderator }}</option>
- <option value="silenced">{{ i18n.ts.silence }}</option>
- <option value="suspended">{{ i18n.ts.suspend }}</option>
- </MkSelect>
- <MkSelect v-model="origin" style="flex: 1;">
- <template #label>{{ i18n.ts.instance }}</template>
- <option value="combined">{{ i18n.ts.all }}</option>
- <option value="local">{{ i18n.ts.local }}</option>
- <option value="remote">{{ i18n.ts.remote }}</option>
- </MkSelect>
- </div>
- <div class="inputs">
- <MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:model-value="$refs.users.reload()">
- <template #prefix>@</template>
- <template #label>{{ i18n.ts.username }}</template>
- </MkInput>
- <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:model-value="$refs.users.reload()">
- <template #prefix>@</template>
- <template #label>{{ i18n.ts.host }}</template>
- </MkInput>
- </div>
-
- <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
- <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${new Date(user.updatedAt).toLocaleString()}`" class="user" :to="`/user-info/${user.id}`">
- <MkUserCardMini :user="user"/>
- </MkA>
- </MkPagination>
- </div>
- </div>
- </MkSpacer>
- </MkStickyContainer>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { computed } from 'vue';
-import XHeader from './_header_.vue';
-import MkInput from '@/components/form/input.vue';
-import MkSelect from '@/components/form/select.vue';
-import MkPagination from '@/components/MkPagination.vue';
-import * as os from '@/os';
-import { lookupUser } from '@/scripts/lookup-user';
-import { i18n } from '@/i18n';
-import { definePageMetadata } from '@/scripts/page-metadata';
-import MkUserCardMini from '@/components/MkUserCardMini.vue';
-
-let paginationComponent = $ref<InstanceType<typeof MkPagination>>();
-
-let sort = $ref('+createdAt');
-let state = $ref('all');
-let origin = $ref('local');
-let searchUsername = $ref('');
-let searchHost = $ref('');
-const pagination = {
- endpoint: 'admin/show-users' as const,
- limit: 10,
- params: computed(() => ({
- sort: sort,
- state: state,
- origin: origin,
- username: searchUsername,
- hostname: searchHost,
- })),
- offsetMode: true,
-};
-
-function searchUser() {
- os.selectUser().then(user => {
- show(user);
- });
-}
-
-async function addUser() {
- const { canceled: canceled1, result: username } = await os.inputText({
- title: i18n.ts.username,
- });
- if (canceled1) return;
-
- const { canceled: canceled2, result: password } = await os.inputText({
- title: i18n.ts.password,
- type: 'password',
- });
- if (canceled2) return;
-
- os.apiWithDialog('admin/accounts/create', {
- username: username,
- password: password,
- }).then(res => {
- paginationComponent.reload();
- });
-}
-
-function show(user) {
- os.pageWindow(`/user-info/${user.id}`);
-}
-
-const headerActions = $computed(() => [{
- icon: 'ti ti-search',
- text: i18n.ts.search,
- handler: searchUser,
-}, {
- asFullButton: true,
- icon: 'ti ti-plus',
- text: i18n.ts.addUser,
- handler: addUser,
-}, {
- asFullButton: true,
- icon: 'ti ti-search',
- text: i18n.ts.lookup,
- handler: lookupUser,
-}]);
-
-const headerTabs = $computed(() => []);
-
-definePageMetadata(computed(() => ({
- title: i18n.ts.users,
- icon: 'ti ti-users',
-})));
-</script>
-
-<style lang="scss" scoped>
-.lknzcolw {
- > .users {
-
- > .inputs {
- display: flex;
- margin-bottom: 16px;
-
- > * {
- margin-right: 16px;
-
- &:last-child {
- margin-right: 0;
- }
- }
- }
-
- > .users {
- margin-top: var(--margin);
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
- grid-gap: 12px;
-
- > .user:hover {
- text-decoration: none;
- }
- }
- }
-}
-</style>