Introduce instances admin page
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
65249b60f2
commit
e717312de7
@ -290,6 +290,7 @@ config :mobilizon, Oban,
|
||||
crontab: [
|
||||
{"@hourly", Mobilizon.Service.Workers.BuildSiteMap, queue: :background},
|
||||
{"17 4 * * *", Mobilizon.Service.Workers.RefreshGroups, queue: :background},
|
||||
{"36 * * * *", Mobilizon.Service.Workers.RefreshInstances, queue: :background},
|
||||
{"@hourly", Mobilizon.Service.Workers.CleanOrphanMediaWorker, queue: :background},
|
||||
{"@hourly", Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker, queue: :background},
|
||||
{"@hourly", Mobilizon.Service.Workers.ExportCleanerWorker, queue: :background},
|
||||
|
@ -70,6 +70,9 @@ export const typePolicies: TypePolicies = {
|
||||
participantStats: { merge: replaceMergePolicy },
|
||||
},
|
||||
},
|
||||
Instance: {
|
||||
keyFields: ["domain"],
|
||||
},
|
||||
RootQueryType: {
|
||||
fields: {
|
||||
relayFollowers: paginatedLimitPagination<IFollower>(),
|
||||
|
9
js/src/assets/logo.svg
Normal file
9
js/src/assets/logo.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60">
|
||||
<path style="opacity:0;fill:#fea72b;fill-opacity:1;stroke:none;stroke-opacity:0" d="M-5.801-6.164h72.69v72.871h-72.69z" />
|
||||
<g data-name="Calque 2">
|
||||
<g data-name="header">
|
||||
<path d="M26.58 27.06q0 8-4.26 12.3a12.21 12.21 0 0 1-9 3.42 12.21 12.21 0 0 1-9-3.42Q0 35.1 0 27.06q0-8.04 4.26-12.3a12.21 12.21 0 0 1 9-3.42 12.21 12.21 0 0 1 9 3.42q4.32 4.24 4.32 12.3zM13.29 17q-5.67 0-5.67 10.06t5.67 10.08q5.71 0 5.71-10.08T13.29 17z" style="fill:#3a384c;fill-opacity:1" transform="translate(14.627 5.256) scale(1.15671)" />
|
||||
<path d="M9 6.78a7.37 7.37 0 0 1-.6-3 7.37 7.37 0 0 1 .6-3A8.09 8.09 0 0 1 12.83 0a7.05 7.05 0 0 1 3.69.84 7.37 7.37 0 0 1 .6 3 7.37 7.37 0 0 1-.6 3 7.46 7.46 0 0 1-3.87.84A6.49 6.49 0 0 1 9 6.78z" style="fill:#fff" transform="translate(14.627 5.256) scale(1.15671)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 920 B |
@ -1,262 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-table
|
||||
v-show="relayFollowers.elements.length > 0"
|
||||
:data="relayFollowers.elements"
|
||||
:loading="$apollo.queries.relayFollowers.loading"
|
||||
ref="table"
|
||||
:checked-rows.sync="checkedRows"
|
||||
detailed
|
||||
:show-detail-icon="false"
|
||||
paginated
|
||||
backend-pagination
|
||||
:current-page.sync="page"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:total="relayFollowers.total"
|
||||
:per-page="FOLLOWERS_PER_PAGE"
|
||||
@page-change="onFollowersPageChange"
|
||||
checkable
|
||||
checkbox-position="left"
|
||||
>
|
||||
<b-table-column
|
||||
field="actor.id"
|
||||
label="ID"
|
||||
width="40"
|
||||
numeric
|
||||
v-slot="props"
|
||||
>{{ props.row.actor.id }}</b-table-column
|
||||
>
|
||||
|
||||
<b-table-column
|
||||
field="actor.type"
|
||||
:label="$t('Type')"
|
||||
width="80"
|
||||
v-slot="props"
|
||||
>
|
||||
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.actor)" />
|
||||
<b-icon icon="account-circle" v-else />
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column
|
||||
field="approved"
|
||||
:label="$t('Status')"
|
||||
width="100"
|
||||
sortable
|
||||
centered
|
||||
v-slot="props"
|
||||
>
|
||||
<span
|
||||
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
|
||||
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
|
||||
>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="actor.domain" :label="$t('Domain')" sortable>
|
||||
<template v-slot:default="props">
|
||||
<a
|
||||
@click="toggle(props.row)"
|
||||
v-if="RelayMixin.isInstance(props.row.actor)"
|
||||
>{{ props.row.actor.domain }}</a
|
||||
>
|
||||
<a @click="toggle(props.row)" v-else>{{
|
||||
`${props.row.actor.preferredUsername}@${props.row.actor.domain}`
|
||||
}}</a>
|
||||
</template>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column
|
||||
field="targetActor.updatedAt"
|
||||
:label="$t('Date')"
|
||||
sortable
|
||||
v-slot="props"
|
||||
>
|
||||
<span
|
||||
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
|
||||
>{{
|
||||
formatDistanceToNow(new Date(props.row.updatedAt), {
|
||||
locale: $dateFnsLocale,
|
||||
})
|
||||
}}</span
|
||||
></b-table-column
|
||||
>
|
||||
|
||||
<template #detail="props">
|
||||
<article>
|
||||
<div class="content">
|
||||
<strong>{{ props.row.actor.name }}</strong>
|
||||
<small v-if="props.row.actor.preferredUsername !== 'relay'"
|
||||
>@{{ props.row.actor.preferredUsername }}</small
|
||||
>
|
||||
<p v-html="props.row.actor.summary" />
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
@click="acceptRelays"
|
||||
type="is-success"
|
||||
v-if="checkedRowsHaveAtLeastOneToApprove"
|
||||
>
|
||||
{{
|
||||
$tc(
|
||||
"No instance to approve|Approve instance|Approve {number} instances",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
<b-button @click="rejectRelays" type="is-danger">
|
||||
{{
|
||||
$tc(
|
||||
"No instance to reject|Reject instance|Reject {number} instances",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
<b-message type="is-danger" v-if="relayFollowers.elements.length === 0">{{
|
||||
$t("No instance follows your instance yet.")
|
||||
}}</b-message>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from "vue-property-decorator";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
ACCEPT_RELAY,
|
||||
REJECT_RELAY,
|
||||
RELAY_FOLLOWERS,
|
||||
} from "../../graphql/admin";
|
||||
import { IFollower } from "../../types/actor/follower.model";
|
||||
import RelayMixin from "../../mixins/relay";
|
||||
import RouteName from "@/router/name";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
|
||||
const FOLLOWERS_PER_PAGE = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
relayFollowers: {
|
||||
query: RELAY_FOLLOWERS,
|
||||
variables() {
|
||||
return {
|
||||
page: this.page,
|
||||
limit: FOLLOWERS_PER_PAGE,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Followers") as string,
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Followers extends Mixins(RelayMixin) {
|
||||
RelayMixin = RelayMixin;
|
||||
|
||||
formatDistanceToNow = formatDistanceToNow;
|
||||
|
||||
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
|
||||
checkedRows: IFollower[] = [];
|
||||
|
||||
FOLLOWERS_PER_PAGE = FOLLOWERS_PER_PAGE;
|
||||
|
||||
toggle(row: Record<string, unknown>): void {
|
||||
this.table.toggleDetails(row);
|
||||
}
|
||||
|
||||
get page(): number {
|
||||
return parseInt((this.$route.query.page as string) || "1", 10);
|
||||
}
|
||||
|
||||
set page(page: number) {
|
||||
this.pushRouter(RouteName.RELAY_FOLLOWERS, {
|
||||
page: page.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
acceptRelays(): void {
|
||||
this.checkedRows.forEach((row: IFollower) => {
|
||||
this.acceptRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
rejectRelays(): void {
|
||||
this.checkedRows.forEach((row: IFollower) => {
|
||||
this.rejectRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async acceptRelay(address: string): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: ACCEPT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async rejectRelay(address: string): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REJECT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get checkedRowsHaveAtLeastOneToApprove(): boolean {
|
||||
return this.checkedRows.some((checkedRow) => !checkedRow.approved);
|
||||
}
|
||||
|
||||
async onFollowersPageChange(page: number): Promise<void> {
|
||||
this.page = page;
|
||||
try {
|
||||
await this.$apollo.queries.relayFollowers.fetchMore({
|
||||
variables: {
|
||||
page: this.page,
|
||||
limit: FOLLOWERS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,311 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="followRelay">
|
||||
<b-field
|
||||
:label="$t('Add an instance')"
|
||||
custom-class="add-relay"
|
||||
horizontal
|
||||
>
|
||||
<b-field grouped expanded size="is-large">
|
||||
<p class="control">
|
||||
<b-input
|
||||
v-model="newRelayAddress"
|
||||
:placeholder="$t('Ex: mobilizon.fr')"
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">{{
|
||||
$t("Add an instance")
|
||||
}}</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
</form>
|
||||
<b-table
|
||||
v-show="relayFollowings.elements.length > 0"
|
||||
:data="relayFollowings.elements"
|
||||
:loading="$apollo.queries.relayFollowings.loading"
|
||||
ref="table"
|
||||
:checked-rows.sync="checkedRows"
|
||||
:is-row-checkable="(row) => row.id !== 3"
|
||||
detailed
|
||||
:show-detail-icon="false"
|
||||
paginated
|
||||
backend-pagination
|
||||
:current-page.sync="page"
|
||||
:aria-next-label="$t('Next page')"
|
||||
:aria-previous-label="$t('Previous page')"
|
||||
:aria-page-label="$t('Page')"
|
||||
:aria-current-label="$t('Current page')"
|
||||
:total="relayFollowings.total"
|
||||
:per-page="FOLLOWINGS_PER_PAGE"
|
||||
@page-change="onFollowingsPageChange"
|
||||
checkable
|
||||
checkbox-position="left"
|
||||
>
|
||||
<b-table-column
|
||||
field="targetActor.id"
|
||||
label="ID"
|
||||
width="40"
|
||||
numeric
|
||||
v-slot="props"
|
||||
>{{ props.row.targetActor.id }}</b-table-column
|
||||
>
|
||||
|
||||
<b-table-column
|
||||
field="targetActor.type"
|
||||
:label="$t('Type')"
|
||||
width="80"
|
||||
v-slot="props"
|
||||
>
|
||||
<b-icon
|
||||
icon="lan"
|
||||
v-if="RelayMixin.isInstance(props.row.targetActor)"
|
||||
/>
|
||||
<b-icon icon="account-circle" v-else />
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column
|
||||
field="approved"
|
||||
:label="$t('Status')"
|
||||
width="100"
|
||||
sortable
|
||||
centered
|
||||
v-slot="props"
|
||||
>
|
||||
<span
|
||||
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
|
||||
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
|
||||
>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
|
||||
<template v-slot:default="props">
|
||||
<a
|
||||
@click="toggle(props.row)"
|
||||
v-if="RelayMixin.isInstance(props.row.targetActor)"
|
||||
>{{ props.row.targetActor.domain }}</a
|
||||
>
|
||||
<a @click="toggle(props.row)" v-else>{{
|
||||
`${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}`
|
||||
}}</a>
|
||||
</template>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column
|
||||
field="targetActor.updatedAt"
|
||||
:label="$t('Date')"
|
||||
sortable
|
||||
v-slot="props"
|
||||
>
|
||||
<span
|
||||
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
|
||||
>{{
|
||||
formatDistanceToNow(new Date(props.row.updatedAt), {
|
||||
locale: $dateFnsLocale,
|
||||
})
|
||||
}}</span
|
||||
></b-table-column
|
||||
>
|
||||
|
||||
<template #detail="props">
|
||||
<article>
|
||||
<div class="content">
|
||||
<strong>{{ props.row.targetActor.name }}</strong>
|
||||
<small v-if="props.row.actor.preferredUsername !== 'relay'"
|
||||
>@{{ props.row.targetActor.preferredUsername }}</small
|
||||
>
|
||||
<p v-html="props.row.targetActor.summary" />
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<b-button @click="removeRelays" type="is-danger">
|
||||
{{
|
||||
$tc(
|
||||
"No instance to remove|Remove instance|Remove {number} instances",
|
||||
checkedRows.length,
|
||||
{ number: checkedRows.length }
|
||||
)
|
||||
}}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
<b-message type="is-danger" v-if="relayFollowings.total === 0">{{
|
||||
$t("You don't follow any instances yet.")
|
||||
}}</b-message>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from "vue-property-decorator";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin";
|
||||
import { IFollower } from "../../types/actor/follower.model";
|
||||
import RelayMixin from "../../mixins/relay";
|
||||
import { RELAY_FOLLOWINGS } from "@/graphql/admin";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import RouteName from "@/router/name";
|
||||
import { ApolloCache, FetchResult, Reference } from "@apollo/client/core";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
const FOLLOWINGS_PER_PAGE = 10;
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
relayFollowings: {
|
||||
query: RELAY_FOLLOWINGS,
|
||||
variables() {
|
||||
return {
|
||||
page: this.page,
|
||||
limit: FOLLOWINGS_PER_PAGE,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Followings") as string,
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Followings extends Mixins(RelayMixin) {
|
||||
newRelayAddress = "";
|
||||
|
||||
RelayMixin = RelayMixin;
|
||||
|
||||
formatDistanceToNow = formatDistanceToNow;
|
||||
|
||||
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
|
||||
FOLLOWINGS_PER_PAGE = FOLLOWINGS_PER_PAGE;
|
||||
|
||||
checkedRows: IFollower[] = [];
|
||||
|
||||
get page(): number {
|
||||
return parseInt((this.$route.query.page as string) || "1", 10);
|
||||
}
|
||||
|
||||
set page(page: number) {
|
||||
this.pushRouter(RouteName.RELAY_FOLLOWINGS, {
|
||||
page: page.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
async onFollowingsPageChange(page: number): Promise<void> {
|
||||
this.page = page;
|
||||
try {
|
||||
await this.$apollo.queries.relayFollowings.fetchMore({
|
||||
variables: {
|
||||
page: this.page,
|
||||
limit: FOLLOWINGS_PER_PAGE,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async followRelay(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await this.$apollo.mutate<{ relayFollowings: Paginate<IFollower> }>({
|
||||
mutation: ADD_RELAY,
|
||||
variables: {
|
||||
address: this.newRelayAddress.trim(), // trim to fix copy and paste domain name spaces and tabs
|
||||
},
|
||||
update(
|
||||
cache: ApolloCache<{ relayFollowings: Paginate<IFollower> }>,
|
||||
{ data }: FetchResult
|
||||
) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
relayFollowings(
|
||||
existingFollowings = { elements: [], total: 0 },
|
||||
{ readField }
|
||||
) {
|
||||
const newFollowingRef = cache.writeFragment({
|
||||
id: `${data?.addRelay.__typename}:${data?.addRelay.id}`,
|
||||
data: data?.addRelay,
|
||||
fragment: gql`
|
||||
fragment NewFollowing on Follower {
|
||||
id
|
||||
}
|
||||
`,
|
||||
});
|
||||
if (
|
||||
existingFollowings.elements.some(
|
||||
(ref: Reference) =>
|
||||
readField("id", ref) === data?.addRelay.id
|
||||
)
|
||||
) {
|
||||
return existingFollowings;
|
||||
}
|
||||
return {
|
||||
total: existingFollowings.total + 1,
|
||||
elements: [newFollowingRef, ...existingFollowings.elements],
|
||||
};
|
||||
},
|
||||
},
|
||||
broadcast: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
this.newRelayAddress = "";
|
||||
} catch (err: any) {
|
||||
if (err.message) {
|
||||
Snackbar.open({
|
||||
message: err.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeRelays(): void {
|
||||
this.checkedRows.forEach((row: IFollower) => {
|
||||
this.removeRelay(row);
|
||||
});
|
||||
}
|
||||
|
||||
async removeRelay(follower: IFollower): Promise<void> {
|
||||
const address = `${follower.targetActor.preferredUsername}@${follower.targetActor.domain}`;
|
||||
try {
|
||||
await this.$apollo.mutate<{ removeRelay: IFollower }>({
|
||||
mutation: REMOVE_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
update(cache: ApolloCache<{ removeRelay: IFollower }>) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
relayFollowings(existingFollowingRefs, { readField }) {
|
||||
return {
|
||||
total: existingFollowingRefs.total - 1,
|
||||
elements: existingFollowingRefs.elements.filter(
|
||||
(followingRef: Reference) =>
|
||||
follower.id !== readField("id", followingRef)
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.checkedRows = [];
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -78,7 +78,7 @@
|
||||
/>
|
||||
<SettingMenuItem
|
||||
:title="$t('Federation')"
|
||||
:to="{ name: RouteName.RELAYS }"
|
||||
:to="{ name: RouteName.INSTANCES }"
|
||||
/>
|
||||
</SettingMenuSection>
|
||||
</ul>
|
||||
|
@ -70,6 +70,67 @@ export const RELAY_FOLLOWINGS = gql`
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const INSTANCE_FRAGMENT = gql`
|
||||
fragment InstanceFragment on Instance {
|
||||
domain
|
||||
hasRelay
|
||||
followerStatus
|
||||
followedStatus
|
||||
eventCount
|
||||
personCount
|
||||
groupCount
|
||||
followersCount
|
||||
followingsCount
|
||||
reportsCount
|
||||
mediaSize
|
||||
}
|
||||
`;
|
||||
|
||||
export const INSTANCE = gql`
|
||||
query instance($domain: ID!) {
|
||||
instance(domain: $domain) {
|
||||
...InstanceFragment
|
||||
}
|
||||
}
|
||||
${INSTANCE_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const INSTANCES = gql`
|
||||
query Instances(
|
||||
$page: Int
|
||||
$limit: Int
|
||||
$orderBy: InstancesSortFields
|
||||
$direction: String
|
||||
$filterDomain: String
|
||||
$filterFollowStatus: InstanceFilterFollowStatus
|
||||
$filterSuspendStatus: InstanceFilterSuspendStatus
|
||||
) {
|
||||
instances(
|
||||
page: $page
|
||||
limit: $limit
|
||||
orderBy: $orderBy
|
||||
direction: $direction
|
||||
filterDomain: $filterDomain
|
||||
filterFollowStatus: $filterFollowStatus
|
||||
filterSuspendStatus: $filterSuspendStatus
|
||||
) {
|
||||
total
|
||||
elements {
|
||||
...InstanceFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${INSTANCE_FRAGMENT}
|
||||
`;
|
||||
export const ADD_INSTANCE = gql`
|
||||
mutation addInstance($domain: String!) {
|
||||
addInstance(domain: $domain) {
|
||||
...InstanceFragment
|
||||
}
|
||||
}
|
||||
${INSTANCE_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const ADD_RELAY = gql`
|
||||
mutation addRelay($address: String!) {
|
||||
addRelay(address: $address) {
|
||||
|
@ -1260,5 +1260,24 @@
|
||||
"This profile was not found": "This profile was not found",
|
||||
"Back to profile list": "Back to profile list",
|
||||
"This user was not found": "This user was not found",
|
||||
"Back to user list": "Back to user list"
|
||||
}
|
||||
"Back to user list": "Back to user list",
|
||||
"Stop following instance": "Stop following instance",
|
||||
"Follow instance": "Follow instance",
|
||||
"Accept follow": "Accept follow",
|
||||
"Reject follow": "Reject follow",
|
||||
"This instance doesn't follow yours.": "This instance doesn't follow yours.",
|
||||
"Only Mobilizon instances can be followed": "",
|
||||
"Follow a new instance": "Follow a new instance",
|
||||
"Follow status": "Follow status",
|
||||
"All": "All",
|
||||
"Following": "Following",
|
||||
"Followed": "Followed",
|
||||
"Followed, pending response": "Followed, pending response",
|
||||
"Follows us": "Follows us",
|
||||
"Follows us, pending approval": "Follows us, pending approval",
|
||||
"No instance found.": "No instance found.",
|
||||
"No instances match this filter. Try resetting filter fields?": "No instances match this filter. Try resetting filter fields?",
|
||||
"You haven't interacted with other instances yet.": "You haven't interacted with other instances yet.",
|
||||
"mobilizon-instance.tld": "mobilizon-instance.tld",
|
||||
"Report status": "Report status"
|
||||
}
|
@ -1260,5 +1260,24 @@
|
||||
"{timezoneLongName} ({timezoneShortName})": "{timezoneLongName} ({timezoneShortName})",
|
||||
"{title} ({count} todos)": "{title} ({count} todos)",
|
||||
"{username} was invited to {group}": "{username} a été invité à {group}",
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap"
|
||||
}
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
|
||||
"Stop following instance": "Arrêter de suivre l'instance",
|
||||
"Follow instance": "Suivre l'instance",
|
||||
"Accept follow": "Accepter le suivi",
|
||||
"Reject follow": "Rejetter le suivi",
|
||||
"This instance doesn't follow yours.": "Cette instance ne suit pas la vôtre.",
|
||||
"Only Mobilizon instances can be followed": "Seules les instances Mobilizon peuvent être suivies",
|
||||
"Follow a new instance": "Suivre une nouvelle instance",
|
||||
"Follow status": "Statut du suivi",
|
||||
"All": "Toutes",
|
||||
"Following": "Suivantes",
|
||||
"Followed": "Suivies",
|
||||
"Followed, pending response": "Suivie, en attente de la réponse",
|
||||
"Follows us": "Nous suit",
|
||||
"Follows us, pending approval": "Nous suit, en attente de validation",
|
||||
"No instance found": "Aucune instance trouvée",
|
||||
"No instances match this filter. Try resetting filter fields?": "Aucune instance ne correspond à ce filtre. Essayer de remettre à zéro les champs des filtres ?",
|
||||
"You haven't interacted with other instances yet.": "Vous n'avez interagi avec encore aucune autre instance.",
|
||||
"mobilizon-instance.tld": "instance-mobilizon.tld",
|
||||
"Report status": "Statut du signalement"
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import { IActor } from "@/types/actor";
|
||||
import { ActorType } from "@/types/enums";
|
||||
import { Component, Vue, Ref } from "vue-property-decorator";
|
||||
import VueRouter from "vue-router";
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
|
||||
@Component
|
||||
export default class RelayMixin extends Vue {
|
||||
@Ref("table") readonly table!: any;
|
||||
|
||||
toggle(row: Record<string, unknown>): void {
|
||||
this.table.toggleDetails(row);
|
||||
}
|
||||
|
||||
protected async pushRouter(
|
||||
routeName: string,
|
||||
args: Record<string, string>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.$router.push({
|
||||
name: routeName,
|
||||
query: { ...this.$route.query, ...args },
|
||||
});
|
||||
} catch (e) {
|
||||
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
|
||||
throw Error(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static isInstance(actor: IActor): boolean {
|
||||
return (
|
||||
actor.type === ActorType.APPLICATION &&
|
||||
(actor.preferredUsername === "relay" ||
|
||||
actor.preferredUsername === actor.domain)
|
||||
);
|
||||
}
|
||||
}
|
@ -11,9 +11,8 @@ export enum SettingsRouteName {
|
||||
ADMIN = "ADMIN",
|
||||
ADMIN_DASHBOARD = "ADMIN_DASHBOARD",
|
||||
ADMIN_SETTINGS = "ADMIN_SETTINGS",
|
||||
RELAYS = "Relays",
|
||||
RELAY_FOLLOWINGS = "Followings",
|
||||
RELAY_FOLLOWERS = "Followers",
|
||||
INSTANCES = "INSTANCES",
|
||||
INSTANCE = "INSTANCE",
|
||||
USERS = "USERS",
|
||||
PROFILES = "PROFILES",
|
||||
ADMIN_PROFILE = "ADMIN_PROFILE",
|
||||
@ -199,44 +198,35 @@ export const settingsRoutes: RouteConfig[] = [
|
||||
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||
},
|
||||
{
|
||||
path: "admin/relays",
|
||||
name: SettingsRouteName.RELAYS,
|
||||
redirect: { name: SettingsRouteName.RELAY_FOLLOWINGS },
|
||||
path: "admin/instances",
|
||||
name: SettingsRouteName.INSTANCES,
|
||||
component: (): Promise<ImportedComponent> =>
|
||||
import(/* webpackChunkName: "Follows" */ "@/views/Admin/Follows.vue"),
|
||||
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||
children: [
|
||||
{
|
||||
path: "followings",
|
||||
name: SettingsRouteName.RELAY_FOLLOWINGS,
|
||||
component: (): Promise<ImportedComponent> =>
|
||||
import(
|
||||
/* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue"
|
||||
),
|
||||
meta: {
|
||||
requiredAuth: true,
|
||||
announcer: {
|
||||
message: (): string => i18n.t("Followings") as string,
|
||||
},
|
||||
},
|
||||
import(
|
||||
/* webpackChunkName: "Instances" */ "@/views/Admin/Instances.vue"
|
||||
),
|
||||
meta: {
|
||||
requiredAuth: true,
|
||||
announcer: {
|
||||
message: (): string => i18n.t("Instances") as string,
|
||||
},
|
||||
{
|
||||
path: "followers",
|
||||
name: SettingsRouteName.RELAY_FOLLOWERS,
|
||||
component: (): Promise<ImportedComponent> =>
|
||||
import(
|
||||
/* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue"
|
||||
),
|
||||
meta: {
|
||||
requiredAuth: true,
|
||||
announcer: {
|
||||
message: (): string => i18n.t("Followers") as string,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "admin/instances/:domain",
|
||||
name: SettingsRouteName.INSTANCE,
|
||||
component: (): Promise<ImportedComponent> =>
|
||||
import(
|
||||
/* webpackChunkName: "Instance" */ "@/views/Admin/Instance.vue"
|
||||
),
|
||||
props: true,
|
||||
meta: {
|
||||
requiredAuth: true,
|
||||
announcer: {
|
||||
message: (): string => i18n.t("Instance") as string,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/moderation",
|
||||
name: SettingsRouteName.MODERATION,
|
||||
|
@ -276,3 +276,15 @@ export enum EventMetadataCategories {
|
||||
BOOKING = "BOOKING",
|
||||
VIDEO_CONFERENCE = "VIDEO_CONFERENCE",
|
||||
}
|
||||
|
||||
export enum InstanceFilterFollowStatus {
|
||||
ALL = "ALL",
|
||||
FOLLOWING = "FOLLOWING",
|
||||
FOLLOWED = "FOLLOWED",
|
||||
}
|
||||
|
||||
export enum InstanceFollowStatus {
|
||||
APPROVED = "APPROVED",
|
||||
PENDING = "PENDING",
|
||||
NONE = "NONE",
|
||||
}
|
||||
|
14
js/src/types/instance.model.ts
Normal file
14
js/src/types/instance.model.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { InstanceFollowStatus } from "./enums";
|
||||
|
||||
export interface IInstance {
|
||||
domain: string;
|
||||
hasRelay: boolean;
|
||||
followerStatus: InstanceFollowStatus;
|
||||
followedStatus: InstanceFollowStatus;
|
||||
personCount: number;
|
||||
groupCount: number;
|
||||
followersCount: number;
|
||||
followingsCount: number;
|
||||
reportsCount: number;
|
||||
mediaSize: number;
|
||||
}
|
@ -384,6 +384,12 @@ export default class AdminProfile extends Vue {
|
||||
{
|
||||
key: this.$t("Domain") as string,
|
||||
value: this.person.domain ? this.person.domain : this.$t("Local"),
|
||||
link: this.person.domain
|
||||
? {
|
||||
name: RouteName.INSTANCE,
|
||||
params: { domain: this.person.domain },
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
key: this.$i18n.t("Uploaded media size"),
|
||||
|
@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.ADMIN }">{{
|
||||
$t("Admin")
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.RELAYS }">{{
|
||||
$t("Federation")
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li class="is-active" v-if="$route.name == RouteName.RELAY_FOLLOWINGS">
|
||||
<router-link :to="{ name: RouteName.RELAY_FOLLOWINGS }">{{
|
||||
$t("Followings")
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li class="is-active" v-if="$route.name == RouteName.RELAY_FOLLOWERS">
|
||||
<router-link :to="{ name: RouteName.RELAY_FOLLOWERS }">{{
|
||||
$t("Followers")
|
||||
}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<h1 class="title">{{ $t("Instances") }}</h1>
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<router-link
|
||||
tag="li"
|
||||
active-class="is-active"
|
||||
:to="{ name: RouteName.RELAY_FOLLOWINGS }"
|
||||
>
|
||||
<a>
|
||||
<b-icon icon="inbox-arrow-down"></b-icon>
|
||||
<span>
|
||||
{{ $t("Followings") }}
|
||||
<b-tag rounded>{{ relayFollowings.total }}</b-tag>
|
||||
</span>
|
||||
</a>
|
||||
</router-link>
|
||||
<router-link
|
||||
tag="li"
|
||||
active-class="is-active"
|
||||
:to="{ name: RouteName.RELAY_FOLLOWERS }"
|
||||
>
|
||||
<a>
|
||||
<b-icon icon="inbox-arrow-up"></b-icon>
|
||||
<span>
|
||||
{{ $t("Followers") }}
|
||||
<b-tag rounded>{{ relayFollowers.total }}</b-tag>
|
||||
</span>
|
||||
</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { RELAY_FOLLOWERS, RELAY_FOLLOWINGS } from "@/graphql/admin";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IFollower } from "@/types/actor/follower.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
relayFollowings: {
|
||||
query: RELAY_FOLLOWINGS,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
relayFollowers: {
|
||||
query: RELAY_FOLLOWERS,
|
||||
fetchPolicy: "cache-and-network",
|
||||
variables: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t("Federation") as string,
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Follows extends Vue {
|
||||
RouteName = RouteName;
|
||||
|
||||
activeTab = 0;
|
||||
|
||||
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
|
||||
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tab-item {
|
||||
form {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
</style>
|
268
js/src/views/Admin/Instance.vue
Normal file
268
js/src/views/Admin/Instance.vue
Normal file
@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div v-if="instance">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.ADMIN }">{{
|
||||
$t("Admin")
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.INSTANCES }">{{
|
||||
$t("Instances")
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.INSTANCE,
|
||||
params: { domain: instance.domain },
|
||||
}"
|
||||
>{{ instance.domain }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h1 class="text-2xl">{{ instance.domain }}</h1>
|
||||
<div class="grid md:grid-cols-4 gap-2 content-center text-center mt-2">
|
||||
<div class="bg-gray-50 rounded-xl p-8 dark:bg-gray-800">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILES,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
class="dark:text-white hover:dark:text-slate-300"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.personCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Profiles") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-8 dark:bg-gray-800">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.ADMIN_GROUPS,
|
||||
query: { domain: instance.domain },
|
||||
}"
|
||||
class="dark:text-white hover:dark:text-slate-300"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.groupCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Groups") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
class="bg-gray-50 rounded-xl p-8 dark:bg-gray-800 dark:text-white hover:dark:text-slate-300"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followingsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Followings") }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="bg-gray-50 rounded-xl p-8 dark:bg-gray-800 dark:text-white hover:dark:text-slate-300"
|
||||
>
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.followersCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Followers") }}</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-8 dark:bg-gray-800">
|
||||
<router-link to="/" class="dark:text-white hover:dark:text-slate-300">
|
||||
<span class="mb-4 text-xl font-semibold block">{{
|
||||
instance.reportsCount
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Reports") }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
class="bg-gray-50 rounded-xl p-8 dark:bg-gray-800 dark:text-white hover:dark:text-slate-300"
|
||||
>
|
||||
<span class="mb-4 font-semibold block">{{
|
||||
formatBytes(instance.mediaSize)
|
||||
}}</span>
|
||||
<span class="text-sm block">{{ $t("Uploaded media size") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 grid md:grid-cols-2 gap-4" v-if="instance.hasRelay">
|
||||
<div class="border bg-white p-6 shadow-md rounded-md">
|
||||
<button
|
||||
@click="removeInstanceFollow"
|
||||
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Stop following instance") }}
|
||||
</button>
|
||||
<button
|
||||
@click="removeInstanceFollow"
|
||||
v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Cancel follow request") }}
|
||||
</button>
|
||||
<button
|
||||
@click="followInstance"
|
||||
v-else
|
||||
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Follow instance") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="border bg-white p-6 shadow-md rounded-md">
|
||||
<button
|
||||
@click="acceptInstance"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
|
||||
class="bg-green-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Accept follow") }}
|
||||
</button>
|
||||
<button
|
||||
@click="rejectInstance"
|
||||
v-else-if="instance.followerStatus != InstanceFollowStatus.NONE"
|
||||
class="bg-red-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
|
||||
>
|
||||
{{ $t("Reject follow") }}
|
||||
</button>
|
||||
<p v-else>
|
||||
{{ $t("This instance doesn't follow yours.") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="md:h-48 py-16 text-center opacity-50">
|
||||
{{ $t("Only Mobilizon instances can be followed") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {
|
||||
ACCEPT_RELAY,
|
||||
ADD_INSTANCE,
|
||||
INSTANCE,
|
||||
REJECT_RELAY,
|
||||
REMOVE_RELAY,
|
||||
} from "@/graphql/admin";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { formatBytes } from "@/utils/datetime";
|
||||
import RouteName from "@/router/name";
|
||||
import { SnackbarProgrammatic as Snackbar } from "buefy";
|
||||
import { IInstance } from "@/types/instance.model";
|
||||
import { ApolloCache, gql, Reference } from "@apollo/client/core";
|
||||
import { InstanceFollowStatus } from "@/types/enums";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
instance: {
|
||||
query: INSTANCE,
|
||||
variables() {
|
||||
return {
|
||||
domain: this.domain,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Instance extends Vue {
|
||||
@Prop({ type: String, required: true }) domain!: string;
|
||||
|
||||
instance!: IInstance;
|
||||
|
||||
InstanceFollowStatus = InstanceFollowStatus;
|
||||
|
||||
formatBytes = formatBytes;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async acceptInstance(): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: ACCEPT_RELAY,
|
||||
variables: {
|
||||
address: `relay@${this.domain}`,
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async rejectInstance(): Promise<void> {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REJECT_RELAY,
|
||||
variables: {
|
||||
address: `relay@${this.domain}`,
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async followInstance(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await this.$apollo.mutate<{ addInstance: Instance }>({
|
||||
mutation: ADD_INSTANCE,
|
||||
variables: {
|
||||
domain: this.domain,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err.message) {
|
||||
Snackbar.open({
|
||||
message: err.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async removeInstanceFollow(): Promise<void> {
|
||||
const { instance } = this;
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REMOVE_RELAY,
|
||||
variables: {
|
||||
address: `relay@${this.domain}`,
|
||||
},
|
||||
update(cache: ApolloCache<any>) {
|
||||
cache.writeFragment({
|
||||
id: cache.identify(instance as unknown as Reference),
|
||||
fragment: gql`
|
||||
fragment InstanceFollowedStatus on Instance {
|
||||
followedStatus
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
followedStatus: InstanceFollowStatus.NONE,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.message) {
|
||||
Snackbar.open({
|
||||
message: e.message,
|
||||
type: "is-danger",
|
||||
position: "is-bottom",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
305
js/src/views/Admin/Instances.vue
Normal file
305
js/src/views/Admin/Instances.vue
Normal file
@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div>
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.ADMIN }">{{
|
||||
$t("Admin")
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link :to="{ name: RouteName.INSTANCES }">{{
|
||||
$t("Instances")
|
||||
}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<h1 class="title">{{ $t("Instances") }}</h1>
|
||||
<form @submit="followInstance" class="my-4">
|
||||
<b-field
|
||||
:label="$t('Follow a new instance')"
|
||||
custom-class="add-relay"
|
||||
horizontal
|
||||
>
|
||||
<b-field grouped expanded size="is-large">
|
||||
<p class="control">
|
||||
<b-input
|
||||
v-model="newRelayAddress"
|
||||
:placeholder="$t('Ex: mobilizon.fr')"
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">{{
|
||||
$t("Add an instance")
|
||||
}}</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
</form>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<b-field :label="$t('Follow status')">
|
||||
<b-radio-button
|
||||
v-model="followStatus"
|
||||
:native-value="InstanceFilterFollowStatus.ALL"
|
||||
>{{ $t("All") }}</b-radio-button
|
||||
>
|
||||
<b-radio-button
|
||||
v-model="followStatus"
|
||||
:native-value="InstanceFilterFollowStatus.FOLLOWING"
|
||||
>{{ $t("Following") }}</b-radio-button
|
||||
>
|
||||
<b-radio-button
|
||||
v-model="followStatus"
|
||||
:native-value="InstanceFilterFollowStatus.FOLLOWED"
|
||||
>{{ $t("Followed") }}</b-radio-button
|
||||
>
|
||||
</b-field>
|
||||
<b-field
|
||||
:label="$t('Domain')"
|
||||
label-for="domain-filter"
|
||||
class="flex-auto"
|
||||
>
|
||||
<b-input
|
||||
id="domain-filter"
|
||||
:placeholder="$t('mobilizon-instance.tld')"
|
||||
:value="filterDomain"
|
||||
@input="debouncedUpdateDomainFilter"
|
||||
/>
|
||||
</b-field>
|
||||
</div>
|
||||
<div v-if="instances && instances.elements.length > 0" class="mt-3">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.INSTANCE,
|
||||
params: { domain: instance.domain },
|
||||
}"
|
||||
class="flex items-center mb-2 rounded bg-secondary p-4 flex-wrap justify-center gap-x-2 gap-y-3"
|
||||
v-for="instance in instances.elements"
|
||||
:key="instance.domain"
|
||||
>
|
||||
<div class="grow overflow-hidden flex items-center gap-1">
|
||||
<img
|
||||
class="w-12"
|
||||
v-if="instance.hasRelay"
|
||||
src="../../assets/logo.svg"
|
||||
alt=""
|
||||
/>
|
||||
<b-icon
|
||||
class="is-large"
|
||||
v-else
|
||||
custom-size="mdi-36px"
|
||||
icon="cloud-question"
|
||||
/>
|
||||
<div class="">
|
||||
<h4 class="text-lg truncate">{{ instance.domain }}</h4>
|
||||
<span
|
||||
class="text-sm"
|
||||
v-if="instance.followedStatus === InstanceFollowStatus.APPROVED"
|
||||
>
|
||||
<b-icon icon="inbox-arrow-down" />
|
||||
{{ $t("Followed") }}</span
|
||||
>
|
||||
<span
|
||||
class="text-sm"
|
||||
v-else-if="
|
||||
instance.followedStatus === InstanceFollowStatus.PENDING
|
||||
"
|
||||
>
|
||||
<b-icon icon="inbox-arrow-down" />
|
||||
{{ $t("Followed, pending response") }}</span
|
||||
>
|
||||
<span
|
||||
class="text-sm"
|
||||
v-if="instance.followerStatus == InstanceFollowStatus.APPROVED"
|
||||
>
|
||||