Skip to content

Commit

Permalink
✨ Allow dynamically sharding the queue through env var (#30)
Browse files Browse the repository at this point in the history
* ✨ Use dids in all links instead of handles

* 🐛 Maintain search params when redirecting

* 🐛 Wait for auth before attempting to resolve handle

* ✨ Add language filter to moderation queue

* ✨ Move lang filter to use tags

* 🐛 Unique key for option

* ✨ Shard the moderation queue on the client side

* ✨ Add tag display and emit form

* ✨ Allow multiple language inclusion and exclusion from the language picker

* 💄 Fix style of the lang picker button

* ⬆️ Upgrade atproto api version

* 💄 Add inline comments and refactor

* 💄 Improve the language picker styling and add help text

* 🐛 Use tag filters in the query key to refresh after lang selection

* 🔨 Refactor queue config to env var
  • Loading branch information
foysalit authored Feb 22, 2024
1 parent 3089191 commit 97bfc0d
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ next-env.d.ts

# cypress
cypress/videos

.env
30 changes: 25 additions & 5 deletions app/reports/page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from 'next/navigation'
import { useInfiniteQuery } from '@tanstack/react-query'
import {
AtUri,
ComAtprotoAdminDefs,
ComAtprotoAdminEmitModerationEvent,
ComAtprotoAdminQueryModerationStatuses,
Expand All @@ -24,6 +25,7 @@ import { useFluentReportSearch } from '@/reports/useFluentReportSearch'
import { SubjectTable } from 'components/subject/table'
import { useTitle } from 'react-use'
import { LanguagePicker } from '@/common/LanguagePicker'
import { QueueSelector, QUEUE_NAMES } from '@/reports/QueueSelector'

const TABS = [
{
Expand Down Expand Up @@ -159,6 +161,7 @@ export const ReportsPageContent = () => {
const reviewState = params.get('reviewState')
const tags = params.get('tags')
const excludeTags = params.get('excludeTags')
const queueName = params.get('queueName')
const { sortField, sortDirection } = getSortParams(params)
const { getReportSearchParams } = useFluentReportSearch()
const { lastReviewedBy, subject, reporters } = getReportSearchParams()
Expand Down Expand Up @@ -191,6 +194,7 @@ export const ReportsPageContent = () => {
appealed,
tags,
excludeTags,
queueName,
},
],
queryFn: async ({ pageParam }) => {
Expand Down Expand Up @@ -234,7 +238,7 @@ export const ReportsPageContent = () => {
}
})

return await getModerationQueue(queryParams)
return await getModerationQueue(queryParams, queueName)
},
getNextPageParam: (lastPage) => lastPage.cursor,
})
Expand All @@ -257,7 +261,7 @@ export const ReportsPageContent = () => {

return (
<>
<SectionHeader title="Queue" tabs={TABS} current={currentTab}>
<SectionHeader title={<QueueSelector />} tabs={TABS} current={currentTab}>
<div className="flex-1 lg:text-right lg:pr-2 pb-4 px-1 pt-5 lg:pt-0">
<button
role="button"
Expand Down Expand Up @@ -312,16 +316,32 @@ function getTabFromParams({ reviewState }: { reviewState?: string | null }) {

async function getModerationQueue(
opts: ComAtprotoAdminQueryModerationStatuses.QueryParams = {},
queueName: string | null,
) {
const res = await client.api.com.atproto.admin.queryModerationStatuses(
const { data } = await client.api.com.atproto.admin.queryModerationStatuses(
{
limit: 25,
limit: 50,
includeMuted: true,
...opts,
},
{ headers: client.adminHeaders() },
)
return res.data

const queueDivider = QUEUE_NAMES.length
const queueIndex = QUEUE_NAMES.indexOf(queueName ?? '')
const statusesInQueue = queueName
? data.subjectStatuses.filter((status) => {
const subjectDid =
status.subject.$type === 'com.atproto.admin.defs#repoRef'
? status.subject.did
: new AtUri(`${status.subject.uri}`).host
const queueDeciderCharCode =
`${subjectDid}`.split(':').pop()?.charCodeAt(0) || 0
return queueDeciderCharCode % queueDivider === queueIndex
})
: data.subjectStatuses

return { cursor: data.cursor, subjectStatuses: statusesInQueue }
}

function unique<T>(arr: T[]) {
Expand Down
12 changes: 8 additions & 4 deletions components/SectionHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function SectionHeader({
current,
children,
}: {
title: string
title: string | JSX.Element
tabs: Tab[]
current: string
children?: ReactNode
Expand All @@ -27,9 +27,13 @@ export function SectionHeader({
className={`px-6 pt-4 ${!!tabs.length ? 'border-b border-gray-200' : ''}`}
>
<div className="sm:flex sm:items-baseline">
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{title}
</h3>
{typeof title === 'string' ? (
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{title}
</h3>
) : (
title
)}
<div className="mt-4 sm:mt-0 sm:ml-10 flex-1">
<nav className="-mb-px flex flex-wrap">
<div className="space-x-8 w-full lg:w-2/3">
Expand Down
68 changes: 68 additions & 0 deletions components/reports/QueueSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Dropdown } from '@/common/Dropdown'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'

type QueueConfig = Record<string, { name: string }>

const getQueueConfig = () => {
const config = process.env.NEXT_PUBLIC_QUEUE_CONFIG || '{}'
try {
return JSON.parse(config) as QueueConfig
} catch (err) {
return {}
}
}

export const QUEUES = getQueueConfig()
export const QUEUE_NAMES = Object.keys(QUEUES)

export const QueueSelector = () => {
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const queueName = searchParams.get('queueName')

const selectQueue = (queue: string) => () => {
const nextParams = new URLSearchParams(searchParams)
if (queue) {
nextParams.set('queueName', queue)
} else {
nextParams.delete('queueName')
}
router.push((pathname ?? '') + '?' + nextParams.toString())
}

// If no queues are configured, just use a static title
if (!QUEUE_NAMES.length) {
return (
<h3 className="flex items-center text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
Queue
</h3>
)
}

return (
<Dropdown
containerClassName="inline-block"
className="inline-flex items-center"
items={[
{
text: 'All',
onClick: selectQueue(''),
},
...QUEUE_NAMES.map((q) => ({
text: QUEUES[q].name,
onClick: selectQueue(q),
})),
]}
>
<h3 className="flex items-center text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{queueName ? `${QUEUES[queueName].name} Queue` : 'Queue'}
<ChevronDownIcon
className="text-gray-900 dark:text-gray-200 h-4 w-4"
aria-hidden="true"
/>
</h3>
</Dropdown>
)
}
2 changes: 1 addition & 1 deletion components/repositories/DidHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid'
import { useQuery } from '@tanstack/react-query'

const PLC_DIRECTORY_URL =
process.env.PLC_DIRECTORY_URL || `https://plc.directory`
process.env.NEXT_PUBLIC_PLC_DIRECTORY_URL || `https://plc.directory`

// Not a complete mapping of the DID history event, just the parts we care about in the UI
type DidHistoryEvent = {
Expand Down
10 changes: 10 additions & 0 deletions environment.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Next from 'next'

declare global {
namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_PLC_DIRECTORY_URL?: string
NEXT_PUBLIC_QUEUE_CONFIG?: string
}
}
}

0 comments on commit 97bfc0d

Please sign in to comment.