Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIE-21 Code Structure and Dependencies PoC #3834

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
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
21 changes: 21 additions & 0 deletions src/appDependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { analysisComponentDependencies } from 'src/dependencies/analysis/analysis-component-resolver'
import { getDefaultAnalysisComponentDeps } from 'src/dependencies/analysis/analysis-component-resolver.defaults'
import { analysisProviderDependencies } from 'src/dependencies/analysis/analysis-provider-resolver'
import { getDefaultAnalysisProviderDeps } from 'src/dependencies/analysis/analysis-provider-resolver.defaults'
import { analysisStateDependencies } from 'src/dependencies/analysis/analysis-state-resolver'
import { getDefaultAnalysisStateDeps } from 'src/dependencies/analysis/analysis-state-resolver.defaults'
import { componentDependencies } from 'src/dependencies/component-resolver'
import { getDefaultComponentDeps } from 'src/dependencies/component-resolver.defaults'
import { dataClientDependencies } from 'src/dependencies/data-client-resolver'
import { getDefaultDataClientDeps } from 'src/dependencies/data-client-resolver.defaults'


export const initAppDependencies = () => {
dataClientDependencies.set(getDefaultDataClientDeps)
componentDependencies.set(getDefaultComponentDeps)

// Analysis dependencies
analysisProviderDependencies.set(getDefaultAnalysisProviderDeps)
analysisStateDependencies.set(getDefaultAnalysisStateDeps)
analysisComponentDependencies.set(getDefaultAnalysisComponentDeps)
}
4 changes: 4 additions & 0 deletions src/appLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { startPollingServiceAlerts } from 'src/libs/service-alerts-polling'
import { initializeTCell } from 'src/libs/tcell'
import Main from 'src/pages/Main'

import { initAppDependencies } from './appDependencies'


initAppDependencies()

const appRoot = document.getElementById('root')

Expand Down
10 changes: 10 additions & 0 deletions src/dependencies/analysis/analysis-component-resolver.defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {
ExportAnalysisModal
} from 'src/pages/workspaces/workspace/analysis/modals/ExportAnalysisModal/ExportAnalysisModal.component'

import { AnalysisComponentDeps } from './analysis-component-resolver'


export const getDefaultAnalysisComponentDeps = (): AnalysisComponentDeps => ({
ExportAnalysisModal: ExportAnalysisModal.resolve()
})
10 changes: 10 additions & 0 deletions src/dependencies/analysis/analysis-component-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ComposedKey, createDependencyResolver } from '../dependency-core'


type ExportAnalysisModalComponentExports =
typeof import('src/pages/workspaces/workspace/analysis/modals/ExportAnalysisModal/ExportAnalysisModal.component')

export type AnalysisComponentDeps =
ComposedKey<ExportAnalysisModalComponentExports, 'ExportAnalysisModal'>

export const analysisComponentDependencies = createDependencyResolver<AnalysisComponentDeps>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AnalysisProviderDependencies } from 'src/dependencies/analysis/analysis-provider-resolver'
import { AnalysisProvider } from 'src/libs/ajax/analysis-providers/AnalysisProvider'


export const getDefaultAnalysisProviderDeps = (): AnalysisProviderDependencies => {
return ({
AnalysisProvider: AnalysisProvider.resolve()
})
}
9 changes: 9 additions & 0 deletions src/dependencies/analysis/analysis-provider-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ComposedKey, createDependencyResolver } from '../dependency-core'


type AnalysisProviderExports =
typeof import('src/libs/ajax/analysis-providers/AnalysisProvider')

export type AnalysisProviderDependencies = ComposedKey<AnalysisProviderExports, 'AnalysisProvider'>

export const analysisProviderDependencies = createDependencyResolver<AnalysisProviderDependencies>()
11 changes: 11 additions & 0 deletions src/dependencies/analysis/analysis-state-resolver.defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {
useAnalysisExportState,
} from 'src/pages/workspaces/workspace/analysis/modals/ExportAnalysisModal/ExportAnalysisModal.state'

import { AnalysisStateDependencies } from './analysis-state-resolver'


