chore: add option to transition your data - internal feature (#2449)
* chore: add option to transition your data - internal feature * chore: fix spec typo
This commit is contained in:
@@ -13,6 +13,8 @@ import {
|
|||||||
MfaServiceInterface,
|
MfaServiceInterface,
|
||||||
GenerateUuid,
|
GenerateUuid,
|
||||||
CreateDecryptedBackupFile,
|
CreateDecryptedBackupFile,
|
||||||
|
GetTransitionStatus,
|
||||||
|
StartTransition,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { VaultLockServiceInterface } from './../VaultLock/VaultLockServiceInterface'
|
import { VaultLockServiceInterface } from './../VaultLock/VaultLockServiceInterface'
|
||||||
import { HistoryServiceInterface } from './../History/HistoryServiceInterface'
|
import { HistoryServiceInterface } from './../History/HistoryServiceInterface'
|
||||||
@@ -76,6 +78,8 @@ export interface ApplicationInterface {
|
|||||||
get generateUuid(): GenerateUuid
|
get generateUuid(): GenerateUuid
|
||||||
get getHost(): GetHost
|
get getHost(): GetHost
|
||||||
get setHost(): SetHost
|
get setHost(): SetHost
|
||||||
|
get getTransitionStatus(): GetTransitionStatus
|
||||||
|
get startTransition(): StartTransition
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
get alerts(): AlertService
|
get alerts(): AlertService
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum InternalFeature {
|
export enum InternalFeature {
|
||||||
Vaults = 'vaults',
|
Vaults = 'vaults',
|
||||||
HomeServer = 'home-server',
|
HomeServer = 'home-server',
|
||||||
|
Transition = 'transition',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { HttpServiceInterface } from '@standardnotes/api'
|
||||||
|
|
||||||
|
import { GetTransitionStatus } from './GetTransitionStatus'
|
||||||
|
|
||||||
|
describe('GetTransitionStatus', () => {
|
||||||
|
let httpService: HttpServiceInterface
|
||||||
|
|
||||||
|
const createUseCase = () => new GetTransitionStatus(httpService)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
httpService = {
|
||||||
|
get: jest.fn(),
|
||||||
|
} as unknown as HttpServiceInterface
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get transition status', async () => {
|
||||||
|
const useCase = createUseCase()
|
||||||
|
;(httpService.get as jest.Mock).mockResolvedValueOnce({ status: 200, data: { status: 'TO-DO' } })
|
||||||
|
|
||||||
|
const result = await useCase.execute()
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(false)
|
||||||
|
expect(result.getValue()).toBe('TO-DO')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail to get transition status', async () => {
|
||||||
|
const useCase = createUseCase()
|
||||||
|
;(httpService.get as jest.Mock).mockResolvedValueOnce({ status: 400 })
|
||||||
|
|
||||||
|
const result = await useCase.execute()
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
import { HttpServiceInterface } from '@standardnotes/api'
|
||||||
|
import { HttpStatusCode } from '@standardnotes/responses'
|
||||||
|
|
||||||
|
export class GetTransitionStatus implements UseCaseInterface<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'> {
|
||||||
|
constructor(private httpService: HttpServiceInterface) {}
|
||||||
|
|
||||||
|
async execute(): Promise<Result<'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED'>> {
|
||||||
|
const response = await this.httpService.get('/v1/users/transition-status')
|
||||||
|
|
||||||
|
if (response.status !== HttpStatusCode.Success) {
|
||||||
|
return Result.fail('Failed to get transition status')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok((response.data as { status: 'TO-DO' | 'STARTED' | 'FINISHED' | 'FAILED' }).status)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { HttpServiceInterface } from '@standardnotes/api'
|
||||||
|
|
||||||
|
import { StartTransition } from './StartTransition'
|
||||||
|
|
||||||
|
describe('StartTransition', () => {
|
||||||
|
let httpService: HttpServiceInterface
|
||||||
|
|
||||||
|
const createUseCase = () => new StartTransition(httpService)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
httpService = {
|
||||||
|
post: jest.fn(),
|
||||||
|
} as unknown as HttpServiceInterface
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should start transition', async () => {
|
||||||
|
const useCase = createUseCase()
|
||||||
|
;(httpService.post as jest.Mock).mockResolvedValueOnce({ status: 200 })
|
||||||
|
|
||||||
|
const result = await useCase.execute()
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail to start transition', async () => {
|
||||||
|
const useCase = createUseCase()
|
||||||
|
;(httpService.post as jest.Mock).mockResolvedValueOnce({ status: 400 })
|
||||||
|
|
||||||
|
const result = await useCase.execute()
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { HttpServiceInterface } from '@standardnotes/api'
|
||||||
|
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
import { HttpStatusCode } from '@standardnotes/responses'
|
||||||
|
|
||||||
|
export class StartTransition implements UseCaseInterface<void> {
|
||||||
|
constructor(private httpService: HttpServiceInterface) {}
|
||||||
|
|
||||||
|
async execute(): Promise<Result<void>> {
|
||||||
|
const response = await this.httpService.post('/v1/items/transition')
|
||||||
|
|
||||||
|
if (response.status !== HttpStatusCode.Success) {
|
||||||
|
return Result.fail('Failed to start transition')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -184,6 +184,8 @@ export * from './Sync/SyncOptions'
|
|||||||
export * from './Sync/SyncQueueStrategy'
|
export * from './Sync/SyncQueueStrategy'
|
||||||
export * from './Sync/SyncServiceInterface'
|
export * from './Sync/SyncServiceInterface'
|
||||||
export * from './Sync/SyncSource'
|
export * from './Sync/SyncSource'
|
||||||
|
export * from './UseCase/Transition/GetTransitionStatus/GetTransitionStatus'
|
||||||
|
export * from './UseCase/Transition/StartTransition/StartTransition'
|
||||||
export * from './UseCase/ChangeAndSaveItem'
|
export * from './UseCase/ChangeAndSaveItem'
|
||||||
export * from './UseCase/DiscardItemsLocally'
|
export * from './UseCase/DiscardItemsLocally'
|
||||||
export * from './UseCase/GenerateUuid'
|
export * from './UseCase/GenerateUuid'
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ import {
|
|||||||
GenerateUuid,
|
GenerateUuid,
|
||||||
CreateDecryptedBackupFile,
|
CreateDecryptedBackupFile,
|
||||||
CreateEncryptedBackupFile,
|
CreateEncryptedBackupFile,
|
||||||
|
GetTransitionStatus,
|
||||||
|
StartTransition,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import {
|
import {
|
||||||
SNNote,
|
SNNote,
|
||||||
@@ -1138,6 +1140,14 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
return this.dependencies.get<SetHost>(TYPES.SetHost)
|
return this.dependencies.get<SetHost>(TYPES.SetHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get getTransitionStatus(): GetTransitionStatus {
|
||||||
|
return this.dependencies.get<GetTransitionStatus>(TYPES.GetTransitionStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
get startTransition(): StartTransition {
|
||||||
|
return this.dependencies.get<StartTransition>(TYPES.StartTransition)
|
||||||
|
}
|
||||||
|
|
||||||
public get legacyApi(): LegacyApiService {
|
public get legacyApi(): LegacyApiService {
|
||||||
return this.dependencies.get<LegacyApiService>(TYPES.LegacyApiService)
|
return this.dependencies.get<LegacyApiService>(TYPES.LegacyApiService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ import {
|
|||||||
CreateDecryptedBackupFile,
|
CreateDecryptedBackupFile,
|
||||||
CreateEncryptedBackupFile,
|
CreateEncryptedBackupFile,
|
||||||
SyncLocalVaultsWithRemoteSharedVaults,
|
SyncLocalVaultsWithRemoteSharedVaults,
|
||||||
|
GetTransitionStatus,
|
||||||
|
StartTransition,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { ItemManager } from '../../Services/Items/ItemManager'
|
import { ItemManager } from '../../Services/Items/ItemManager'
|
||||||
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
||||||
@@ -153,6 +155,7 @@ import {
|
|||||||
AuthenticatorApiService,
|
AuthenticatorApiService,
|
||||||
AuthenticatorServer,
|
AuthenticatorServer,
|
||||||
HttpService,
|
HttpService,
|
||||||
|
HttpServiceInterface,
|
||||||
RevisionApiService,
|
RevisionApiService,
|
||||||
RevisionServer,
|
RevisionServer,
|
||||||
SharedVaultInvitesServer,
|
SharedVaultInvitesServer,
|
||||||
@@ -1023,6 +1026,14 @@ export class Dependencies {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.factory.set(TYPES.GetTransitionStatus, () => {
|
||||||
|
return new GetTransitionStatus(this.get<HttpServiceInterface>(TYPES.HttpService))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.factory.set(TYPES.StartTransition, () => {
|
||||||
|
return new StartTransition(this.get<HttpServiceInterface>(TYPES.HttpService))
|
||||||
|
})
|
||||||
|
|
||||||
this.factory.set(TYPES.ListRevisions, () => {
|
this.factory.set(TYPES.ListRevisions, () => {
|
||||||
return new ListRevisions(this.get<RevisionManager>(TYPES.RevisionManager))
|
return new ListRevisions(this.get<RevisionManager>(TYPES.RevisionManager))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -171,6 +171,8 @@ export const TYPES = {
|
|||||||
AuthorizeVaultDeletion: Symbol.for('AuthorizeVaultDeletion'),
|
AuthorizeVaultDeletion: Symbol.for('AuthorizeVaultDeletion'),
|
||||||
CreateDecryptedBackupFile: Symbol.for('CreateDecryptedBackupFile'),
|
CreateDecryptedBackupFile: Symbol.for('CreateDecryptedBackupFile'),
|
||||||
CreateEncryptedBackupFile: Symbol.for('CreateEncryptedBackupFile'),
|
CreateEncryptedBackupFile: Symbol.for('CreateEncryptedBackupFile'),
|
||||||
|
GetTransitionStatus: Symbol.for('GetTransitionStatus'),
|
||||||
|
StartTransition: Symbol.for('StartTransition'),
|
||||||
|
|
||||||
// Mappers
|
// Mappers
|
||||||
SessionStorageMapper: Symbol.for('SessionStorageMapper'),
|
SessionStorageMapper: Symbol.for('SessionStorageMapper'),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export class DevMode {
|
|||||||
constructor(private application: WebApplicationInterface) {
|
constructor(private application: WebApplicationInterface) {
|
||||||
InternalFeatureService.get().enableFeature(InternalFeature.Vaults)
|
InternalFeatureService.get().enableFeature(InternalFeature.Vaults)
|
||||||
InternalFeatureService.get().enableFeature(InternalFeature.HomeServer)
|
InternalFeatureService.get().enableFeature(InternalFeature.HomeServer)
|
||||||
|
InternalFeatureService.get().enableFeature(InternalFeature.Transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Valid only when running a mock event publisher on port 3124 */
|
/** Valid only when running a mock event publisher on port 3124 */
|
||||||
|
|||||||
@@ -1,22 +1,91 @@
|
|||||||
|
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
|
||||||
import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import { SyncQueueStrategy } from '@standardnotes/snjs'
|
import { SyncQueueStrategy } from '@standardnotes/snjs'
|
||||||
import { STRING_GENERIC_SYNC_ERROR } from '@/Constants/Strings'
|
import { STRING_GENERIC_SYNC_ERROR } from '@/Constants/Strings'
|
||||||
import { observer } from 'mobx-react-lite'
|
|
||||||
import { WebApplication } from '@/Application/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import { FunctionComponent, useState } from 'react'
|
|
||||||
import { formatLastSyncDate } from '@/Utils/DateUtils'
|
import { formatLastSyncDate } from '@/Utils/DateUtils'
|
||||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||||
|
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||||
|
import { featureTrunkTransitionEnabled } from '@/FeatureTrunk'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sync: FunctionComponent<Props> = ({ application }: Props) => {
|
const Sync: FunctionComponent<Props> = ({ application }: Props) => {
|
||||||
|
const TRANSITION_STATUS_REFRESH_INTERVAL = 5000
|
||||||
|
|
||||||
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
|
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
|
||||||
|
const [isTransitionInProgress, setIsTransitionInProgress] = useState(false)
|
||||||
|
const [showTransitionSegment, setShowTransitionSegment] = useState(false)
|
||||||
|
const [transitionStatus, setTransitionStatus] = useState('')
|
||||||
|
const [transitionStatusIntervalRef, setTransitionStatusIntervalRef] = useState<NodeJS.Timer | null>(null)
|
||||||
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
|
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
|
||||||
|
|
||||||
|
const setupTransitionStatusRefresh = useCallback(async () => {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
const statusOrError = await application.getTransitionStatus.execute()
|
||||||
|
if (statusOrError.isFailed()) {
|
||||||
|
await application.alerts.alert(statusOrError.getError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const status = statusOrError.getValue()
|
||||||
|
|
||||||
|
setTransitionStatus(status)
|
||||||
|
}, TRANSITION_STATUS_REFRESH_INTERVAL)
|
||||||
|
|
||||||
|
setTransitionStatusIntervalRef(interval)
|
||||||
|
}, [application, setTransitionStatus, setTransitionStatusIntervalRef])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!featureTrunkTransitionEnabled()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkTransitionStatus() {
|
||||||
|
const statusOrError = await application.getTransitionStatus.execute()
|
||||||
|
if (statusOrError.isFailed()) {
|
||||||
|
await application.alerts.alert(statusOrError.getError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const status = statusOrError.getValue()
|
||||||
|
|
||||||
|
if (status === 'FINISHED') {
|
||||||
|
if (transitionStatusIntervalRef) {
|
||||||
|
clearInterval(transitionStatusIntervalRef)
|
||||||
|
}
|
||||||
|
setIsTransitionInProgress(false)
|
||||||
|
setTransitionStatus(status)
|
||||||
|
setShowTransitionSegment(false)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowTransitionSegment(true)
|
||||||
|
setTransitionStatus(status)
|
||||||
|
|
||||||
|
if (status === 'STARTED') {
|
||||||
|
setIsTransitionInProgress(true)
|
||||||
|
if (!transitionStatusIntervalRef) {
|
||||||
|
await setupTransitionStatusRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkTransitionStatus()
|
||||||
|
}, [
|
||||||
|
application,
|
||||||
|
setIsTransitionInProgress,
|
||||||
|
setTransitionStatus,
|
||||||
|
setShowTransitionSegment,
|
||||||
|
setupTransitionStatusRefresh,
|
||||||
|
transitionStatusIntervalRef,
|
||||||
|
])
|
||||||
|
|
||||||
const doSynchronization = async () => {
|
const doSynchronization = async () => {
|
||||||
setIsSyncingInProgress(true)
|
setIsSyncingInProgress(true)
|
||||||
|
|
||||||
@@ -32,6 +101,19 @@ const Sync: FunctionComponent<Props> = ({ application }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const doTransition = useCallback(async () => {
|
||||||
|
const resultOrError = await application.startTransition.execute()
|
||||||
|
if (resultOrError.isFailed()) {
|
||||||
|
await application.alerts.alert(resultOrError.getError())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTransitionInProgress(true)
|
||||||
|
|
||||||
|
await setupTransitionStatusRefresh()
|
||||||
|
}, [application, setupTransitionStatusRefresh])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
@@ -50,6 +132,35 @@ const Sync: FunctionComponent<Props> = ({ application }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
|
{showTransitionSegment && (
|
||||||
|
<>
|
||||||
|
<HorizontalSeparator classes="my-4" />
|
||||||
|
<PreferencesSegment>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex flex-grow flex-col">
|
||||||
|
<Title>Transition Account</Title>
|
||||||
|
<Text>
|
||||||
|
Transition your account to our new infrastructure in order to enable new features and improve your
|
||||||
|
overall experience. Depending on the amount of data you have, this process may take a few moments.
|
||||||
|
</Text>
|
||||||
|
{isTransitionInProgress && (
|
||||||
|
<Text>
|
||||||
|
<span className="font-bold">Transition status:</span> {transitionStatus}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!isTransitionInProgress && (
|
||||||
|
<Button
|
||||||
|
className="mt-3 min-w-20"
|
||||||
|
label="Start transition"
|
||||||
|
disabled={isTransitionInProgress}
|
||||||
|
onClick={doTransition}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PreferencesSegment>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ export function featureTrunkVaultsEnabled(): boolean {
|
|||||||
export function featureTrunkHomeServerEnabled(): boolean {
|
export function featureTrunkHomeServerEnabled(): boolean {
|
||||||
return InternalFeatureService.get().isFeatureEnabled(InternalFeature.HomeServer)
|
return InternalFeatureService.get().isFeatureEnabled(InternalFeature.HomeServer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function featureTrunkTransitionEnabled(): boolean {
|
||||||
|
return InternalFeatureService.get().isFeatureEnabled(InternalFeature.Transition)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user