Skip to content
This repository has been archived by the owner on Oct 25, 2023. It is now read-only.

Manual import of fragments from smuggler in local mode #429

Merged
merged 23 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
27 changes: 24 additions & 3 deletions truthsayer-archaeologist-communication/src/message/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,37 @@ import type {
StorageApiMsgReturnValue,
} from 'smuggler-api'
import { VersionStruct } from '../Version'
import { log, errorise } from 'armoury'

export type StorageType =
| 'datacenter' /** Stores user data in @file storage_api_datacenter.ts */
| 'browser_ext' /** Stores user data in @file storage_api_browser_ext.ts */

export type AppSettings = {
storageType:
| 'datacenter' /** Stores user data in @file storage_api_datacenter.ts */
| 'browser_ext' /** Stores user data in @file storage_api_browser_ext.ts */
storageType: StorageType
}

export function defaultSettings(): AppSettings {
return { storageType: 'datacenter' }
}

export async function getAppSettings(): Promise<AppSettings> {
try {
const response = await FromTruthsayer.sendMessage({
type: 'GET_APP_SETTINGS_REQUEST',
})
return response.settings
} catch (err) {
const defaults = defaultSettings()
log.warning(
`Failed to get user's app settings,` +
` falling back to ${JSON.stringify(defaults)}; ` +
`error - ${errorise(err).message}`
)
return defaults
}
}

