clear memory cache for instances statistics when appropriate

Add a loading state to buttons InstanceView

Automatically refresh DashboardView data

Fixes #1915
This commit is contained in:
Massedil
2025-12-17 14:15:19 +01:00
parent 33e0d13b4e
commit 2c0adc8670
5 changed files with 102 additions and 68 deletions

View File

@@ -608,6 +608,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
def remove_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}}) def remove_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do when is_admin(role) do
with {:ok, _activity, follow} <- Relay.unfollow(address) do with {:ok, _activity, follow} <- Relay.unfollow(address) do
Statistics.clear_cached_value(:instance_followers)
Statistics.clear_cached_value(:instance_followings)
{:ok, follow} {:ok, follow}
end end
end end
@@ -621,6 +623,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
) )
when is_admin(role) do when is_admin(role) do
with {:ok, _activity, follow} <- Relay.accept(address) do with {:ok, _activity, follow} <- Relay.accept(address) do
Statistics.clear_cached_value(:instance_followers)
Statistics.clear_cached_value(:instance_followings)
{:ok, follow} {:ok, follow}
end end
end end
@@ -634,6 +638,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
) )
when is_admin(role) do when is_admin(role) do
with {:ok, _activity, follow} <- Relay.reject(address) do with {:ok, _activity, follow} <- Relay.reject(address) do
Statistics.clear_cached_value(:instance_followers)
Statistics.clear_cached_value(:instance_followings)
{:ok, follow} {:ok, follow}
end end
end end

View File

@@ -20,6 +20,10 @@ defmodule Mobilizon.Service.Statistics do
end end
end end
def clear_cached_value(key) do
Cachex.del(:statistics, key)
end
defp create_cache(:local_users) do defp create_cache(:local_users) do
Users.count_users() Users.count_users()
end end

View File

@@ -50,7 +50,7 @@
" "
:to="{ :to="{
name: RouteName.INSTANCES, name: RouteName.INSTANCES,
query: { followStatus: InstanceFilterFollowStatus.FOLLOWING }, query: { followStatus: InstanceFilterFollowStatus.THEY_FOLLOW_US },
}" }"
/> />
<LinkedNumberDashboardTile <LinkedNumberDashboardTile
@@ -60,7 +60,7 @@
" "
:to="{ :to="{
name: RouteName.INSTANCES, name: RouteName.INSTANCES,
query: { followStatus: InstanceFilterFollowStatus.FOLLOWED }, query: { followStatus: InstanceFilterFollowStatus.WE_FOLLOW_THEM },
}" }"
/> />
</div> </div>
@@ -98,7 +98,11 @@ import GroupCard from "@/components/Group/GroupCard.vue";
import EventCard from "@/components/Event/EventCard.vue"; import EventCard from "@/components/Event/EventCard.vue";
const { result: dashboardResult } = useQuery<{ dashboard: IDashboard }>( const { result: dashboardResult } = useQuery<{ dashboard: IDashboard }>(
DASHBOARD DASHBOARD,
{},
{
fetchPolicy: "cache-and-network",
}
); );
const dashboard = computed(() => dashboardResult.value?.dashboard); const dashboard = computed(() => dashboardResult.value?.dashboard);

View File

