Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 24 additions & 63 deletions apps/dotcom/client/src/pages/admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -488,13 +488,19 @@ function BatchMigrateUsersToGroups() {
const [progressLog, setProgressLog] = useState<string[]>([])
const [error, setError] = useState(null as string | null)
const [isComplete, setIsComplete] = useState(false)
const [stats, setStats] = useState({ successCount: 0, failureCount: 0, totalUsers: 0 })
const [stats, setStats] = useState(
{} as {
successCount: number
failureCount: number
totalUsers: number
usersToMigrate: number
progress: number
}
)
const [unmigratedCount, setUnmigratedCount] = useState<number | null>(null)
const [isLoadingCount, setIsLoadingCount] = useState(false)
const [eventSource, setEventSource] = useState<EventSource | null>(null)
const [batchSize, setBatchSize] = useState(100)
const [batchSleepMs, setBatchSleepMs] = useState(100)
const [maxUsers, setMaxUsers] = useState<number | ''>('')
const [sleepMs, setSleepMs] = useState(100)
const logContainerRef = useRef<HTMLDivElement>(null)

// Cleanup EventSource on unmount
Expand Down Expand Up @@ -540,9 +546,7 @@ function BatchMigrateUsersToGroups() {
}, [eventSource])

const onMigrate = useCallback(async () => {
const migrationMessage = maxUsers
? `Are you sure you want to migrate up to ${maxUsers} users without the groups_backend flag? This action cannot be undone.`
: `Are you sure you want to migrate ALL users without the groups_backend flag? This action cannot be undone.`
const migrationMessage = `Are you sure you want to migrate ALL users without the groups_backend flag? This action cannot be undone.`

if (!window.confirm(migrationMessage)) {
return
Expand All @@ -552,16 +556,12 @@ function BatchMigrateUsersToGroups() {
setError(null)
setProgressLog([])
setIsComplete(false)
setStats({ successCount: 0, failureCount: 0, totalUsers: 0 })
setStats({ successCount: 0, failureCount: 0, totalUsers: 0, usersToMigrate: 0, progress: 0 })

try {
const params = new URLSearchParams({
batchSize: batchSize.toString(),
batchSleepMs: batchSleepMs.toString(),
sleepMs: sleepMs.toString(),
})
if (maxUsers) {
params.set('maxUsers', maxUsers.toString())
}
const es = new EventSource(`/api/app/admin/migrate_users_batch?${params}`)
setEventSource(es)

Expand All @@ -579,16 +579,7 @@ function BatchMigrateUsersToGroups() {

// Update stats from details
if (data.details) {
if (data.details.totalUsers !== undefined) {
setStats((prev) => ({ ...prev, totalUsers: data.details.totalUsers }))
}
if (data.details.successCount !== undefined && data.details.failureCount !== undefined) {
setStats({
totalUsers: data.details.totalUsers || 0,
successCount: data.details.successCount,
failureCount: data.details.failureCount,
})
}
setStats(data.details)
}

if (data.type === 'complete') {
Expand All @@ -615,7 +606,7 @@ function BatchMigrateUsersToGroups() {
setIsMigrating(false)
setEventSource(null)
}
}, [batchSize, batchSleepMs, maxUsers])
}, [sleepMs])

return (
<div className={styles.dangerZone}>
Expand All @@ -640,55 +631,27 @@ function BatchMigrateUsersToGroups() {

<p className="tla-text_ui__small">
This will migrate all users who don&apos;t have the groups_backend flag. The process will
run sequentially and report progress in real-time. Configure the batch size (number of users
processed before a pause), sleep duration (milliseconds to wait between batches), and max
users (limit for incremental rollout, leave empty to migrate all) below.
run sequentially (one user at a time) and report progress in real-time. Configure the sleep
duration (milliseconds to wait between each user migration) below.
</p>

{error && <div className={styles.errorMessage}>{error}</div>}

{/* Configuration Inputs */}
<div className={styles.configContainer}>
<div>
<label htmlFor="batchSize">Batch size:</label>
<label htmlFor="sleepMs">Sleep between migrations (ms):</label>
<input
id="batchSize"
id="sleepMs"
type="number"
value={batchSize}
onChange={(e) => setBatchSize(Number(e.target.value))}
disabled={isMigrating}
min={1}
className={styles.searchInput}
style={{ width: '100px', marginLeft: '8px' }}
/>
</div>
<div>
<label htmlFor="batchSleepMs">Sleep between batches (ms):</label>
<input
id="batchSleepMs"
type="number"
value={batchSleepMs}
onChange={(e) => setBatchSleepMs(Number(e.target.value))}
value={sleepMs}
onChange={(e) => setSleepMs(Number(e.target.value))}
disabled={isMigrating}
min={0}
className={styles.searchInput}
style={{ width: '100px', marginLeft: '8px' }}
/>
</div>
<div>
<label htmlFor="maxUsers">Max users (leave empty for all):</label>
<input
id="maxUsers"
type="number"
value={maxUsers}
onChange={(e) => setMaxUsers(e.target.value === '' ? '' : Number(e.target.value))}
disabled={isMigrating}
min={1}
placeholder="All users"
className={styles.searchInput}
style={{ width: '100px', marginLeft: '8px' }}
/>
</div>
</div>

{/* Stats Display */}
Expand All @@ -699,8 +662,8 @@ function BatchMigrateUsersToGroups() {
<span className={styles.statValue}>{stats.totalUsers}</span>
</div>
<div className={styles.statItem}>
<span className={styles.statLabel}>Completed:</span>
<span className={styles.statValue}>{stats.successCount + stats.failureCount}</span>
<span className={styles.statLabel}>Users to Migrate:</span>
<span className={styles.statValue}>{stats.usersToMigrate}</span>
</div>
<div className={styles.statItem}>
<span className={styles.statLabel}>Succeeded:</span>
Expand All @@ -712,9 +675,7 @@ function BatchMigrateUsersToGroups() {
</div>
<div className={styles.statItem}>
<span className={styles.statLabel}>Progress:</span>
<span className={styles.statValue}>
{Math.round(((stats.successCount + stats.failureCount) / stats.totalUsers) * 100)}%
</span>
<span className={styles.statValue}>{(stats.progress * 100).toFixed(2)}%</span>
</div>
</div>
)}
Expand Down
7 changes: 6 additions & 1 deletion apps/dotcom/sync-worker/src/TLUserDurableObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,16 +565,21 @@ export class TLUserDurableObject extends DurableObject<Environment> {
async admin_migrateToGroups(userId: string, inviteSecret: string | null = null) {
this.userId ??= userId

this.log.debug('migrating to groups', userId, inviteSecret)
// Call the Postgres migration function
const result = await sql<{
files_migrated: number
pinned_files_migrated: number
flag_added: boolean
}>`SELECT * FROM migrate_user_to_groups(${userId}, ${inviteSecret})`.execute(this.db)

// Reboot the user's cache to pick up the new data structure
this.log.debug('migration result', result.rows[0])

await this.env.USER_DO_SNAPSHOTS.delete(getUserDoSnapshotKey(this.env, userId))
await this.cache?.reboot({ delay: false, source: 'admin', hard: true })

this.log.debug('migration complete, user rebooted')

return result.rows[0]
}

Expand Down
2 changes: 1 addition & 1 deletion apps/dotcom/sync-worker/src/UserDataSyncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export class UserDataSyncer {
this.ctx.abort()
return
}
this.log.debug('rebooting', source)
this.log.debug('rebooting', source, 'hard:', hard, 'delay:', delay)
this.logEvent({ type: 'reboot', id: this.userId })
await this.queue.push(async () => {
if (delay) {
Expand Down
Loading
Loading