export const getDefaultAnalysisStateDeps = (): AnalysisStateDependencies => ({
useAnalysisExportState: useAnalysisExportState.resolve()
})

10 changes: 10 additions & 0 deletions src/dependencies/analysis/analysis-state-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ComposedKey, createDependencyResolver } from '../dependency-core'


type ExportAnalysisModalStateExports =
typeof import('src/pages/workspaces/workspace/analysis/modals/ExportAnalysisModal/ExportAnalysisModal.state')

export type AnalysisStateDependencies =
ComposedKey<ExportAnalysisModalStateExports, 'useAnalysisExportState'>

export const analysisStateDependencies = createDependencyResolver<AnalysisStateDependencies>()
8 changes: 8 additions & 0 deletions src/dependencies/component-resolver.defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ButtonPrimary, ButtonSecondary } from 'src/components/common'
import { ComponentDependencies } from 'src/dependencies/component-resolver'


export const getDefaultComponentDeps = (): ComponentDependencies => ({
ButtonPrimary,
ButtonSecondary
})
10 changes: 10 additions & 0 deletions src/dependencies/component-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createDependencyResolver, UnComposedKey } from 'src/dependencies/dependency-core'


type CommonComponentsExports = typeof import('src/components/common')

export type ComponentDependencies =
UnComposedKey<CommonComponentsExports, 'ButtonPrimary'> &
UnComposedKey<CommonComponentsExports, 'ButtonSecondary'>

export const componentDependencies = createDependencyResolver<ComponentDependencies>()
9 changes: 9 additions & 0 deletions src/dependencies/data-client-resolver.defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { DataClientDependencies } from 'src/dependencies/data-client-resolver'
import { AzureStorage } from 'src/libs/ajax/AzureStorage'
import { GoogleStorage } from 'src/libs/ajax/GoogleStorage'


export const getDefaultDataClientDeps = (): DataClientDependencies => ({
GoogleStorage,
AzureStorage
})
11 changes: 11 additions & 0 deletions src/dependencies/data-client-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createDependencyResolver, UnComposedKey } from 'src/dependencies/dependency-core'


type GoogleStorageExports = typeof import('src/libs/ajax/GoogleStorage')
type AzureStorageExports = typeof import('src/libs/ajax/AzureStorage')

export type DataClientDependencies =
UnComposedKey<GoogleStorageExports, 'GoogleStorage'> &
UnComposedKey<AzureStorageExports, 'AzureStorage'>

export const dataClientDependencies = createDependencyResolver<DataClientDependencies>()
80 changes: 80 additions & 0 deletions src/dependencies/dependency-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
export interface DependencyResolver<Deps> {
get: () => Deps
set: (getDeps: () => Deps) => void
override: (getOverrides: () => Partial<Deps>) => void
reset: () => void
}

/**
* Types a thing (typically the main export of a file) as requiring specific
* late-binding dependencies (Deps) and participating in the
* "Composable Dependency" DI-Container pattern.
* [wiki link TBD]
*/
export interface Composable<Deps, Output> {
compose: (deps: Deps) => Output
resolve: () => Output
}

/**
* Utility type for the resolved Output type of a Composable<<Deps, Output>> type
*/
export type Composed<C extends Composable<any, any>> = ReturnType<C['resolve']>

/**
* Utility type to safely specify an export by export name and have that be the
* key name of a dependency collection item. Key names must exist as an export
* of the same name. ComposedKey works with exports that satisfy the Composed
* type signature, and will enforce this requirement. The dependency is typed
* as the Output type of the Composed type signature. Dependency collections can be
* specified using this and UnComposedKey utility types as needed:
* @example
* type XxxxExports = typeof import('path/to/Xxxx')
* //...
* export type SomeDependencies =
* ComposedKey<XxxxExports, 'xName'> &
* ComposedKey<YyyyExports, 'yName'> &
* UncomposedKey<ZzzzExports, 'zName'>
*/
export type ComposedKey<Exports, K extends string & keyof Exports> = Exports[K] extends Composable<any, any>
? { [Property in K]: Composed<Exports[K]> } : never

