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 | ### Improvements | ||||||
| - Server: Add rate limit to i/notifications @tamaina | - 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 | - Client: Show warning in control panel when there is an unresolved abuse report @syuilo | ||||||
| - Improve player detection in URL preview @mei23 | - Improve player detection in URL preview @mei23 | ||||||
| - Add Badge Image to Push Notification #8012 @tamaina | - Add Badge Image to Push Notification #8012 @tamaina | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import define from '../../../define.js'; |  | ||||||
| import { DriveFiles } from '@/models/index.js'; | import { DriveFiles } from '@/models/index.js'; | ||||||
|  | import define from '../../../define.js'; | ||||||
| import { makePaginationQuery } from '../../../common/make-pagination-query.js'; | import { makePaginationQuery } from '../../../common/make-pagination-query.js'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
|  | @ -25,8 +25,9 @@ export const paramDef = { | ||||||
| 		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, | 		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, | ||||||
| 		sinceId: { type: 'string', format: 'misskey:id' }, | 		sinceId: { type: 'string', format: 'misskey:id' }, | ||||||
| 		untilId: { 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) }, | 		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: { | 		hostname: { | ||||||
| 			type: 'string', | 			type: 'string', | ||||||
| 			nullable: true, | 			nullable: true, | ||||||
|  | @ -41,14 +42,18 @@ export const paramDef = { | ||||||
| export default define(meta, paramDef, async (ps, me) => { | export default define(meta, paramDef, async (ps, me) => { | ||||||
| 	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId); | 	const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId); | ||||||
| 
 | 
 | ||||||
| 	if (ps.origin === 'local') { | 	if (ps.userId) { | ||||||
| 		query.andWhere('file.userHost IS NULL'); | 		query.andWhere('file.userId = :userId', { userId: ps.userId }); | ||||||
| 	} else if (ps.origin === 'remote') { | 	} else { | ||||||
| 		query.andWhere('file.userHost IS NOT NULL'); | 		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) { | 		if (ps.hostname) { | ||||||
| 		query.andWhere('file.userHost = :hostname', { hostname: ps.hostname }); | 			query.andWhere('file.userHost = :hostname', { hostname: ps.hostname }); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (ps.type) { | 	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> | 							<template #label>{{ $ts.host }}</template> | ||||||
| 						</MkInput> | 						</MkInput> | ||||||
| 					</div> | 					</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;"> | 						<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> | ||||||
| 							<template #label>MIME type</template> | 							<template #label>MIME type</template> | ||||||
| 						</MkInput> | 						</MkInput> | ||||||
| 					</div> | 					</div> | ||||||
| 					<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }"> | 					<MkFileListForAdmin :pagination="pagination" :view-mode="viewMode"/> | ||||||
| 						<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> |  | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</MkSpacer> | 		</MkSpacer> | ||||||
|  | @ -56,9 +39,7 @@ import XHeader from './_header_.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkSelect from '@/components/form/select.vue'; | import MkSelect from '@/components/form/select.vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkFileListForAdmin from '@/components/file-list-for-admin.vue'; | ||||||
| import MkContainer from '@/components/ui/container.vue'; |  | ||||||
| import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; |  | ||||||
| import bytes from '@/filters/bytes'; | import bytes from '@/filters/bytes'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | @ -67,12 +48,14 @@ import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| let origin = $ref('local'); | let origin = $ref('local'); | ||||||
| let type = $ref(null); | let type = $ref(null); | ||||||
| let searchHost = $ref(''); | let searchHost = $ref(''); | ||||||
|  | let userId = $ref(''); | ||||||
| let viewMode = $ref('grid'); | let viewMode = $ref('grid'); | ||||||
| const pagination = { | const pagination = { | ||||||
| 	endpoint: 'admin/drive/files' as const, | 	endpoint: 'admin/drive/files' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
| 	params: computed(() => ({ | 	params: computed(() => ({ | ||||||
| 		type: (type && type !== '') ? type : null, | 		type: (type && type !== '') ? type : null, | ||||||
|  | 		userId: (userId && userId !== '') ? userId : null, | ||||||
| 		origin: origin, | 		origin: origin, | ||||||
| 		hostname: (searchHost && searchHost !== '') ? searchHost : null, | 		hostname: (searchHost && searchHost !== '') ? searchHost : null, | ||||||
| 	})), | 	})), | ||||||
|  | @ -134,54 +117,5 @@ definePageMetadata(computed(() => ({ | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .xrmjdkdw { | .xrmjdkdw { | ||||||
| 	margin: var(--margin); | 	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> | </style> | ||||||
|  |  | ||||||
|  | @ -76,6 +76,9 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</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"> | 			<div v-else-if="tab === 'ap'" class="_formRoot"> | ||||||
| 				<MkObjectView v-if="ap" tall :value="user"> | 				<MkObjectView v-if="ap" tall :value="user"> | ||||||
| 				</MkObjectView> | 				</MkObjectView> | ||||||
|  | @ -105,6 +108,7 @@ import FormButton from '@/components/ui/button.vue'; | ||||||
| import MkKeyValue from '@/components/key-value.vue'; | import MkKeyValue from '@/components/key-value.vue'; | ||||||
| import MkSelect from '@/components/form/select.vue'; | import MkSelect from '@/components/form/select.vue'; | ||||||
| import FormSuspense from '@/components/form/suspense.vue'; | import FormSuspense from '@/components/form/suspense.vue'; | ||||||
|  | import MkFileListForAdmin from '@/components/file-list-for-admin.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import number from '@/filters/number'; | import number from '@/filters/number'; | ||||||
| import bytes from '@/filters/bytes'; | import bytes from '@/filters/bytes'; | ||||||
|  | @ -127,6 +131,13 @@ let ap = $ref(null); | ||||||
| let moderator = $ref(false); | let moderator = $ref(false); | ||||||
| let silenced = $ref(false); | let silenced = $ref(false); | ||||||
| let suspended = $ref(false); | let suspended = $ref(false); | ||||||
|  | const filesPagination = { | ||||||
|  | 	endpoint: 'admin/drive/files' as const, | ||||||
|  | 	limit: 10, | ||||||
|  | 	params: computed(() => ({ | ||||||
|  | 		userId: props.userId, | ||||||
|  | 	})), | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| function createFetcher() { | function createFetcher() { | ||||||
| 	if (iAmModerator) { | 	if (iAmModerator) { | ||||||
|  | @ -244,7 +255,11 @@ const headerTabs = $computed(() => [{ | ||||||
| 	key: 'chart', | 	key: 'chart', | ||||||
| 	title: i18n.ts.charts, | 	title: i18n.ts.charts, | ||||||
| 	icon: 'fas fa-chart-simple', | 	icon: 'fas fa-chart-simple', | ||||||
| }, { | }, iAmModerator ? { | ||||||
|  | 	key: 'files', | ||||||
|  | 	title: i18n.ts.files, | ||||||
|  | 	icon: 'fas fa-cloud', | ||||||
|  | } : null, { | ||||||
| 	key: 'ap', | 	key: 'ap', | ||||||
| 	title: 'AP', | 	title: 'AP', | ||||||
| 	icon: 'fas fa-share-alt', | 	icon: 'fas fa-share-alt', | ||||||
|  | @ -252,7 +267,7 @@ const headerTabs = $computed(() => [{ | ||||||
| 	key: 'raw', | 	key: 'raw', | ||||||
| 	title: 'Raw data', | 	title: 'Raw data', | ||||||
| 	icon: 'fas fa-code', | 	icon: 'fas fa-code', | ||||||
| }]); | }].filter(x => x != null)); | ||||||
| 
 | 
 | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
| 	title: user ? acct(user) : i18n.ts.userInfo, | 	title: user ? acct(user) : i18n.ts.userInfo, | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue