kopia lustrzana https://github.com/pixelfed/pixelfed
				
				
				
			Improve admin dashboard by moving expensive stats to its page and loading stats and recent data async on the dashboard home page
							rodzic
							
								
									b4bc9fe31c
								
							
						
					
					
						commit
						9d52b9c2d6
					
				|  | @ -6,6 +6,7 @@ use App\{ | |||
| 	AccountInterstitial, | ||||
| 	Contact, | ||||
| 	Hashtag, | ||||
| 	Instance, | ||||
| 	Newsroom, | ||||
| 	OauthClient, | ||||
| 	Profile, | ||||
|  | @ -31,6 +32,7 @@ use App\Http\Controllers\Admin\{ | |||
| }; | ||||
| use Illuminate\Validation\Rule; | ||||
| use App\Services\AdminStatsService; | ||||
| use App\Services\AccountService; | ||||
| use App\Services\StatusService; | ||||
| use App\Services\StoryService; | ||||
| use App\Models\CustomEmoji; | ||||
|  | @ -54,9 +56,71 @@ class AdminController extends Controller | |||
| 	} | ||||
| 
 | ||||
| 	public function home() | ||||
| 	{ | ||||
| 		return view('admin.home'); | ||||
| 	} | ||||
| 
 | ||||
| 	public function stats() | ||||
| 	{ | ||||
| 		$data = AdminStatsService::get(); | ||||
| 		return view('admin.home', compact('data')); | ||||
| 		return view('admin.stats', compact('data')); | ||||
| 	} | ||||
| 
 | ||||
| 	public function getStats() | ||||
| 	{ | ||||
| 		return AdminStatsService::summary(); | ||||
| 	} | ||||
| 
 | ||||
| 	public function getAccounts() | ||||
| 	{ | ||||
| 		$users = User::orderByDesc('id')->cursorPaginate(10); | ||||
| 
 | ||||
| 		$res = [ | ||||
| 			"next_page_url" => $users->nextPageUrl(), | ||||
| 			"data" => $users->map(function($user) { | ||||
| 				$account = AccountService::get($user->profile_id, true); | ||||
| 				if(!$account) { | ||||
| 					return [ | ||||
| 						"id" => $user->profile_id, | ||||
| 						"username" => $user->username, | ||||
| 						"status" => "deleted", | ||||
| 						"avatar" => "/storage/avatars/default.jpg", | ||||
| 						"created_at" => $user->created_at | ||||
| 					]; | ||||
| 				} | ||||
| 				$account['user_id'] = $user->id; | ||||
| 				return $account; | ||||
| 			}) | ||||
| 			->filter(function($user) { | ||||
| 				return $user; | ||||
| 			}) | ||||
| 		]; | ||||
| 		return $res; | ||||
| 	} | ||||
| 
 | ||||
| 	public function getPosts() | ||||
| 	{ | ||||
| 		$posts = DB::table('statuses') | ||||
| 			->orderByDesc('id') | ||||
| 			->cursorPaginate(10); | ||||
| 
 | ||||
| 		$res = [ | ||||
| 			"next_page_url" => $posts->nextPageUrl(), | ||||
| 			"data" => $posts->map(function($post) { | ||||
| 				$status = StatusService::get($post->id, false); | ||||
| 				if(!$status) { | ||||
| 					return ["id" => $post->id, "created_at" => $post->created_at]; | ||||
| 				} | ||||
| 				return $status; | ||||
| 			}) | ||||
| 		]; | ||||
| 
 | ||||
| 		return $res; | ||||
| 	} | ||||
| 
 | ||||
| 	public function getInstances() | ||||
| 	{ | ||||
| 		return Instance::orderByDesc('id')->cursorPaginate(10); | ||||
| 	} | ||||
| 
 | ||||
| 	public function statuses(Request $request) | ||||
|  |  | |||
|  | @ -32,6 +32,14 @@ class AdminStatsService | |||
| 		); | ||||
| 	} | ||||
| 
 | ||||
| 	public static function summary() | ||||
| 	{ | ||||
| 		return array_merge( | ||||
| 			self::recentData(), | ||||
| 			self::additionalDataSummary(), | ||||
| 		); | ||||
| 	} | ||||
| 
 | ||||
