48935e2168
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
283 lines
8.5 KiB
Vue
283 lines
8.5 KiB
Vue
<template>
|
|
<div>
|
|
<breadcrumbs-nav
|
|
:links="[
|
|
{ name: RouteName.ADMIN, text: t('Admin') },
|
|
{ text: t('Instances') },
|
|
]"
|
|
/>
|
|
<section>
|
|
<h1 class="title">{{ t("Instances") }}</h1>
|
|
<form @submit="followInstance" class="my-4">
|
|
<o-field
|
|
:label="t('Follow a new instance')"
|
|
horizontal
|
|
label-for="newRelayAddress"
|
|
>
|
|
<o-field grouped group-multiline expanded size="large">
|
|
<p class="control">
|
|
<o-input
|
|
id="newRelayAddress"
|
|
v-model="newRelayAddress"
|
|
:placeholder="t('Ex: mobilizon.fr')"
|
|
/>
|
|
</p>
|
|
<p class="control">
|
|
<o-button variant="primary" native-type="submit">{{
|
|
t("Add an instance")
|
|
}}</o-button>
|
|
<o-loading
|
|
:is-full-page="true"
|
|
v-model="followInstanceLoading"
|
|
:can-cancel="false"
|
|
/>
|
|
</p>
|
|
</o-field>
|
|
</o-field>
|
|
</form>
|
|
<div class="flex flex-wrap gap-2">
|
|
<o-field :label="t('Follow status')">
|
|
<o-radio
|
|
v-model="followStatus"
|
|
:native-value="InstanceFilterFollowStatus.ALL"
|
|
>{{ t("All") }}</o-radio
|
|
>
|
|
<o-radio
|
|
v-model="followStatus"
|
|
:native-value="InstanceFilterFollowStatus.FOLLOWING"
|
|
>{{ t("Following") }}</o-radio
|
|
>
|
|
<o-radio
|
|
v-model="followStatus"
|
|
:native-value="InstanceFilterFollowStatus.FOLLOWED"
|
|
>{{ t("Followed") }}</o-radio
|
|
>
|
|
</o-field>
|
|
<o-field
|
|
:label="t('Domain')"
|
|
label-for="domain-filter"
|
|
class="flex-auto"
|
|
>
|
|
<o-input
|
|
id="domain-filter"
|
|
:placeholder="t('mobilizon-instance.tld')"
|
|
:value="filterDomain"
|
|
@input="debouncedUpdateDomainFilter"
|
|
/>
|
|
</o-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-mbz-yellow-alt-300 dark:bg-mbz-purple-400 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="/img/logo.svg"
|
|
alt=""
|
|
/>
|
|
<CloudQuestion v-else :size="36" />
|
|
|
|
<div class="">
|
|
<h3 class="text-lg truncate">{{ instance.domain }}</h3>
|
|
<span
|
|
class="text-sm"
|
|
v-if="instance.followedStatus === InstanceFollowStatus.APPROVED"
|
|
>
|
|
<o-icon icon="inbox-arrow-down" />
|
|
{{ t("Followed") }}</span
|
|
>
|
|
<span
|
|
class="text-sm"
|
|
v-else-if="
|
|
instance.followedStatus === InstanceFollowStatus.PENDING
|
|
"
|
|
>
|
|
<o-icon icon="inbox-arrow-down" />
|
|
{{ t("Followed, pending response") }}</span
|
|
>
|
|
<span
|
|
class="text-sm"
|
|
v-if="instance.followerStatus == InstanceFollowStatus.APPROVED"
|
|
>
|
|
<o-icon icon="inbox-arrow-up" />
|
|
{{ t("Follows us") }}</span
|
|
>
|
|
<span
|
|
class="text-sm"
|
|
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
|
|
>
|
|
<o-icon icon="inbox-arrow-up" />
|
|
{{ t("Follows us, pending approval") }}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="flex-none flex gap-3 ltr:ml-3 rtl:mr-3">
|
|
<p class="flex flex-col text-center">
|
|
<span class="text-xl">{{ instance.eventCount }}</span
|
|
><span class="text-sm">{{ t("Events") }}</span>
|
|
</p>
|
|
<p class="flex flex-col text-center">
|
|
<span class="text-xl">{{ instance.personCount }}</span
|
|
><span class="text-sm">{{ t("Profiles") }}</span>
|
|
</p>
|
|
</div>
|
|
</router-link>
|
|
<o-pagination
|
|
v-show="instances.total > INSTANCES_PAGE_LIMIT"
|
|
:total="instances.total"
|
|
v-model="instancePage"
|
|
:per-page="INSTANCES_PAGE_LIMIT"
|
|
:aria-next-label="t('Next page')"
|
|
:aria-previous-label="t('Previous page')"
|
|
:aria-page-label="t('Page')"
|
|
:aria-current-label="t('Current page')"
|
|
>
|
|
</o-pagination>
|
|
</div>
|
|
<div v-else-if="instances && instances.elements.length == 0">
|
|
<empty-content icon="lan-disconnect" :inline="true">
|
|
{{ t("No instance found.") }}
|
|
<template #desc>
|
|
<span v-if="hasFilter">
|
|
{{
|
|
t(
|
|
"No instances match this filter. Try resetting filter fields?"
|
|
)
|
|
}}
|
|
</span>
|
|
<span v-else>
|
|
{{ t("You haven't interacted with other instances yet.") }}
|
|
</span>
|
|
</template>
|
|
</empty-content>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ADD_INSTANCE, INSTANCES } from "@/graphql/admin";
|
|
import { Paginate } from "@/types/paginate";
|
|
import RouteName from "../../router/name";
|
|
import { IInstance } from "@/types/instance.model";
|
|
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
|
import debounce from "lodash/debounce";
|
|
import {
|
|
InstanceFilterFollowStatus,
|
|
InstanceFollowStatus,
|
|
} from "@/types/enums";
|
|
import { useI18n } from "vue-i18n";
|
|
import {
|
|
enumTransformer,
|
|
integerTransformer,
|
|
useRouteQuery,
|
|
} from "vue-use-route-query";
|
|
import { useMutation, useQuery } from "@vue/apollo-composable";
|
|
import { computed, inject, ref } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
import { useHead } from "@vueuse/head";
|
|
import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue";
|
|
import { Notifier } from "@/plugins/notifier";
|
|
|
|
const INSTANCES_PAGE_LIMIT = 10;
|
|
|
|
const instancePage = useRouteQuery("page", 1, integerTransformer);
|
|
const filterDomain = useRouteQuery("filterDomain", "");
|
|
const followStatus = useRouteQuery(
|
|
"followStatus",
|
|
InstanceFilterFollowStatus.ALL,
|
|
enumTransformer(InstanceFilterFollowStatus)
|
|
);
|
|
|
|
const { result: instancesResult } = useQuery<{
|
|
instances: Paginate<IInstance>;
|
|
}>(INSTANCES, () => ({
|
|
page: instancePage.value,
|
|
limit: INSTANCES_PAGE_LIMIT,
|
|
filterDomain: filterDomain.value,
|
|
filterFollowStatus: followStatus.value,
|
|
}));
|
|
|
|
const instances = computed(() => instancesResult.value?.instances);
|
|
|
|
const { t } = useI18n({ useScope: "global" });
|
|
useHead({
|
|
title: computed(() => t("Federation")),
|
|
});
|
|
|
|
const followInstanceLoading = ref(false);
|
|
|
|
const newRelayAddress = ref("");
|
|
|
|
// relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
|
|
|
|
// relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
|
|
|
const updateDomainFilter = (domain: string) => {
|
|
filterDomain.value = domain;
|
|
};
|
|
|
|
const debouncedUpdateDomainFilter = debounce(updateDomainFilter, 500);
|
|
|
|
const hasFilter = computed((): boolean => {
|
|
return (
|
|
followStatus.value !== InstanceFilterFollowStatus.ALL ||
|
|
filterDomain.value !== ""
|
|
);
|
|
});
|
|
|
|
const router = useRouter();
|
|
|
|
const { mutate, onDone, onError } = useMutation<{
|
|
addInstance: IInstance;
|
|
}>(ADD_INSTANCE);
|
|
|
|
onDone(({ data }) => {
|
|
newRelayAddress.value = "";
|
|
followInstanceLoading.value = false;
|
|
router.push({
|
|
name: RouteName.INSTANCE,
|
|
params: { domain: data?.addInstance.domain },
|
|
});
|
|
});
|
|
|
|
const notifier = inject<Notifier>("notifier");
|
|
|
|
onError((error) => {
|
|
if (error.message) {
|
|
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
|
|
notifier?.error(error.graphQLErrors[0].message);
|
|
}
|
|
}
|
|
followInstanceLoading.value = false;
|
|
});
|
|
|
|
const followInstance = async (e: Event): Promise<void> => {
|
|
e.preventDefault();
|
|
followInstanceLoading.value = true;
|
|
const domain = newRelayAddress.value.trim(); // trim to fix copy and paste domain name spaces and tabs
|
|
mutate({
|
|
domain,
|
|
});
|
|
};
|
|
</script>
|
|
<style lang="scss" scoped>
|
|
.tab-item {
|
|
form {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
}
|
|
|
|
a {
|
|
text-decoration: none !important;
|
|
}
|
|
</style>
|