/**
* Utility type to safely specify an export by export name and have that be the
* key name of a dependency collection item. Key names must exist as an export of the same name.
* The type signature of the named export will be the contract of the dependency.
* Dependency collections can be specified using this and ComposedKey utitlity types as needed:
* @example
* type XxxxExports = typeof import('path/to/Xxxx')
* //...
* export type SomeDependencies =
* ComposedKey<XxxxExports, 'xName'> &
* ComposedKey<YyyyExports, 'yName'> &
* UncomposedKey<ZzzzExports, 'zName'>
*/
export type UnComposedKey<Exports, K extends string & keyof Exports> =
{ [Property in K]: Exports[K] }


export const createDependencyResolver = <Deps>(): DependencyResolver<Deps> => {
const getNullDeps = (): Deps => {
throw Error('Dependency Resolver not initialized.')
}
let getCurrentDeps: () => Deps = getNullDeps
const resolver: DependencyResolver<Deps> = {
get: () => getCurrentDeps(),
set: (getDeps: () => Deps): void => {
getCurrentDeps = getDeps
},
override: (getOverrides: () => Partial<Deps>): void => {
getCurrentDeps = () => ({
...getCurrentDeps(),
...getOverrides()
})
},
reset: () => {
getCurrentDeps = getNullDeps
}
}
return resolver
}
24 changes: 13 additions & 11 deletions src/libs/ajax.js → src/libs/ajax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { getUser } from 'src/libs/state'
import * as Utils from 'src/libs/utils'


