main
ts 129 lines 5.79 KB
Raw
1 import { alertDialog, formDialog, newDialog } from './dialog'
2 import { createElement as h, Fragment, useEffect, useState } from 'react'
3 import { Group, Upload } from '@mui/icons-material'
4 import { Box } from '@mui/material'
5 import { apiCall } from './api'
6 import { apiNewPassword, HTTP_CONFLICT, prefix, readFile, selectFiles } from './misc'
7 import { IconProgress } from './mui'
8 import { NumberField, BoolField } from '@hfs/mui-grid-form'
9 import Parser from '@gregoranders/csv';
10
11 export async function importAccountsCsv(cb?: () => void) {
12 selectFiles(async list => {
13 const f = list?.[0]
14 if (!f) return
15 const txt = await readFile(f)
16 if (!txt) return
17 const parser = new Parser()
18 const rows = parser.parse(txt.trim())
19 const colField = { comp: NumberField, min: 1, max: 9, xs: 6, typing: true, }
20 const initialConfig = {
21 skipFirstLines: 0,
22 usernameColumn: 1,
23 passwordColumn: 2,
24 groupColumn: 3,
25 redirectColumn: 4,
26 overwriteExistingAccounts: false,
27 }
28 const cfg = await formDialog<typeof initialConfig>({
29 title: "Import accounts from CSV",
30 dialogProps: { maxWidth: 'sm' },
31 values: initialConfig,
32 form: values => {
33 const row = rows[values.skipFirstLines || 0]
34 const rec = getRec(row, { ...initialConfig, ...values })
35 return {
36 save: { startIcon: h(Upload), children: 'Go' },
37 fields: [
38 h(Box, { sx: { p: 1 } }, "Total lines:", rows.length),
39 { k: 'skipFirstLines', comp: NumberField, max: rows.length-1, typing: true, md: 6,
40 helperText: h(Fragment, {}, "First line: ", h('code', {}, row.join(', ')) ),
41 },
42 { k: 'overwriteExistingAccounts', comp: BoolField, md: 6 },
43 { k: 'usernameColumn', ...colField,
44 helperText: h(Fragment, {}, "First username: ", rec.u),
45 },
46 { k: 'passwordColumn', ...colField,
47 helperText: h(Fragment, {}, "First password: ", rec.p),
48 },
49 { k: 'groupColumn', ...colField,
50 helperText: h(Fragment, {}, "First group: ", rec.g),
51 },
52 { k: 'redirectColumn', ...colField,
53 helperText: h(Fragment, {}, "First redirect: ", rec.r),
54 },
55 ],
56 }
57 },
58 })
59 if (!cfg) return
60 const { close } = newDialog({
61 title: "Importing...",
62 Content() {
63 const [progress, setProgress] = useState(0)
64 const [record, setRecord] = useState<undefined | ReturnType<typeof getRec>>()
65 useEffect(() => {
66 let stop = false
67 setTimeout(async () => {
68 if (stop) return
69 let bad = 0
70 let already =0
71 let skip = cfg.skipFirstLines
72 const total = rows.length - skip
73 try {
74 let i = 0
75 for (const row of rows) {
76 if (stop) return
77 if (skip) {
78 skip--
79 continue
80 }
81 const rec = getRec(row, cfg)
82 setRecord(rec)
83 setProgress(i++ / total)
84 await apiCall('add_account', {
85 username: rec.u,
86 belongs: rec.g?.split(','),
87 redirect: rec.r,
88 overwrite: cfg.overwriteExistingAccounts
89 }).then(() => {
90 if (rec.p)
91 return apiNewPassword(rec.u, rec.p)
92 }, e => {
93 if (e.code === HTTP_CONFLICT)
94 return already++
95 bad++
96 })
97 }
98 }
99 finally {
100 close()
101 const good = total - bad - already
102 const msg = "Results: " + [
103 prefix('', bad, " failed"),
104 prefix('', good, " succeeded"),
105 prefix('', already, " skipped because already present"),
106 ].filter(Boolean).join(', ')
107 alertDialog(msg, !good && bad ? 'error' : (bad || already) ? 'warning' : 'success')
108 cb?.()
109 }
110 })
111 return () => { stop = true }
112 }, [])
113 return h(Box, { sx: { display: 'flex', gap: 2, alignItems: 'center' } },
114 h(IconProgress, { icon: Group, progress }),
115 record?.u,
116 )
117 }
118 })
119
120 function getRec(row: string[], config: typeof initialConfig) {
121 return {
122 u: row[config.usernameColumn - 1],
123 p: row[config.passwordColumn - 1],
124 g: row[config.groupColumn - 1],
125 r: row[config.redirectColumn - 1],
126 }
127 }
128 }, { multiple: false, accept: '.csv' })
129 }