Skip to content

Commit 3911da7

Browse files
committed
Add new admin instance manage page. Fixes #448
1 parent 15fce05 commit 3911da7

8 files changed

Lines changed: 646 additions & 18 deletions

File tree

app/Http/Controllers/Api/AdminController.php

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,11 @@ public function instances(Request $request)
743743
if (str_starts_with($search, 'software:')) {
744744
$software = trim(substr($search, 9));
745745
if (! empty($software)) {
746-
$query->where('software', 'like', $software.'%');
746+
if ($software === 'unknown') {
747+
$query->whereNull('software');
748+
} else {
749+
$query->where('software', 'like', $software.'%');
750+
}
747751
}
748752
} elseif (str_starts_with($search, 'description:')) {
749753
$desc = trim(substr($search, 12));
@@ -893,6 +897,8 @@ public function updateInstanceRefreshData(Request $request, $id)
893897
$instance->report_count = Report::where('domain', $instance->domain)->count();
894898
$instance->save();
895899

900+
FetchInstanceNodeinfo::dispatch($instance)->onQueue('actor-update');
901+
896902
return $this->success();
897903
}
898904

@@ -926,11 +932,11 @@ public function instanceStats(Request $request)
926932
{
927933
$res = [
928934
[
929-
'name' => 'Total Instances',
935+
'name' => 'Total',
930936
'value' => Instance::whereNotNull('software')->whereFederationState(5)->count(),
931937
],
932938
[
933-
'name' => 'New (past 24h)',
939+
'name' => 'New',
934940
'value' => Instance::whereNotNull('software')->whereFederationState(5)->where('created_at', '>', now()->subHours(24))->count(),
935941
],
936942
[
@@ -946,6 +952,78 @@ public function instanceStats(Request $request)
946952
return $this->data($res);
947953
}
948954

955+
public function instanceAdvancedStats()
956+
{
957+
$totalInstances = Instance::count();
958+
$activeInstances = Instance::where('is_blocked', false)->count();
959+
$allowedVideoPosts = Instance::where('allow_video_posts', true)->count();
960+
$allowedInFyf = Instance::where('allow_videos_in_fyf', true)->count();
961+
962+
$softwareStats = Instance::select('software')
963+
->selectRaw('COUNT(*) as count')
964+
->selectRaw('SUM(CASE WHEN allow_video_posts = 1 THEN 1 ELSE 0 END) as allow_video_posts_count')
965+
->groupBy('software')
966+
->orderByDesc('count')
967+
->get();
968+
969+
return response()->json([
970+
'data' => [
971+
'stats' => [
972+
['name' => 'Total Instances', 'value' => $totalInstances],
973+
['name' => 'Active Instances', 'value' => $activeInstances],
974+
['name' => 'Video Posts Allowed', 'value' => $allowedVideoPosts],
975+
['name' => 'FYF Allowed', 'value' => $allowedInFyf],
976+
],
977+
'software_stats' => $softwareStats,
978+
],
979+
]);
980+
}
981+
982+
public function manageInstanceToggleBySoftware(Request $request)
983+
{
984+
$request->validate([
985+
'software' => 'required|string',
986+
'allow_video_posts' => 'required|boolean',
987+
]);
988+
989+
$software = $request->software;
990+
$allowVideoPosts = (bool) $request->allow_video_posts;
991+
$updatedCount = Instance::where('software', $software)
992+
->update(['allow_video_posts' => $request->allow_video_posts]);
993+
994+
app(AdminAuditLogService::class)->logInstanceSoftwareUpdateAllowVideoPosts($request->user(), ['software' => $software, 'allow_video_posts' => $allowVideoPosts]);
995+
996+
return response()->json([
997+
'data' => [
998+
'updated_count' => $updatedCount,
999+
],
1000+
]);
1001+
}
1002+
1003+
public function manageInstanceToggleByDomains(Request $request)
1004+
{
1005+
$request->validate([
1006+
'domains' => 'required|array',
1007+
'domains.*' => 'string',
1008+
'allow_video_posts' => 'required|boolean',
1009+
]);
1010+
1011+
$domains = array_map(function ($domain) {
1012+
return parse_url($domain, PHP_URL_HOST) ?: $domain;
1013+
}, $request->domains);
1014+
1015+
$updatedCount = Instance::whereIn('domain', $domains)
1016+
->update(['allow_video_posts' => $request->allow_video_posts]);
1017+
1018+
app(AdminAuditLogService::class)->logInstanceDomainsUpdateAllowVideoPosts($request->user(), ['domains' => $domains, 'allow_video_posts' => $request->allow_video_posts]);
1019+
1020+
return response()->json([
1021+
'data' => [
1022+
'updated_count' => $updatedCount,
1023+
],
1024+
]);
1025+
}
1026+
9491027
public function instanceCreate(Request $request)
9501028
{
9511029
$validated = $request->validate([
@@ -1156,6 +1234,10 @@ public function deleteAdminInvite(Request $request, $id)
11561234

11571235
private function applySorting($query, $sort)
11581236
{
1237+
if ($sort === 'allow_video_posts') {
1238+
return $query->where('allow_video_posts', 1);
1239+
}
1240+
11591241
if ($sort === 'unprocessed') {
11601242
return $query->where('status', 1);
11611243
}

app/Services/AdminAuditLogService.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ public function logInstanceUpdateSettings(User|int $user, $instance, $changes)
5757
return $this->log($user, 'instance:updated_settings', $changes, $instance, null, 1);
5858
}
5959

60+
public function logInstanceSoftwareUpdateAllowVideoPosts(User|int $user, $changes)
61+
{
62+
return $this->log($user, 'instance:software_allow_video_posts', $changes, null, null, 1);
63+
}
64+
65+
public function logInstanceDomainsUpdateAllowVideoPosts(User|int $user, $changes)
66+
{
67+
return $this->log($user, 'instance:domains_allow_video_posts', $changes, null, null, 1);
68+
}
69+
6070
public function logInstanceUpdateNotes(User|int $user, $instance, $changes)
6171
{
6272
return $this->log($user, 'instance:update_notes', $changes, $instance, null, 1);

resources/js/components/DataTable.vue

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,38 @@
6666
</div>
6767
</div>
6868

69+
<div
70+
v-if="hasActiveFilters"
71+
class="px-4 py-3 bg-blue-50 dark:bg-blue-900/20 border-b border-blue-200 dark:border-blue-800"
72+
>
73+
<div class="flex items-center justify-between flex-col sm:flex-row gap-2">
74+
<div class="flex items-start sm:items-center gap-2 w-full sm:w-auto">
75+
<InformationCircleIcon
76+
class="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5 sm:mt-0"
77+
/>
78+
<p class="text-sm text-blue-800 dark:text-blue-200">
79+
<span class="font-medium">Filtered results:</span>
80+
<span class="ml-1">
81+
You are viewing
82+
<template v-if="searchQuery && selectedSort">
83+
{{ filterDescription }}
84+
</template>
85+
<template v-else-if="searchQuery">
86+
search results for "{{ searchQuery }}"
87+
</template>
88+
<template v-else> sorted results </template>
89+
</span>
90+
</p>
91+
</div>
92+
<button
93+
@click="clearFilters"
94+
class="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline whitespace-nowrap transition-colors"
95+
>
96+
Clear filters
97+
</button>
98+
</div>
99+
</div>
100+
69101
<div class="overflow-x-auto">
70102
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
71103
<thead class="bg-gray-50 dark:bg-gray-700">
@@ -180,8 +212,13 @@
180212
</template>
181213

182214
<script setup>
183-
import { ref, watch } from 'vue'
184-
import { MagnifyingGlassIcon, ArrowPathIcon, ChevronDownIcon } from '@heroicons/vue/24/outline'
215+
import { ref, watch, computed } from 'vue'
216+
import {
217+
MagnifyingGlassIcon,
218+
ArrowPathIcon,
219+
ChevronDownIcon,
220+
InformationCircleIcon
221+
} from '@heroicons/vue/24/outline'
185222
186223
const props = defineProps({
187224
title: String,
@@ -202,6 +239,10 @@ const props = defineProps({
202239
type: String,
203240
default: ''
204241
},
242+
initialSort: {
243+
type: String,
244+
default: ''
245+
},
205246
showLocalFilter: {
206247
type: Boolean,
207248
default: false
@@ -215,7 +256,7 @@ const props = defineProps({
215256
const emit = defineEmits(['search', 'refresh', 'previous', 'next', 'sort', 'localChange'])
216257
217258
const searchQuery = ref(props.initialSearchQuery)
218-
const selectedSort = ref('')
259+
const selectedSort = ref(props.initialSort)
219260
const localFilter = ref(props.initialLocalFilter)
220261
221262
watch(
@@ -225,11 +266,36 @@ watch(
225266
}
226267
)
227268
269+
const hasActiveFilters = computed(() => {
270+
return searchQuery.value.trim() !== '' || selectedSort.value !== ''
271+
})
272+
273+
const filterDescription = computed(() => {
274+
const parts = []
275+
if (searchQuery.value) {
276+
parts.push(`search results for "${searchQuery.value}"`)
277+
}
278+
if (selectedSort.value) {
279+
const sortOption = props.sortOptions.find((opt) => opt.value === selectedSort.value)
280+
if (sortOption) {
281+
parts.push(`sorted by ${sortOption.name.toLowerCase()}`)
282+
}
283+
}
284+
return parts.join(' and ')
285+
})
286+
228287
const handleSortChange = () => {
229288
emit('sort', selectedSort.value)
230289
}
231290
232291
const handleLocalChange = () => {
233292
emit('localChange', localFilter.value)
234293
}
294+
295+
const clearFilters = () => {
296+
searchQuery.value = ''
297+
selectedSort.value = ''
298+
emit('search', '')
299+
emit('sort', '')
300+
}
235301
</script>

resources/js/layouts/AdminLayout.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
:class="[sidebarOpen ? 'translate-x-0' : '-translate-x-full']"
1111
class="fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-gray-800 shadow-lg transform transition-transform lg:translate-x-0 lg:relative lg:flex lg:flex-col"
1212
>
13-
<div class="flex items-center justify-between h-16 px-6 bg-sky-500 flex-shrink-0">
13+
<div class="flex items-center justify-between h-16 px-6 bg-[#F02C56] flex-shrink-0">
1414
<h1 class="text-xl font-bold text-white">Loops Admin</h1>
1515
<button
1616
@click="sidebarOpen = false"
@@ -90,7 +90,9 @@
9090
</button>
9191

9292
<div class="flex items-center space-x-4">
93-
<span class="text-sm text-gray-500 dark:text-gray-400">Hello Admin!</span>
93+
<span class="text-sm text-gray-500 dark:text-gray-400"
94+
>Hello {{ authStore.getUser.username }}!</span
95+
>
9496
</div>
9597
</div>
9698
</header>
@@ -108,6 +110,7 @@
108110
import { ref, onMounted, inject } from 'vue'
109111
import { useRoute } from 'vue-router'
110112
import { storeToRefs } from 'pinia'
113+
import { useAuthStore } from '@/stores/auth'
111114
import {
112115
ChartBarSquareIcon,
113116
Cog6ToothIcon,
@@ -128,6 +131,8 @@ import { useAdminStore } from '~/stores/admin'
128131
const route = useRoute()
129132
const sidebarOpen = ref(false)
130133
134+
const authStore = useAuthStore()
135+
131136
const navigation = [
132137
{ name: 'Dashboard', href: '/admin/dashboard', icon: ChartBarSquareIcon },
133138
{ name: 'Comments', href: '/admin/comments', icon: ChatBubbleOvalLeftIcon },

0 commit comments

Comments
 (0)