| 	public static function storage() | ||||
|     { | ||||
|         return Cache::remember('admin:dashboard:storage:stats', 120000, function() { | ||||
|  | @ -102,6 +110,19 @@ class AdminStatsService | |||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	protected static function additionalDataSummary() | ||||
| 	{ | ||||
| 		$ttl = now()->addHours(24); | ||||
| 		return Cache::remember('admin:dashboard:home:data:v0:24hr', $ttl, function() { | ||||
| 			return [ | ||||
| 				'statuses' => PrettyNumber::convert(Status::count()), | ||||
| 				'profiles' => PrettyNumber::convert(Profile::count()), | ||||
| 				'users' => PrettyNumber::convert(User::count()), | ||||
| 				'instances' => PrettyNumber::convert(Instance::count()), | ||||
| 			]; | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	protected static function postsGraph() | ||||
| 	{ | ||||
| 		$ttl = now()->addHours(12); | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| @section('section') | ||||
| </div> | ||||
| <div class="header bg-primary pb-6 mt-n4"> | ||||
| <div class="header bg-primary pb-2 mt-n4"> | ||||
| 	<div class="container-fluid"> | ||||
| 		<div class="header-body"> | ||||
| 			<div class="row align-items-center py-4"> | ||||
|  | @ -10,14 +10,14 @@ | |||
| 					<p class="display-1 text-white d-inline-block mb-0">Dashboard</p> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="row"> | ||||
| 			<div v-if="loaded.stats" class="row"> | ||||
| 				<div class="col-xl-3 col-md-6"> | ||||
| 					<div class="card card-stats"> | ||||
| 						<div class="card-body"> | ||||
| 							<div class="row"> | ||||
| 								<div class="col"> | ||||
| 									<h5 class="card-title text-uppercase text-muted mb-0">Total posts</h5> | ||||
| 									<span class="h2 font-weight-bold mb-0">{{$data['statuses']}}</span> | ||||
| 									<span class="h2 font-weight-bold mb-0" v-text="stats.statuses"></span> | ||||
| 								</div> | ||||
| 								<div class="col-auto"> | ||||
| 									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | ||||
|  | @ -25,10 +25,6 @@ | |||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 							<p class="mt-3 mb-0 text-sm"> | ||||
| 								<span class="text-success mr-2"><i class="fa fa-arrow-up"></i> {{$data['statuses_monthly']}}</span> | ||||
| 								<span class="text-nowrap">in last 30 days</span> | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | @ -38,7 +34,7 @@ | |||
| 							<div class="row"> | ||||
| 								<div class="col"> | ||||
| 									<h5 class="card-title text-uppercase text-muted mb-0">Total users</h5> | ||||
| 									<span class="h2 font-weight-bold mb-0">{{$data['users']}}</span> | ||||
| 									<span class="h2 font-weight-bold mb-0" v-text="stats.users"></span> | ||||
| 								</div> | ||||
| 								<div class="col-auto"> | ||||
| 									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | ||||
|  | @ -46,10 +42,6 @@ | |||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 							<p class="mt-3 mb-0 text-sm"> | ||||
| 								<span class="text-success mr-2"><i class="fa fa-arrow-up"></i> {{$data['users_monthly']}}</span> | ||||
| 								<span class="text-nowrap">in last 30 days</span> | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | @ -59,7 +51,7 @@ | |||
| 							<div class="row"> | ||||
| 								<div class="col"> | ||||
| 									<h5 class="card-title text-uppercase text-muted mb-0">Reports</h5> | ||||
| 									<span class="h2 font-weight-bold mb-0">{{$data['reports']}}</span> | ||||
| 									<span class="h2 font-weight-bold mb-0" v-text="stats.reports"></span> | ||||
| 								</div> | ||||
| 								<div class="col-auto"> | ||||
| 									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | ||||
|  | @ -67,10 +59,6 @@ | |||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 							<p class="mt-3 mb-0 text-sm"> | ||||
| 								<span class="text-success mr-2"><i class="fa fa-arrow-up"></i> {{$data['reports_monthly']}}</span> | ||||
| 								<span class="text-nowrap">in last 30 days</span> | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | @ -80,7 +68,7 @@ | |||
| 							<div class="row"> | ||||
| 								<div class="col"> | ||||
| 									<h5 class="card-title text-uppercase text-muted mb-0">Messages</h5> | ||||
| 									<span class="h2 font-weight-bold mb-0">{{$data['contact']}}</span> | ||||
| 									<span class="h2 font-weight-bold mb-0" v-text="stats.contact"></span> | ||||
| 								</div> | ||||
| 								<div class="col-auto"> | ||||
| 									<div class="icon icon-shape bg-gradient-info text-white rounded-circle shadow"> | ||||
|  | @ -88,10 +76,6 @@ | |||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 							<p class="mt-3 mb-0 text-sm"> | ||||
| 								<span class="text-success mr-2"><i class="fa fa-arrow-up"></i> {{$data['contact_monthly']}}</span> | ||||
| 								<span class="text-nowrap">in last 30 days</span> | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | @ -102,64 +86,126 @@ | |||
| <div class="container-fluid mt-4"> | ||||
| 
 | ||||
| 	<div class="row"> | ||||
| 		<div class="col-md-8"> | ||||
| 		<div class="col-md-4"> | ||||
| 			<div class="card bg-default"> | ||||
| 				<div class="card-header bg-transparent"> | ||||
| 					<div class="row align-items-center"> | ||||
| 						<div class="col"> | ||||
| 							<h6 class="text-light text-uppercase ls-1 mb-1">Overview</h6> | ||||
| 							<h5 class="h3 text-white mb-0">Daily Posts</h5> | ||||
| 							<h6 class="text-light text-uppercase ls-1 mb-1">New</h6> | ||||
| 							<h5 class="h3 text-white mb-0">Accounts</h5> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div v-if="!loaded.accounts" class="card-body text-center"> | ||||
|                     <b-spinner class="mb-4"></b-spinner> | ||||
|                 </div> | ||||
|                 <div v-else class="list-group list-group-scroll"> | ||||
|                     <a | ||||
|                         v-for="(item, index) in accounts" | ||||
|                         class="list-group-item" | ||||
|                         :href="`/i/admin/users/show/${item.user_id}`"> | ||||
| 
 | ||||
|                         <div class="d-flex align-items-center mr-1"> | ||||
|                             <img :src="item.avatar" class="avatar" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';"/> | ||||
|                             <div v-if="item.status && item.status == 'deleted'"> | ||||
|                                 <span v-text="item.username" class="font-weight-bold text-danger">Loading...</span> | ||||
|                                 <span class="ml-2 badge badge-danger">Deleted</span> | ||||
|                             </div> | ||||
|                             <div v-else> | ||||
|                                 <div v-text="item.username" class="font-weight-bold">Loading...</div> | ||||
|                                 <div v-if="item.note_text" v-text="renderNote(item.note_text)" class="note">Loading...</div> | ||||
|                             </div> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div> | ||||
|                             <div class="d-flex" style="font-size: 13px;"> | ||||
|                                 <div v-text="timeAgo(item.created_at)" class="small text-light"></div> | ||||
|                             </div> | ||||
|                         </div> | ||||
| 						<div class="col"> | ||||
| 							<ul class="nav nav-pills justify-content-end"> | ||||
| 								<li class="nav-item mr-2 mr-md-0 posts-this-week" data-toggle="chart" data-target="#c1-dark" data-update='{"data":{"datasets":[{"data":{{$data['posts_this_week']}}}]}}'> | ||||
| 									<a href="#" class="nav-link py-2 px-3 active" data-toggle="tab"> | ||||
| 										<span class="d-none d-md-block">This Week</span> | ||||
| 										<span class="d-md-none">W</span> | ||||
|                     </a> | ||||
| 								</li> | ||||
| 								<li class="nav-item" data-toggle="chart" data-target="#c1-dark" data-update='{"data":{"datasets":[{"data":{{$data['posts_last_week']}}}]}}'> | ||||
| 									<a href="#" class="nav-link py-2 px-3" data-toggle="tab"> | ||||
| 										<span class="d-none d-md-block">Last Week</span> | ||||
| 										<span class="d-md-none">W</span> | ||||
| 									</a> | ||||
| 								</li> | ||||
| 							</ul> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div class="card-body"> | ||||
| 					<!-- Chart --> | ||||
| 					<div class="chart"> | ||||
| 						<!-- Chart wrapper --> | ||||
| 						<canvas id="c1-dark" class="chart-canvas"></canvas> | ||||
| 					</div> | ||||
| 
 | ||||
|                     <a v-if="pagination.accounts" class="list-group-item font-weight-bold justify-content-center" href="#" @click.prevent="loadMoreAccounts()">Load more</a> | ||||
|                 </div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
|         <div class="col-md-4"> | ||||
| 			<div class="card shadow-none border mb-2" style="min-height:125px"> | ||||
| 				<div class="card-body"> | ||||
| 					<p class="small text-uppercase font-weight-bold text-muted">Failed Jobs (24h)</p> | ||||
| 					<p class="h2 mb-0">{{$data['failedjobs']}}</p> | ||||
|             <div class="card bg-default"> | ||||
|                 <div class="card-header bg-transparent"> | ||||
|                     <div class="row align-items-center"> | ||||
|                         <div class="col"> | ||||
|                             <h6 class="text-light text-uppercase ls-1 mb-1">New</h6> | ||||
|                             <h5 class="h3 text-white mb-0">Posts</h5> | ||||
|                         </div> | ||||
|                     </div> | ||||
| 			<div class="card shadow-none border mb-2" style="min-height:125px"> | ||||
| 				<div class="card-body"> | ||||
| 					<p class="small text-uppercase font-weight-bold text-muted">Remote Instances</p> | ||||
| 					<p class="h2 mb-0">{{$data['instances']}}</p> | ||||
|                 </div> | ||||
|                 <div v-if="!loaded.posts" class="card-body text-center"> | ||||
|                     <b-spinner class="mb-4"></b-spinner> | ||||
|                 </div> | ||||
|                 <div v-else class="list-group list-group-scroll"> | ||||
|                     <a | ||||
|                         v-for="(item, index) in posts" | ||||
|                         class="list-group-item" | ||||
|                         :href="`/i/web/post/${item.id}`"> | ||||
| 
 | ||||
|                         <div v-if="item.account" class="d-flex align-items-center mr-1"> | ||||
|                             <img :src="item.account.avatar" class="avatar" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';"/> | ||||
|                             <div> | ||||
|                                 <div v-text="item.account.acct" class="font-weight-bold">Loading...</div> | ||||
|                                 <div v-if="item.content" v-text="renderNote(item.content_text)" class="note">Loading...</div> | ||||
|                                 <div v-else class="badge badge-primary" v-text="item.pf_type" style="font-size:9px"></div> | ||||
|                             </div> | ||||
|                         </div> | ||||
| 			<div class="card shadow-none border mb-2" style="min-height:125px"> | ||||
| 				<div class="card-body"> | ||||
| 					<p class="small text-uppercase font-weight-bold text-muted">Photos Uploaded</p> | ||||
| 					<p class="h2 mb-0">{{$data['media']}}</p> | ||||
|                         <div v-else> | ||||
|                             <div class="text-muted font-weight-bold">Deleted or unavailable post</div> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div> | ||||
|                             <div class="d-flex" style="font-size: 13px;"> | ||||
|                                 <div v-text="timeAgo(item.created_at)" class="small text-light"></div> | ||||
|                             </div> | ||||
|                         </div> | ||||
| 			<div class="card shadow-none border" style="min-height:125px"> | ||||
| 				<div class="card-body"> | ||||
| 					<p class="small text-uppercase font-weight-bold text-muted">Storage Used</p> | ||||
| 					<p class="human-size mb-0" data-bytes="{{$data['storage']}}">{{$data['storage']}} bytes</p> | ||||
|                     </a> | ||||
| 
 | ||||
|                     <a v-if="pagination.posts" class="list-group-item font-weight-bold justify-content-center" href="#" @click.prevent="loadMorePosts()">Load more</a> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
| 
 | ||||
| 		<div class="col-md-4"> | ||||
|             <div class="card bg-default"> | ||||
|                 <div class="card-header bg-transparent"> | ||||
|                     <div class="row align-items-center"> | ||||
|                         <div class="col"> | ||||
|                             <h6 class="text-light text-uppercase ls-1 mb-1">New</h6> | ||||
|                             <h5 class="h3 text-white mb-0">Instances</h5> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div v-if="!loaded.instances" class="card-body text-center"> | ||||
|                     <b-spinner class="mb-4"></b-spinner> | ||||
|                 </div> | ||||
|                 <div v-else class="list-group list-group-scroll"> | ||||
|                     <a | ||||
|                         v-for="(item, index) in instances" | ||||
|                         class="list-group-item" | ||||
|                         :href="`/i/admin/instances/show/${item.id}`"> | ||||
| 
 | ||||
|                         <div v-text="item.domain" class="font-weight-bold">Loading...</div> | ||||
| 
 | ||||
|                         <div> | ||||
|                             <div class="d-flex" style="font-size: 13px;"> | ||||
|                                 <div v-if="item.software" class="badge badge-secondary mr-2" v-text="item.software"></div> | ||||
|                                 <div v-if="item.user_count" class="badge badge-primary mr-2"> | ||||
|                                     <span class="mr-1"><i class="far fa-user"></i></span> | ||||
|                                     <span v-text="item.user_count"></span> | ||||
|                                 </div> | ||||
|                                 <div v-text="timeAgo(item.created_at)" class="small text-light"></div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </a> | ||||
| 
 | ||||
|                     <a v-if="pagination.instances" class="list-group-item font-weight-bold justify-content-center" href="#" @click.prevent="loadMoreInstances()">Load more</a> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | @ -168,13 +214,152 @@ | |||
| 
 | ||||
| @push('scripts') | ||||
| <script type="text/javascript"> | ||||
| 	$(document).ready(function() { | ||||
| 		$('.human-size').each(function(d,a) { | ||||
| 			let el = $(a); | ||||
| 			let size = el.data('bytes'); | ||||
| 			el.addClass('h2'); | ||||
| 			el.text(filesize(size, {round: 0})); | ||||
| 		}); | ||||
|     let app = new Vue({ | ||||
|         el: '#panel', | ||||
| 
 | ||||
|         data: { | ||||
|             stats: { | ||||
|                 "contact": 0, | ||||
|                 "contact_monthly": 0, | ||||
|                 "reports": 0, | ||||
|                 "reports_monthly": 0, | ||||
|                 "failedjobs": 0, | ||||
|                 "statuses": 0, | ||||
|                 "statuses_monthly": 0, | ||||
|                 "profiles": 0, | ||||
|                 "users": 0, | ||||
|                 "users_monthly": 0, | ||||
|                 "instances": 0, | ||||
|                 "media": 0, | ||||
|                 "storage": 0, | ||||
|                 "posts_this_week": [], | ||||
|                 "posts_last_week": [] | ||||
|             }, | ||||
|             loaded: { | ||||
|                 stats: false, | ||||
|                 accounts: false, | ||||
|                 posts: false, | ||||
|                 instances: false | ||||
|             }, | ||||
|             pagination: { | ||||
|                 accounts: false, | ||||
|                 posts: false, | ||||
|                 instances: false | ||||
|             }, | ||||
|             accounts: [], | ||||
|             posts: [], | ||||
|             instances: [] | ||||
|         }, | ||||
| 
 | ||||
|         mounted() { | ||||
|             this.fetchStats(); | ||||
|         }, | ||||
| 
 | ||||
|         methods: { | ||||
|             fetchStats() { | ||||
|                 axios.get('/i/admin/api/stats') | ||||
|                 .then(res => { | ||||
|                     this.stats = res.data; | ||||
|                     this.loaded.stats = true; | ||||
|                     this.fetchAccounts(); | ||||
|                 }) | ||||
|             }, | ||||
| 
 | ||||
|             fetchAccounts() { | ||||
|                 axios.get('/i/admin/api/accounts') | ||||
|                 .then(res => { | ||||
|                     this.accounts = res.data.data; | ||||
|                     this.loaded.accounts = true; | ||||
|                     this.pagination.accounts = res.data.next_page_url; | ||||
| 
 | ||||
|                     this.fetchPosts(); | ||||
|                 }) | ||||
|             }, | ||||
| 
 | ||||
|             loadMoreAccounts() { | ||||
|                 axios.get(this.pagination.accounts) | ||||
|                 .then(res => { | ||||
|                     this.accounts.push(...res.data.data); | ||||
|                     this.pagination.accounts = res.data.next_page_url; | ||||
|                 }) | ||||
|             }, | ||||
| 
 | ||||
|             fetchPosts() { | ||||
|                 axios.get('/i/admin/api/posts') | ||||
|                 .then(res => { | ||||
|                     this.posts = res.data.data; | ||||
|                     this.loaded.posts = true; | ||||
|                     this.pagination.posts = res.data.next_page_url; | ||||
| 
 | ||||
|                     this.fetchInstances(); | ||||
|                 }) | ||||
|             }, | ||||
| 
 | ||||
|             loadMorePosts() { | ||||
|                 axios.get(this.pagination.posts) | ||||
|                 .then(res => { | ||||
|                     res.data.data.map(a => console.log(a.id)); | ||||
|                     this.posts.push(...res.data.data); | ||||
|                     this.pagination.posts = res.data.next_page_url; | ||||
|                 }) | ||||
|             }, | ||||
| 
 | ||||
|             fetchInstances() { | ||||
|                 axios.get('/i/admin/api/instances') | ||||
|                 .then(res => { | ||||
|                     this.instances = res.data.data; | ||||
|                     this.loaded.instances = true; | ||||
|                     this.pagination.instances = res.data.next_page_url; | ||||
|                 }) | ||||
|             }, | ||||
| 
 | ||||
|             loadMoreInstances() { | ||||
|                 axios.get(this.pagination.instances) | ||||
|                 .then(res => { | ||||
|                     this.instances.push(...res.data.data); | ||||
|                     this.pagination.instances = res.data.next_page_url; | ||||
|                 }) | ||||
|             }, | ||||
| 
 | ||||
|             timeAgo(ts) { | ||||
|                 return App.util.format.timeAgo(ts); | ||||
|             }, | ||||
| 
 | ||||
|             renderNote(val) { | ||||
|                 if(val.length > 60) { | ||||
|                     return val.slice(0, 60) + ' ...'; | ||||
|                 } | ||||
|                 return val; | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
| @endpush | ||||
| 
 | ||||
| @push('styles') | ||||
| <style type="text/css"> | ||||
|     .list-group-scroll { | ||||
|         max-height: 300px; | ||||
|         overflow-y: auto; | ||||
|     } | ||||
| 
 | ||||
|     .list-group-scroll .list-group-item { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
|     } | ||||
| 
 | ||||
|     .list-group-scroll .avatar { | ||||
|         width: 30px; | ||||
|         height: 30px; | ||||
|         border-radius: 30px; | ||||
|         margin-right: 1rem; | ||||
|     } | ||||
| 
 | ||||
|     .list-group-scroll .note { | ||||
|         color: #bbb;
 | ||||
|         font-size: 10px; | ||||
|         line-height: 12px; | ||||
|     } | ||||
| </style> | ||||
| @endpush | ||||
|  |  | |||
|  | @ -132,6 +132,13 @@ | |||
| 						</a> | ||||
| 					</li> | ||||
| 
 | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link {{request()->is('*stats')?'active':''}}" href="/i/admin/stats"> | ||||
|                             <i class="ni ni-bold-right text-primary"></i> | ||||
|                             <span class="nav-link-text">Stats</span> | ||||
|                         </a> | ||||
|                     </li> | ||||
| 
 | ||||
| 					<li class="nav-item"> | ||||
| 						<a class="nav-link {{request()->is('*settings/system')?'active':''}}" href="/i/admin/settings/system"> | ||||
| 							<i class="ni ni-bold-right text-primary"></i> | ||||
|  |  | |||
|  | @ -0,0 +1,180 @@ | |||
| @extends('admin.partial.template-full') | ||||
| 
 | ||||
| @section('section') | ||||
| </div> | ||||
| <div class="header bg-primary pb-6 mt-n4"> | ||||
|     <div class="container-fluid"> | ||||
|         <div class="header-body"> | ||||
|             <div class="row align-items-center py-4"> | ||||
|                 <div class="col-lg-6 col-7"> | ||||
|                     <p class="display-1 text-white d-inline-block mb-0">Stats</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="row"> | ||||
|                 <div class="col-xl-3 col-md-6"> | ||||
|                     <div class="card card-stats"> | ||||
|                         <div class="card-body"> | ||||
|                             <div class="row"> | ||||
|                                 <div class="col"> | ||||
|                                     <h5 class="card-title text-uppercase text-muted mb-0">Total posts</h5> | ||||
|                                     <span class="h2 font-weight-bold mb-0">{{$data['statuses']}}</span> | ||||
|                                 </div> | ||||
|                                 <div class="col-auto"> | ||||
|                                     <div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | ||||
|                                         <i class="ni ni-image"></i> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                             <p class="mt-3 mb-0 text-sm"> | ||||
|                                 <span class="text-success mr-2"><i class="fa fa-arrow-up"></i> {{$data['statuses_monthly']}}</span> | ||||
|                                 <span class="text-nowrap">in last 30 days</span> | ||||
|                             </p> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-xl-3 col-md-6"> | ||||
|                     <div class="card card-stats"> | ||||
|                         <div class="card-body"> | ||||
|                             <div class="row"> | ||||
|                                 <div class="col"> | ||||
|                                     <h5 class="card-title text-uppercase text-muted mb-0">Total users</h5> | ||||
|                                     <span class="h2 font-weight-bold mb-0">{{$data['users']}}</span> | ||||
|                                 </div> | ||||
|                                 <div class="col-auto"> | ||||
|                                     <div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | ||||
|                                         <i class="ni ni-circle-08"></i> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                             <p class="mt-3 mb-0 text-sm"> | ||||
|                                 <span class="text-success mr-2"><i class="fa fa-arrow-up"></i> {{$data['users_monthly']}}</span> | ||||
|                                 <span class="text-nowrap">in last 30 days</span> | ||||
|                             </p> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-xl-3 col-md-6"> | ||||
|                     <div class="card card-stats"> | ||||
|                         <div class="card-body"> | ||||
|                             <div class="row"> | ||||
|                                 <div class="col"> | ||||
|                                     <h5 class="card-title text-uppercase text-muted mb-0">Reports</h5> | ||||
|                                     <span class="h2 font-weight-bold mb-0">{{$data['reports']}}</span> | ||||
|                                 </div> | ||||
|                                 <div class="col-auto"> | ||||
|                                     <div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | ||||
|                                         <i class="ni ni-bell-55"></i> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                             <p class="mt-3 mb-0 text-sm"> | ||||
|                                 <span class="text-success mr-2"><i class="fa fa-arrow-up"></i> {{$data['reports_monthly']}}</span> | ||||
|                                 <span class="text-nowrap">in last 30 days</span> | ||||
|                             </p> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="col-xl-3 col-md-6"> | ||||
|                     <div class="card card-stats"> | ||||
|                         <div class="card-body"> | ||||
|                             <div class="row"> | ||||
|                                 <div class="col"> | ||||
|                                     <h5 class="card-title text-uppercase text-muted mb-0">Messages</h5> | ||||
|                                     <span class="h2 font-weight-bold mb-0">{{$data['contact']}}</span> | ||||
|                                 </div> | ||||
|                                 <div class="col-auto"> | ||||
|                                     <div class="icon icon-shape bg-gradient-info text-white rounded-circle shadow"> | ||||
|                                         <i class="ni ni-chat-round"></i> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                             <p class="mt-3 mb-0 text-sm"> | ||||
|                                 <span class="text-success mr-2"><i class="fa fa-arrow-up"></i> {{$data['contact_monthly']}}</span> | ||||
|                                 <span class="text-nowrap">in last 30 days</span> | ||||
|                             </p> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| <div class="container-fluid mt-4"> | ||||
| 
 | ||||
|     <div class="row"> | ||||
|         <div class="col-md-8"> | ||||
|             <div class="card bg-default"> | ||||
|                 <div class="card-header bg-transparent"> | ||||
|                     <div class="row align-items-center"> | ||||
|                         <div class="col"> | ||||
|                             <h6 class="text-light text-uppercase ls-1 mb-1">Overview</h6> | ||||
|                             <h5 class="h3 text-white mb-0">Daily Posts</h5> | ||||
|                         </div> | ||||
|                         <div class="col"> | ||||
|                             <ul class="nav nav-pills justify-content-end"> | ||||
|                                 <li class="nav-item mr-2 mr-md-0 posts-this-week" data-toggle="chart" data-target="#c1-dark" data-update='{"data":{"datasets":[{"data":{{$data['posts_this_week']}}}]}}'> | ||||
|                                     <a href="#" class="nav-link py-2 px-3 active" data-toggle="tab"> | ||||
|                                         <span class="d-none d-md-block">This Week</span> | ||||
|                                         <span class="d-md-none">W</span> | ||||
|                                     </a> | ||||
|                                 </li> | ||||
|                                 <li class="nav-item" data-toggle="chart" data-target="#c1-dark" data-update='{"data":{"datasets":[{"data":{{$data['posts_last_week']}}}]}}'> | ||||
|                                     <a href="#" class="nav-link py-2 px-3" data-toggle="tab"> | ||||
|                                         <span class="d-none d-md-block">Last Week</span> | ||||
|                                         <span class="d-md-none">W</span> | ||||
|                                     </a> | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="card-body"> | ||||
|                     <!-- Chart --> | ||||
|                     <div class="chart"> | ||||
|                         <!-- Chart wrapper --> | ||||
|                         <canvas id="c1-dark" class="chart-canvas"></canvas> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-md-4"> | ||||
|             <div class="card shadow-none border mb-2" style="min-height:125px"> | ||||
|                 <div class="card-body"> | ||||
|                     <p class="small text-uppercase font-weight-bold text-muted">Failed Jobs (24h)</p> | ||||
|                     <p class="h2 mb-0">{{$data['failedjobs']}}</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="card shadow-none border mb-2" style="min-height:125px"> | ||||
|                 <div class="card-body"> | ||||
|                     <p class="small text-uppercase font-weight-bold text-muted">Remote Instances</p> | ||||
|                     <p class="h2 mb-0">{{$data['instances']}}</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="card shadow-none border mb-2" style="min-height:125px"> | ||||
|                 <div class="card-body"> | ||||
|                     <p class="small text-uppercase font-weight-bold text-muted">Photos Uploaded</p> | ||||
|                     <p class="h2 mb-0">{{$data['media']}}</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="card shadow-none border" style="min-height:125px"> | ||||
|                 <div class="card-body"> | ||||
|                     <p class="small text-uppercase font-weight-bold text-muted">Storage Used</p> | ||||
|                     <p class="human-size mb-0" data-bytes="{{$data['storage']}}">{{$data['storage']}} bytes</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| @endsection | ||||
| 
 | ||||
| @push('scripts') | ||||
| <script type="text/javascript"> | ||||
|     $(document).ready(function() { | ||||
|         $('.human-size').each(function(d,a) { | ||||
|             let el = $(a); | ||||
|             let size = el.data('bytes'); | ||||
|             el.addClass('h2'); | ||||
|             el.text(filesize(size, {round: 0})); | ||||
|         }); | ||||
|     }); | ||||
| </script> | ||||
| @endpush | ||||
|  | @ -4,6 +4,7 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio | |||
| 	Route::redirect('/', '/dashboard'); | ||||
| 	Route::redirect('timeline', config('app.url').'/timeline'); | ||||
| 	Route::get('dashboard', 'AdminController@home')->name('admin.home'); | ||||
| 	Route::get('stats', 'AdminController@stats')->name('admin.stats'); | ||||
| 	Route::get('reports', 'AdminController@reports')->name('admin.reports'); | ||||
| 	Route::get('reports/show/{id}', 'AdminController@showReport'); | ||||
| 	Route::post('reports/show/{id}', 'AdminController@updateReport'); | ||||
|  | @ -90,6 +91,13 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio | |||
| 	Route::post('custom-emoji/new', 'AdminController@customEmojiStore'); | ||||
| 	Route::post('custom-emoji/delete/{id}', 'AdminController@customEmojiDelete'); | ||||
| 	Route::get('custom-emoji/duplicates/{id}', 'AdminController@customEmojiShowDuplicates'); | ||||
| 
 | ||||
| 	Route::prefix('api')->group(function() { | ||||
| 		Route::get('stats', 'AdminController@getStats'); | ||||
| 		Route::get('accounts', 'AdminController@getAccounts'); | ||||
| 		Route::get('posts', 'AdminController@getPosts'); | ||||
| 		Route::get('instances', 'AdminController@getInstances'); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () { | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Daniel Supernault
						Daniel Supernault