export namespace FromTruthsayer {
export type GetArchaeologistStateRequest = {
type: 'GET_ARCHAEOLOGIST_STATE_REQUEST'
Expand Down
156 changes: 156 additions & 0 deletions truthsayer/src/external-import/DataCentreImporter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React from 'react'
import styled from '@emotion/styled'
import { useAsyncEffect } from 'use-async-effect'

import * as truthsayer_archaeologist_communication from 'truthsayer-archaeologist-communication'
import { Spinner } from 'elementary'
import { genOriginId } from 'armoury'
import { makeDatacenterStorageApi, StorageApi, Nid } from 'smuggler-api'

import { getLogoImage } from '../util/env'
import { MzdGlobalContext } from '../lib/global'

export { getLogoImage }

const Box = styled.div``
const Title = styled.div`
margin-bottom: 10px;
`
export function NotImplementedMessage() {
return (
<Title>To store data locally please enable local mode in Settings</Title>
)
}

async function downloadUserDataFromMazedBackend(
localStorageApi: StorageApi,
datacenterStorageApi: StorageApi,
setLoadingState: (value: LoadingState) => void
): Promise<void> {
const oldToNewNids: Map<Nid, Nid> = new Map()
const iter = datacenterStorageApi.node.iterate()
let progressCounter = 0
// Clone all nodes, saving mapping between { old-nid → new-nid }
while (true) {
const node = await iter.next()
if (node == null) {
break
}
setLoadingState({
type: 'loading',
progress: `Downloading fragments (${++progressCounter})...`,
})
const url = node.extattrs?.web?.url ?? node.extattrs?.web_quote?.url
const origin = url != null ? genOriginId(url) : undefined
const r = await localStorageApi.node.create({
text: node.text,
index_text: node.index_text,
extattrs: node.extattrs,
ntype: node.ntype,
origin: origin ? { ...origin } : undefined,
created_at: node.created_at.unix(),
})
oldToNewNids.set(node.nid, r.nid)
}
// Clone all edges
progressCounter = 0
for (const oldNid of oldToNewNids.keys()) {
const edges = await datacenterStorageApi.edge.get({ nid: oldNid })
for (const oldEdge of edges.from_edges.concat(edges.to_edges)) {
setLoadingState({
type: 'loading',
progress: `Creating associations (${++progressCounter})...`,
})
const fromNid = oldToNewNids.get(oldEdge.from_nid)
const toNid = oldToNewNids.get(oldEdge.to_nid)
if (fromNid && toNid) {
await localStorageApi.edge.create({ from: fromNid, to: toNid })
}
}
}
setLoadingState({ type: 'done' })
}

const ControlBox = styled.div``
const BoxButtons = styled.div``
const Button = styled.button`
background-color: #ffffff;
border-style: solid;
border-width: 0;
border-radius: 32px;

vertical-align: middle;
&:hover {
background-color: #d0d1d2;
}
`

type LoadingState =
| { type: 'standby' }
| { type: 'loading'; progress: string }
| { type: 'done' }
export function DownloadUserDataFromMazedBackendControl() {
const [loadingState, setLoadingState] = React.useState<LoadingState>({
type: 'standby',
})
const ctx = React.useContext(MzdGlobalContext)
const sync = React.useCallback(() => {
setLoadingState({ type: 'loading', progress: '...' })
const datacenterStorageApi = makeDatacenterStorageApi()
downloadUserDataFromMazedBackend(
ctx.storage,
datacenterStorageApi,
setLoadingState
)
}, [ctx.storage])
let buttonText
switch (loadingState.type) {
case 'loading':
buttonText = (
<>
Loading <Spinner.Ring /> {loadingState.progress}
</>
)
break
case 'standby':
buttonText = <>Start</>
break
case 'done':
buttonText = <>Done 🏁 </>
break
}
return (
<ControlBox>
<Title>Download fragments from Mazed backend</Title>
<BoxButtons>
<Button onClick={sync} disabled={loadingState.type !== 'standby'}>
{buttonText}
</Button>
</BoxButtons>
</ControlBox>
)
}

export function DataCentreImporter({ className }: { className?: string }) {
const [storageType, setStorageType] = React.useState<
'loading' | truthsayer_archaeologist_communication.StorageType
>('loading')
useAsyncEffect(async () => {
const settings =
await truthsayer_archaeologist_communication.getAppSettings()
setStorageType(settings.storageType)
})
let element
switch (storageType) {
case 'loading':
element = <Spinner.Ring />
break
case 'browser_ext':
element = <DownloadUserDataFromMazedBackendControl />
break
case 'datacenter':
element = <NotImplementedMessage />
break
}
return <Box className={className}>{element}</Box>
}
5 changes: 5 additions & 0 deletions truthsayer/src/external-import/ExternalImport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
BrowserHistoryImporter,
BrowserLogo as BrowserHistoryImporterLogo,
} from './BrowserHistoryImporter'
import { DataCentreImporter, getLogoImage } from './DataCentreImporter'

const Box = styled.div`
padding: 18px;
Expand Down Expand Up @@ -65,6 +66,10 @@ export function ExternalImport({
<LogoImg src={MicrosoftOfficeOneDriveLogoImg} />
<MicrosoftOfficeOneDriveImporter />
</Item>
<Item key={'data-centre-importer'}>
<LogoImg src={getLogoImage()} />
<DataCentreImporter />
</Item>
</ItemsBox>
</Box>
)
Expand Down
17 changes: 3 additions & 14 deletions truthsayer/src/lib/global.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ import { NotificationToast } from './Toaster'
import { errorise, log, productanalytics } from 'armoury'
import { useAsyncEffect } from 'use-async-effect'
import {
defaultSettings,
FromTruthsayer,
ToTruthsayer,
getAppSettings,
} from 'truthsayer-archaeologist-communication'
import type { AppSettings } from 'truthsayer-archaeologist-communication'
import { Loader } from './loader'
Expand Down Expand Up @@ -106,18 +105,8 @@ export function MzdGlobal(props: React.PropsWithChildren<MzdGlobalProps>) {

const [storage, setStorage] = React.useState<StorageApi | null>(null)
useAsyncEffect(async () => {
const response = await FromTruthsayer.sendMessage({
type: 'GET_APP_SETTINGS_REQUEST',
}).catch((reason): ToTruthsayer.GetAppSettingsResponse => {
const defaults = defaultSettings()
log.warning(
`Failed to get user's app settings,` +
` falling back to ${JSON.stringify(defaults)}; ` +
`error - ${errorise(reason).message}`
)
return { type: 'GET_APP_SETTINGS_RESPONSE', settings: defaults }
})
setStorage(makeStorageApi(response.settings))
const settings = await getAppSettings()
setStorage(makeStorageApi(settings))
}, [])

const [state] = React.useState<Omit<MzdGlobalState, 'account' | 'storage'>>({
Expand Down