window.ajaxOverrideUtils = {
(window as any).ajaxOverrideUtils = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal conversion of ajax.js to .ts is not central to the DI patterns. I did this just to avoid type anoyances/polution of the core code bits elsewhere.

mapJsonBody: _.curry((fn, wrappedFetch) => async (...args) => {
const res = await wrappedFetch(...args)
return new Response(JSON.stringify(fn(await res.json())), res)
Expand Down Expand Up @@ -107,7 +107,7 @@ const User = signal => ({
_.mergeAll([authOpts(), { signal, method: 'POST' }, jsonBody('app.terra.bio/#terms-of-service')])
)
return response.json()
} catch (error) {
} catch (error: any) {
if (error.status !== 404) {
throw error
}
Expand All @@ -118,7 +118,7 @@ const User = signal => ({
try {
const res = await(fetchSam('register/user/v2/self/termsOfServiceDetails', _.merge(authOpts(), { signal })))
return res.json()
} catch (error) {
} catch (error: any) {
if (error.status === 404 || error.status === 403) {
return null
} else {
Expand Down Expand Up @@ -192,7 +192,7 @@ const User = signal => ({
try {
const res = await fetchOrchestration('api/nih/status', _.merge(authOpts(), { signal }))
return res.json()
} catch (error) {
} catch (error: any) {
if (error.status === 404) {
return {}
} else {
Expand All @@ -214,7 +214,7 @@ const User = signal => ({
try {
const res = await fetchBond(`api/link/v1/${provider}`, _.merge(authOpts(), { signal }))
return res.json()
} catch (error) {
} catch (error: any) {
if (error.status === 404) {
return {}
} else {
Expand Down Expand Up @@ -259,7 +259,7 @@ const User = signal => ({
try {
const res = await fetchEcm(root, _.merge(authOpts(), { signal }))
return res.json()
} catch (error) {
} catch (error: any) {
if (error.status === 404) {
return null
} else {
Expand Down Expand Up @@ -292,7 +292,7 @@ const User = signal => ({
isUserRegistered: async email => {
try {
await fetchSam(`api/users/v1/${encodeURIComponent(email)}`, _.merge(authOpts(), { signal, method: 'GET' }))
} catch (error) {
} catch (error: any) {
if (error.status === 404) {
return false
} else {
Expand Down Expand Up @@ -436,7 +436,7 @@ const Workspaces = signal => ({
},

getTags: async (tag, limit) => {
const params = { q: tag }
const params: any = { q: tag }
if (limit) {
params.limit = limit
}
Expand Down Expand Up @@ -922,7 +922,7 @@ const Methods = signal => ({
return res.json()
},

toWorkspace: async (workspace, config = {}) => {
toWorkspace: async (workspace, config: any = {}) => {
const res = await fetchRawls(`workspaces/${workspace.namespace}/${workspace.name}/methodconfigs`,
_.mergeAll([authOpts(), jsonBody(_.merge({
methodRepoMethod: {
Expand Down Expand Up @@ -1013,7 +1013,7 @@ const Surveys = signal => ({
submitForm: (formId, data) => fetchGoogleForms(`${formId}/formResponse?${qs.stringify(data)}`, { signal })
})

export const Ajax = signal => {
export const Ajax = (signal?: AbortSignal) => {
return {
User: User(signal),
Groups: Groups(signal),
Expand Down Expand Up @@ -1042,8 +1042,10 @@ export const Ajax = signal => {
}
}

export type AjaxContract = ReturnType<typeof Ajax>

// Exposing Ajax for use by integration tests (and debugging, or whatever)
window.Ajax = Ajax
(window as any).Ajax = Ajax

// Experimental: Pulling Ajax from context allows replacing for usage outside of Terra UI.
// https://github.com/DataBiosphere/terra-ui/pull/3669
Expand Down
62 changes: 62 additions & 0 deletions src/libs/ajax/analysis-providers/AnalysisProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { DataClientDependencies, dataClientDependencies } from 'src/dependencies/data-client-resolver'
import { Composable } from 'src/dependencies/dependency-core'
import { GoogleWorkspaceInfo, isGoogleWorkspaceInfo, WorkspaceInfo } from 'src/libs/workspace-utils'
import { AnalysisFile, getExtension, stripExtension } from 'src/pages/workspaces/workspace/analysis/file-utils'
import { ToolLabel } from 'src/pages/workspaces/workspace/analysis/tool-utils'


export type AnalysisProviderDeps = {
dataClients: Pick<DataClientDependencies, 'AzureStorage' | 'GoogleStorage'>
}

export interface AnalysisProviderContract {
listAnalyses: (workspaceInfo: WorkspaceInfo, signal?: AbortSignal) => Promise<AnalysisFile[]>
copyAnalysis: (
sourceWorkspace: WorkspaceInfo,
printName: string,
toolLabel: ToolLabel,
targetWorkspace: WorkspaceInfo,
newName: string,
signal?: AbortSignal
) => Promise<void>
}

export type ComposableAnalysisProvider = Composable<AnalysisProviderDeps, AnalysisProviderContract>

export const AnalysisProvider: ComposableAnalysisProvider = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this provider is implemented in a pending PR for IA team, shown here to have an more robust example of the proposed UI stack.

compose: (deps: AnalysisProviderDeps) => {
const { AzureStorage, GoogleStorage } = deps.dataClients

const provider: AnalysisProviderContract = {
listAnalyses: async (workspaceInfo: WorkspaceInfo, signal?: AbortSignal): Promise<AnalysisFile[]> => {
const selectedAnalyses: AnalysisFile[] =
isGoogleWorkspaceInfo(workspaceInfo) ?
await GoogleStorage(signal).listAnalyses(workspaceInfo.googleProject, workspaceInfo.bucketName) :
// TODO: cleanup once TS is merged in for AzureStorage module
(await AzureStorage(signal).listNotebooks(workspaceInfo.workspaceId) as any)
return selectedAnalyses
},
copyAnalysis: async (
sourceWorkspace: WorkspaceInfo,
printName: string,
toolLabel: ToolLabel,
targetWorkspace: WorkspaceInfo,
newName: string,
signal?: AbortSignal
): Promise<void> => {
if (isGoogleWorkspaceInfo(sourceWorkspace)) {
await GoogleStorage()
.analysis(sourceWorkspace.googleProject, sourceWorkspace.bucketName, printName, toolLabel)
// assumes GCP to GCP copy
.copy(`${newName}.${getExtension(printName)}`, (targetWorkspace as GoogleWorkspaceInfo).bucketName, false)
} else {
await AzureStorage(signal)
.blob(sourceWorkspace.workspaceId, printName)
.copy(stripExtension(newName), targetWorkspace.workspaceId)
}
}
}
return provider
},
resolve: () => AnalysisProvider.compose({ dataClients: dataClientDependencies.get() })
}
Loading