@@ -119,35 +119,41 @@
) )
" "
> >
<button <o-button
@click=" @click="
removeInstanceFollow({ removeInstanceFollow({
address: instance?.relayAddress, address: instance?.relayAddress,
}) })
" "
:loading="removeInstanceFollowLoading"
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED" v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
variant="primary"
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" 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") }} {{ t("Stop following instance") }}
</button> </o-button>
<button <o-button
@click=" @click="
removeInstanceFollow({ removeInstanceFollow({
address: instance?.relayAddress, address: instance?.relayAddress,
}) })
" "
:loading="removeInstanceFollowLoading"
v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING" v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING"
variant="primary"
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" 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") }} {{ t("Cancel follow request") }}
</button> </o-button>
<button <o-button
@click="followInstance" @click="followInstance"
:loading="followInstanceLoading"
v-else v-else
variant="primary"
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" 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") }} {{ t("Follow instance") }}
</button> </o-button>
</div> </div>
<div <div
v-else v-else
@@ -158,31 +164,35 @@
<div <div
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2 justify-center" class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2 justify-center"
> >
<button <o-button
@click=" @click="
acceptInstance({ acceptInstance({
address: instance?.relayAddress, address: instance?.relayAddress,
}) })
" "
:loading="acceptInstanceLoading"
v-if="instance.followerStatus == InstanceFollowStatus.PENDING" v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
variant="primary"
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" 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") }} {{ t("Accept follow") }}
</button> </o-button>
<button <o-button
@click=" @click="
rejectInstance({ rejectInstance({
address: instance?.relayAddress, address: instance?.relayAddress,
}) })
" "
:loading="rejectInstanceLoading"
v-if=" v-if="
instance.followerStatus == InstanceFollowStatus.PENDING || instance.followerStatus == InstanceFollowStatus.PENDING ||
instance.followerStatus == InstanceFollowStatus.APPROVED instance.followerStatus == InstanceFollowStatus.APPROVED
" "
variant="primary"
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" 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") }} {{ t("Reject follow") }}
</button> </o-button>
<p v-if="instance.followerStatus == InstanceFollowStatus.NONE"> <p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
{{ t("This instance doesn't follow yours.") }} {{ t("This instance doesn't follow yours.") }}
</p> </p>
@@ -222,24 +232,25 @@ const notifier = inject<Notifier>("notifier");
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const { mutate: acceptInstance, onError: onAcceptInstanceError } = useMutation( const {
ACCEPT_RELAY, mutate: acceptInstance,
() => ({ loading: acceptInstanceLoading,
update(cache: ApolloCache<any>) { onError: onAcceptInstanceError,
cache.writeFragment({ } = useMutation(ACCEPT_RELAY, () => ({
id: cache.identify(instance.value as unknown as Reference), update(cache: ApolloCache<any>) {
fragment: gql` cache.writeFragment({
fragment InstanceFollowerStatus on Instance { id: cache.identify(instance.value as unknown as Reference),
followerStatus fragment: gql`
} fragment InstanceFollowerStatus on Instance {
`, followerStatus
data: { }
followerStatus: InstanceFollowStatus.APPROVED, `,
}, data: {
}); followerStatus: InstanceFollowStatus.APPROVED,
}, },
}) });
); },
}));
onAcceptInstanceError((error) => { onAcceptInstanceError((error) => {
if (error.graphQLErrors && error.graphQLErrors.length > 0) { if (error.graphQLErrors && error.graphQLErrors.length > 0) {
@@ -250,24 +261,25 @@ onAcceptInstanceError((error) => {
/** /**
* Reject instance follow * Reject instance follow
*/ */
const { mutate: rejectInstance, onError: onRejectInstanceError } = useMutation( const {
REJECT_RELAY, mutate: rejectInstance,
() => ({ loading: rejectInstanceLoading,
update(cache: ApolloCache<any>) { onError: onRejectInstanceError,
cache.writeFragment({ } = useMutation(REJECT_RELAY, () => ({
id: cache.identify(instance.value as unknown as Reference), update(cache: ApolloCache<any>) {
fragment: gql` cache.writeFragment({
fragment InstanceFollowerStatus on Instance { id: cache.identify(instance.value as unknown as Reference),
followerStatus fragment: gql`
} fragment InstanceFollowerStatus on Instance {
`, followerStatus
data: { }
followerStatus: InstanceFollowStatus.NONE, `,
}, data: {
}); followerStatus: InstanceFollowStatus.NONE,
}, },
}) });
); },
}));
onRejectInstanceError((error) => { onRejectInstanceError((error) => {
if (error.graphQLErrors && error.graphQLErrors.length > 0) { if (error.graphQLErrors && error.graphQLErrors.length > 0) {
@@ -275,8 +287,11 @@ onRejectInstanceError((error) => {
} }
}); });
const { mutate: followInstanceMutation, onError: onFollowInstanceError } = const {
useMutation<{ addInstance: IInstance }>(ADD_INSTANCE); mutate: followInstanceMutation,
loading: followInstanceLoading,
onError: onFollowInstanceError,
} = useMutation<{ addInstance: IInstance }>(ADD_INSTANCE);
onFollowInstanceError((error) => { onFollowInstanceError((error) => {
if (error.graphQLErrors && error.graphQLErrors.length > 0) { if (error.graphQLErrors && error.graphQLErrors.length > 0) {
@@ -292,22 +307,25 @@ const followInstance = async (e: Event): Promise<void> => {
/** /**
* Stop following instance * Stop following instance
*/ */
const { mutate: removeInstanceFollow, onError: onRemoveInstanceFollowError } = const {
useMutation(REMOVE_RELAY, () => ({ mutate: removeInstanceFollow,
update(cache: ApolloCache<any>) { loading: removeInstanceFollowLoading,
cache.writeFragment({ onError: onRemoveInstanceFollowError,
id: cache.identify(instance.value as unknown as Reference), } = useMutation(REMOVE_RELAY, () => ({
fragment: gql` update(cache: ApolloCache<any>) {
fragment InstanceFollowedStatus on Instance { cache.writeFragment({
followedStatus id: cache.identify(instance.value as unknown as Reference),
} fragment: gql`
`, fragment InstanceFollowedStatus on Instance {
data: { followedStatus
followedStatus: InstanceFollowStatus.NONE, }
}, `,
}); data: {
}, followedStatus: InstanceFollowStatus.NONE,
})); },
});
},
}));
onRemoveInstanceFollowError((error) => { onRemoveInstanceFollowError((error) => {
if (error.graphQLErrors && error.graphQLErrors.length > 0) { if (error.graphQLErrors && error.graphQLErrors.length > 0) {

View File

@@ -21,7 +21,9 @@ exports[`InstanceView > Show simple 1`] = `
</section> </section>
<section> <section>
<div class="mt-3 grid xl:grid-cols-2 gap-4"> <div class="mt-3 grid xl:grid-cols-2 gap-4">
<div class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2 justify-center"><button 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">Follow instance</button></div> <div class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2 justify-center"><button data-oruga="button" type="button" role="button" tabindex="0" class="o-button o-button--primary 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"><span class="o-button__wrapper"><!----><span class="o-button__label">Follow instance</span>
<!----></span>
</button></div>
<div class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2 justify-center"> <div class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2 justify-center">
<!--v-if--> <!--v-if-->
<!--v-if--> <!--v-if-->