feat: 管理者が特定ユーザーのアップロードしたファイル一覧を見れるように
This commit is contained in:
		
							parent
							
								
									905d8625f8
								
							
						
					
					
						commit
						696e8add00
					
				
					 5 changed files with 133 additions and 86 deletions
				
			
		|  | @ -13,7 +13,7 @@ You should also include the user name that made the change. | |||
| 
 | ||||
| ### Improvements | ||||
| - Server: Add rate limit to i/notifications @tamaina | ||||
| - Client: Improve files page of control panel @syuilo | ||||
| - Client: Improve control panel @syuilo | ||||
| - Client: Show warning in control panel when there is an unresolved abuse report @syuilo | ||||
| - Improve player detection in URL preview @mei23 | ||||
| - Add Badge Image to Push Notification #8012 @tamaina | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import define from '../../../define.js'; | ||||
| import { DriveFiles } from '@/models/index.js'; | ||||
| import define from '../../../define.js'; | ||||
| import { makePaginationQuery } from '../../../common/make-pagination-query.js'; | ||||
| 
 | ||||
| export const meta = { | ||||
|  | @ -25,8 +25,9 @@ export const paramDef = { | |||
| 		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, | ||||
| 		sinceId: { type: 'string', format: 'misskey:id' }, | ||||
| 		untilId: { type: 'string', format: 'misskey:id' }, | ||||
| 		userId: { type: 'string', format: 'misskey:id', nullable: true }, | ||||
| 		type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) }, | ||||
| 		origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" }, | ||||
| 		origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, | ||||
| 		hostname: { | ||||
| 			type: 'string', | ||||
| 			nullable: true, | ||||
|  | @ -41,14 +42,18 @@ export const paramDef = { | |||
| export default define(meta, paramDef, async (ps, me) => { | ||||
| 	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId); | ||||
| 
 | ||||
| 	if (ps.origin === 'local') { | ||||
| 		query.andWhere('file.userHost IS NULL'); | ||||
| 	} else if (ps.origin === 'remote') { | ||||
| 		query.andWhere('file.userHost IS NOT NULL'); | ||||
| 	} | ||||
| 	if (ps.userId) { | ||||
| 		query.andWhere('file.userId = :userId', { userId: ps.userId }); | ||||
| 	} else { | ||||
| 		if (ps.origin === 'local') { | ||||
| 			query.andWhere('file.userHost IS NULL'); | ||||
| 		} else if (ps.origin === 'remote') { | ||||
| 			query.andWhere('file.userHost IS NOT NULL'); | ||||
| 		} | ||||
| 
 | ||||
| 	if (ps.hostname) { | ||||
| 		query.andWhere('file.userHost = :hostname', { hostname: ps.hostname }); | ||||
| 		if (ps.hostname) { | ||||
| 			query.andWhere('file.userHost = :hostname', { hostname: ps.hostname }); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.type) { | ||||
|  |  | |||
							
								
								
									
										93
									
								
								packages/client/src/components/file-list-for-admin.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								packages/client/src/components/file-list-for-admin.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,93 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }"> | ||||
| 		<button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _button"> | ||||
| 			<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||
| 			<div v-if="viewMode === 'list'" class="body"> | ||||
| 				<div> | ||||
| 					<small style="opacity: 0.7;">{{ file.name }}</small> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<MkAcct v-if="file.user" :user="file.user"/> | ||||
| 					<div v-else>{{ $ts.system }}</div> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<span style="margin-right: 1em;">{{ file.type }}</span> | ||||
| 					<span>{{ bytes(file.size) }}</span> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</button> | ||||
| 	</MkPagination> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed } from 'vue'; | ||||
| import * as Acct from 'misskey-js/built/acct'; | ||||
| import MkSwitch from '@/components/ui/switch.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; | ||||
| import bytes from '@/filters/bytes'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	pagination: any; | ||||
| 	viewMode: 'grid' | 'list'; | ||||
| }>(); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .urempief { | ||||
| 	margin-top: var(--margin); | ||||
| 
 | ||||
| 	&.list { | ||||
| 		> .file { | ||||
| 			display: flex; | ||||
| 			width: 100%; | ||||
| 			box-sizing: border-box; | ||||
| 			text-align: left; | ||||
| 			align-items: center; | ||||
| 
 | ||||
| 			&:hover { | ||||
| 				color: var(--accent); | ||||
| 			} | ||||
| 
 | ||||
| 			> .thumbnail { | ||||
| 				width: 128px; | ||||
| 				height: 128px; | ||||
| 			} | ||||
| 
 | ||||
| 			> .body { | ||||
| 				margin-left: 0.3em; | ||||
| 				padding: 8px; | ||||
| 				flex: 1; | ||||
| 
 | ||||
| 				@media (max-width: 500px) { | ||||
| 					font-size: 14px; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.grid { | ||||
| 		display: grid; | ||||
| 		grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | ||||
| 		grid-gap: 12px; | ||||
| 		margin: var(--margin) 0; | ||||
| 
 | ||||
| 		> .file { | ||||
| 			aspect-ratio: 1; | ||||
| 		 | ||||
| 			> .thumbnail { | ||||
| 				width: 100%; | ||||
| 				height: 100%; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -16,32 +16,15 @@ | |||
| 							<template #label>{{ $ts.host }}</template> | ||||
| 						</MkInput> | ||||
| 					</div> | ||||
| 					<div class="inputs" style="display: flex; padding-top: 1.2em;"> | ||||
| 					<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap; padding-top: 1.2em;"> | ||||
| 						<MkInput v-model="userId" :debounce="true" type="search" style="margin: 0; flex: 1;"> | ||||
| 							<template #label>User ID</template> | ||||
| 						</MkInput> | ||||
| 						<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> | ||||
| 							<template #label>MIME type</template> | ||||
| 						</MkInput> | ||||
| 					</div> | ||||
| 					<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }"> | ||||
| 						<button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _button" @click="show(file, $event)"> | ||||
| 							<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||
| 							<div v-if="viewMode === 'list'" class="body"> | ||||
| 								<div> | ||||
| 									<small style="opacity: 0.7;">{{ file.name }}</small> | ||||
| 								</div> | ||||
| 								<div> | ||||
| 									<MkAcct v-if="file.user" :user="file.user"/> | ||||
| 									<div v-else>{{ $ts.system }}</div> | ||||
| 								</div> | ||||
| 								<div> | ||||
| 									<span style="margin-right: 1em;">{{ file.type }}</span> | ||||
| 									<span>{{ bytes(file.size) }}</span> | ||||
| 								</div> | ||||
| 								<div> | ||||
| 									<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</button> | ||||
| 					</MkPagination> | ||||
| 					<MkFileListForAdmin :pagination="pagination" :view-mode="viewMode"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</MkSpacer> | ||||
|  | @ -56,9 +39,7 @@ import XHeader from './_header_.vue'; | |||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; | ||||
| import MkFileListForAdmin from '@/components/file-list-for-admin.vue'; | ||||
| import bytes from '@/filters/bytes'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | @ -67,12 +48,14 @@ import { definePageMetadata } from '@/scripts/page-metadata'; | |||
| let origin = $ref('local'); | ||||
| let type = $ref(null); | ||||
| let searchHost = $ref(''); | ||||
| let userId = $ref(''); | ||||
| let viewMode = $ref('grid'); | ||||
| const pagination = { | ||||
| 	endpoint: 'admin/drive/files' as const, | ||||
| 	limit: 10, | ||||
| 	params: computed(() => ({ | ||||
| 		type: (type && type !== '') ? type : null, | ||||
| 		userId: (userId && userId !== '') ? userId : null, | ||||
| 		origin: origin, | ||||
| 		hostname: (searchHost && searchHost !== '') ? searchHost : null, | ||||
| 	})), | ||||
|  | @ -134,54 +117,5 @@ definePageMetadata(computed(() => ({ | |||
| <style lang="scss" scoped> | ||||
| .xrmjdkdw { | ||||
| 	margin: var(--margin); | ||||
| 
 | ||||
| 	.urempief { | ||||
| 		margin-top: var(--margin); | ||||
| 
 | ||||
| 		&.list { | ||||
| 			> .file { | ||||
| 				display: flex; | ||||
| 				width: 100%; | ||||
| 				box-sizing: border-box; | ||||
| 				text-align: left; | ||||
| 				align-items: center; | ||||
| 
 | ||||
| 				&:hover { | ||||
| 					color: var(--accent); | ||||
| 				} | ||||
| 
 | ||||
| 				> .thumbnail { | ||||
| 					width: 128px; | ||||
| 					height: 128px; | ||||
| 				} | ||||
| 
 | ||||
| 				> .body { | ||||
| 					margin-left: 0.3em; | ||||
| 					padding: 8px; | ||||
| 					flex: 1; | ||||
| 
 | ||||
| 					@media (max-width: 500px) { | ||||
| 						font-size: 14px; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		&.grid { | ||||
| 			display: grid; | ||||
| 			grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | ||||
| 			grid-gap: 12px; | ||||
| 			margin: var(--margin) 0; | ||||
| 
 | ||||
| 			> .file { | ||||
| 				aspect-ratio: 1; | ||||
| 			 | ||||
| 				> .thumbnail { | ||||
| 					width: 100%; | ||||
| 					height: 100%; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -76,6 +76,9 @@ | |||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'files'" class="_formRoot"> | ||||
| 				<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'ap'" class="_formRoot"> | ||||
| 				<MkObjectView v-if="ap" tall :value="user"> | ||||
| 				</MkObjectView> | ||||
|  | @ -105,6 +108,7 @@ import FormButton from '@/components/ui/button.vue'; | |||
| import MkKeyValue from '@/components/key-value.vue'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import MkFileListForAdmin from '@/components/file-list-for-admin.vue'; | ||||
| import * as os from '@/os'; | ||||
| import number from '@/filters/number'; | ||||
| import bytes from '@/filters/bytes'; | ||||
|  | @ -127,6 +131,13 @@ let ap = $ref(null); | |||
| let moderator = $ref(false); | ||||
| let silenced = $ref(false); | ||||
| let suspended = $ref(false); | ||||
| const filesPagination = { | ||||
| 	endpoint: 'admin/drive/files' as const, | ||||
| 	limit: 10, | ||||
| 	params: computed(() => ({ | ||||
| 		userId: props.userId, | ||||
| 	})), | ||||
| }; | ||||
| 
 | ||||
| function createFetcher() { | ||||
| 	if (iAmModerator) { | ||||
|  | @ -244,7 +255,11 @@ const headerTabs = $computed(() => [{ | |||
| 	key: 'chart', | ||||
| 	title: i18n.ts.charts, | ||||
| 	icon: 'fas fa-chart-simple', | ||||
| }, { | ||||
| }, iAmModerator ? { | ||||
| 	key: 'files', | ||||
| 	title: i18n.ts.files, | ||||
| 	icon: 'fas fa-cloud', | ||||
| } : null, { | ||||
| 	key: 'ap', | ||||
| 	title: 'AP', | ||||
| 	icon: 'fas fa-share-alt', | ||||
|  | @ -252,7 +267,7 @@ const headerTabs = $computed(() => [{ | |||
| 	key: 'raw', | ||||
| 	title: 'Raw data', | ||||
| 	icon: 'fas fa-code', | ||||
| }]); | ||||
| }].filter(x => x != null)); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: user ? acct(user) : i18n.ts.userInfo, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue