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, | 	AccountInterstitial, | ||||||
| 	Contact, | 	Contact, | ||||||
| 	Hashtag, | 	Hashtag, | ||||||
|  | 	Instance, | ||||||
| 	Newsroom, | 	Newsroom, | ||||||
| 	OauthClient, | 	OauthClient, | ||||||
| 	Profile, | 	Profile, | ||||||
|  | @ -31,6 +32,7 @@ use App\Http\Controllers\Admin\{ | ||||||
| }; | }; | ||||||
| use Illuminate\Validation\Rule; | use Illuminate\Validation\Rule; | ||||||
| use App\Services\AdminStatsService; | use App\Services\AdminStatsService; | ||||||
|  | use App\Services\AccountService; | ||||||
| use App\Services\StatusService; | use App\Services\StatusService; | ||||||
| use App\Services\StoryService; | use App\Services\StoryService; | ||||||
| use App\Models\CustomEmoji; | use App\Models\CustomEmoji; | ||||||
|  | @ -54,9 +56,71 @@ class AdminController extends Controller | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public function home() | 	public function home() | ||||||
|  | 	{ | ||||||
|  | 		return view('admin.home'); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public function stats() | ||||||
| 	{ | 	{ | ||||||
| 		$data = AdminStatsService::get(); | 		$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) | 	public function statuses(Request $request) | ||||||
|  |  | ||||||
|  | @ -26,10 +26,18 @@ class AdminStatsService | ||||||
| 	public static function get() | 	public static function get() | ||||||
| 	{ | 	{ | ||||||
| 		return array_merge( | 		return array_merge( | ||||||
| 				self::recentData(), | 			self::recentData(), | ||||||
| 				self::additionalData(), | 			self::additionalData(), | ||||||
| 				self::postsGraph() | 			self::postsGraph() | ||||||
| 			); | 		); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public static function summary() | ||||||
|  | 	{ | ||||||
|  | 		return array_merge( | ||||||
|  | 			self::recentData(), | ||||||
|  | 			self::additionalDataSummary(), | ||||||
|  | 		); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public static function storage() | 	public static function storage() | ||||||
|  | @ -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() | 	protected static function postsGraph() | ||||||
| 	{ | 	{ | ||||||
| 		$ttl = now()->addHours(12); | 		$ttl = now()->addHours(12); | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| @section('section') | @section('section') | ||||||
| </div> | </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="container-fluid"> | ||||||
| 		<div class="header-body"> | 		<div class="header-body"> | ||||||
| 			<div class="row align-items-center py-4"> | 			<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> | 					<p class="display-1 text-white d-inline-block mb-0">Dashboard</p> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="row"> | 			<div v-if="loaded.stats" class="row"> | ||||||
| 				<div class="col-xl-3 col-md-6"> | 				<div class="col-xl-3 col-md-6"> | ||||||
| 					<div class="card card-stats"> | 					<div class="card card-stats"> | ||||||
| 						<div class="card-body"> | 						<div class="card-body"> | ||||||
| 							<div class="row"> | 							<div class="row"> | ||||||
| 								<div class="col"> | 								<div class="col"> | ||||||
| 									<h5 class="card-title text-uppercase text-muted mb-0">Total posts</h5> | 									<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> | ||||||
| 								<div class="col-auto"> | 								<div class="col-auto"> | ||||||
| 									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | 									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | ||||||
|  | @ -25,10 +25,6 @@ | ||||||
| 									</div> | 									</div> | ||||||
| 								</div> | 								</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> | ||||||
| 				</div> | 				</div> | ||||||
|  | @ -38,7 +34,7 @@ | ||||||
| 							<div class="row"> | 							<div class="row"> | ||||||
| 								<div class="col"> | 								<div class="col"> | ||||||
| 									<h5 class="card-title text-uppercase text-muted mb-0">Total users</h5> | 									<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> | ||||||
| 								<div class="col-auto"> | 								<div class="col-auto"> | ||||||
| 									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | 									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | ||||||
|  | @ -46,10 +42,6 @@ | ||||||
| 									</div> | 									</div> | ||||||
| 								</div> | 								</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> | ||||||
| 				</div> | 				</div> | ||||||
|  | @ -59,7 +51,7 @@ | ||||||
| 							<div class="row"> | 							<div class="row"> | ||||||
| 								<div class="col"> | 								<div class="col"> | ||||||
| 									<h5 class="card-title text-uppercase text-muted mb-0">Reports</h5> | 									<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> | ||||||
| 								<div class="col-auto"> | 								<div class="col-auto"> | ||||||
| 									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | 									<div class="icon icon-shape bg-gradient-primary text-white rounded-circle shadow"> | ||||||
|  | @ -67,10 +59,6 @@ | ||||||
| 									</div> | 									</div> | ||||||
| 								</div> | 								</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> | ||||||
| 				</div> | 				</div> | ||||||
|  | @ -80,7 +68,7 @@ | ||||||
| 							<div class="row"> | 							<div class="row"> | ||||||
| 								<div class="col"> | 								<div class="col"> | ||||||
| 									<h5 class="card-title text-uppercase text-muted mb-0">Messages</h5> | 									<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> | ||||||
| 								<div class="col-auto"> | 								<div class="col-auto"> | ||||||
| 									<div class="icon icon-shape bg-gradient-info text-white rounded-circle shadow"> | 									<div class="icon icon-shape bg-gradient-info text-white rounded-circle shadow"> | ||||||
|  | @ -88,10 +76,6 @@ | ||||||
| 									</div> | 									</div> | ||||||
| 								</div> | 								</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> | ||||||
|  | @ -102,79 +86,280 @@ | ||||||
| <div class="container-fluid mt-4"> | <div class="container-fluid mt-4"> | ||||||
| 
 | 
 | ||||||
| 	<div class="row"> | 	<div class="row"> | ||||||
| 		<div class="col-md-8"> | 		<div class="col-md-4"> | ||||||
| 			<div class="card bg-default"> | 			<div class="card bg-default"> | ||||||
| 				<div class="card-header bg-transparent"> | 				<div class="card-header bg-transparent"> | ||||||
| 					<div class="row align-items-center"> | 					<div class="row align-items-center"> | ||||||
| 						<div class="col"> | 						<div class="col"> | ||||||
| 							<h6 class="text-light text-uppercase ls-1 mb-1">Overview</h6> | 							<h6 class="text-light text-uppercase ls-1 mb-1">New</h6> | ||||||
| 							<h5 class="h3 text-white mb-0">Daily Posts</h5> | 							<h5 class="h3 text-white mb-0">Accounts</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> | ||||||
| 				</div> | 				</div> | ||||||
| 				<div class="card-body"> | 				<div v-if="!loaded.accounts" class="card-body text-center"> | ||||||
| 					<!-- Chart --> |                     <b-spinner class="mb-4"></b-spinner> | ||||||
| 					<div class="chart"> |                 </div> | ||||||
| 						<!-- Chart wrapper --> |                 <div v-else class="list-group list-group-scroll"> | ||||||
| 						<canvas id="c1-dark" class="chart-canvas"></canvas> |                     <a | ||||||
| 					</div> |                         v-for="(item, index) in accounts" | ||||||
| 				</div> |                         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> | ||||||
|  |                     </a> | ||||||
|  | 
 | ||||||
|  |                     <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> | 		</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">Posts</h5> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </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 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> | ||||||
|  |                     </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="col-md-4"> | ||||||
| 			<div class="card shadow-none border mb-2" style="min-height:125px"> |             <div class="card bg-default"> | ||||||
| 				<div class="card-body"> |                 <div class="card-header bg-transparent"> | ||||||
| 					<p class="small text-uppercase font-weight-bold text-muted">Failed Jobs (24h)</p> |                     <div class="row align-items-center"> | ||||||
| 					<p class="h2 mb-0">{{$data['failedjobs']}}</p> |                         <div class="col"> | ||||||
| 				</div> |                             <h6 class="text-light text-uppercase ls-1 mb-1">New</h6> | ||||||
| 			</div> |                             <h5 class="h3 text-white mb-0">Instances</h5> | ||||||
| 			<div class="card shadow-none border mb-2" style="min-height:125px"> |                         </div> | ||||||
| 				<div class="card-body"> |                     </div> | ||||||
| 					<p class="small text-uppercase font-weight-bold text-muted">Remote Instances</p> |                 </div> | ||||||
| 					<p class="h2 mb-0">{{$data['instances']}}</p> |                 <div v-if="!loaded.instances" class="card-body text-center"> | ||||||
| 				</div> |                     <b-spinner class="mb-4"></b-spinner> | ||||||
| 			</div> |                 </div> | ||||||
| 			<div class="card shadow-none border mb-2" style="min-height:125px"> |                 <div v-else class="list-group list-group-scroll"> | ||||||
| 				<div class="card-body"> |                     <a | ||||||
| 					<p class="small text-uppercase font-weight-bold text-muted">Photos Uploaded</p> |                         v-for="(item, index) in instances" | ||||||
| 					<p class="h2 mb-0">{{$data['media']}}</p> |                         class="list-group-item" | ||||||
| 				</div> |                         :href="`/i/admin/instances/show/${item.id}`"> | ||||||
| 			</div> | 
 | ||||||
| 			<div class="card shadow-none border" style="min-height:125px"> |                         <div v-text="item.domain" class="font-weight-bold">Loading...</div> | ||||||
| 				<div class="card-body"> | 
 | ||||||
| 					<p class="small text-uppercase font-weight-bold text-muted">Storage Used</p> |                         <div> | ||||||
| 					<p class="human-size mb-0" data-bytes="{{$data['storage']}}">{{$data['storage']}} bytes</p> |                             <div class="d-flex" style="font-size: 13px;"> | ||||||
| 				</div> |                                 <div v-if="item.software" class="badge badge-secondary mr-2" v-text="item.software"></div> | ||||||
| 			</div> |                                 <div v-if="item.user_count" class="badge badge-primary mr-2"> | ||||||
| 		</div> |                                     <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> | ||||||
| 	</div> | 	</div> | ||||||
| @endsection | @endsection | ||||||
| 
 | 
 | ||||||
| @push('scripts') | @push('scripts') | ||||||
| <script type="text/javascript"> | <script type="text/javascript"> | ||||||
| 	$(document).ready(function() { |     let app = new Vue({ | ||||||
| 		$('.human-size').each(function(d,a) { |         el: '#panel', | ||||||
| 			let el = $(a); | 
 | ||||||
| 			let size = el.data('bytes'); |         data: { | ||||||
| 			el.addClass('h2'); |             stats: { | ||||||
| 			el.text(filesize(size, {round: 0})); |                 "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> | </script> | ||||||
| @endpush | @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> | 						</a> | ||||||
| 					</li> | 					</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"> | 					<li class="nav-item"> | ||||||
| 						<a class="nav-link {{request()->is('*settings/system')?'active':''}}" href="/i/admin/settings/system"> | 						<a class="nav-link {{request()->is('*settings/system')?'active':''}}" href="/i/admin/settings/system"> | ||||||
| 							<i class="ni ni-bold-right text-primary"></i> | 							<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('/', '/dashboard'); | ||||||
| 	Route::redirect('timeline', config('app.url').'/timeline'); | 	Route::redirect('timeline', config('app.url').'/timeline'); | ||||||
| 	Route::get('dashboard', 'AdminController@home')->name('admin.home'); | 	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', 'AdminController@reports')->name('admin.reports'); | ||||||
| 	Route::get('reports/show/{id}', 'AdminController@showReport'); | 	Route::get('reports/show/{id}', 'AdminController@showReport'); | ||||||
| 	Route::post('reports/show/{id}', 'AdminController@updateReport'); | 	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/new', 'AdminController@customEmojiStore'); | ||||||
| 	Route::post('custom-emoji/delete/{id}', 'AdminController@customEmojiDelete'); | 	Route::post('custom-emoji/delete/{id}', 'AdminController@customEmojiDelete'); | ||||||
| 	Route::get('custom-emoji/duplicates/{id}', 'AdminController@customEmojiShowDuplicates'); | 	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 () { | Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () { | ||||||
|  |  | ||||||
		Ładowanie…
	
		Reference in New Issue
	
	 Daniel Supernault
						Daniel Supernault