import dayjs from 'dayjs' import { getOrElse } from 'fp-ts/lib/Either' import { pipe } from 'fp-ts/lib/function' import { some } from 'lodash' import { clone, get, set } from 'lodash-es' import { Context } from '.' import { showConfirm } from '../utils/dialog' import { ApplicationDialogState, ApplicationsByIdCodec, AppState, createApplicationConfig, createApplicationDialogState, createDeployWorkflowSettings, DeploymentDialogState, DeployWorkflowCodec, DeployWorkflowSettings, EnvironmentDialogState, EnvironmentSettings, RepoModel, } from './state' import { getDeploymentId } from './utils' export const setToken = ({ state }: Context, token: string) => { state.token = token } export const showSettings = ({ state }: Context) => (state.settingsDialog = {}) export const hideSettings = ({ state }: Context) => delete state.settingsDialog export const setState = <T>( { state }: Context, { selector, value }: { selector: (state: AppState) => T; value: T } ) => { const path = selector.toString().replace(/^.*?\./, '') if (get(state, path) === undefined) { throw Error('Unkown path ' + path) } set(state, path, value) } export const showNewApplicationModal = ({ state }: Context) => { state.newApplicationDialog = createApplicationDialogState() if (state.selectedApplication) { state.newApplicationDialog.repo = clone(state.selectedApplication.repo) } } export const updateWorkflowSettings = ( { state: { selectedApplication } }: Context, update: (settings: DeployWorkflowSettings) => void ) => { if (selectedApplication) { let deploySettings = selectedApplication.deploySettings if (DeployWorkflowCodec.is(deploySettings)) { update(deploySettings) } else { deploySettings = createDeployWorkflowSettings({ ref: selectedApplication.repo.defaultBranch, }) update(deploySettings) } } } export const updateDeployWorkflowDialog = ( { state: { deploymentDialog } }: Context, update: (state: DeploymentDialogState) => void ) => { if (deploymentDialog) { update(deploymentDialog) } } export const triggerDeployment = async ( { effects, state }: Context, { release, environmentName, }: { release: string environmentName: string } ) => { const { selectedApplication } = state if (!selectedApplication) return const { deploySettings, environmentSettingsByName } = selectedApplication if (!DeployWorkflowCodec.is(deploySettings)) return if (!(environmentName in environmentSettingsByName)) return const environmentSettings = environmentSettingsByName[environmentName] const { repo } = selectedApplication if ( await showConfirm( `Are you sure you want to deploy "${release}" to "${environmentSettings.name}" in "${repo.owner}/${repo.name}@${deploySettings.ref}"?` ) ) { const deploymentId = getDeploymentId({ release, environment: environmentName, owner: repo.owner, repo: repo.name, }) state.pendingDeployments[deploymentId] = dayjs() const { owner, name } = repo const { ref, workflowId, environmentKey, releaseKey, extraArgs } = deploySettings const environmentArg = environmentSettings.workflowInputValue || environmentSettings.name const inputs = environmentKey ? { [releaseKey]: release, [environmentKey]: environmentArg, ...extraArgs, } : { [releaseKey]: release, ...extraArgs, } await effects.restApi.octokit.actions.createWorkflowDispatch({ owner, repo: name, ref, workflow_id: workflowId, inputs, }) } } export const createNewApplication = ( { state, actions }: Context, { repo, name, releaseFilter, }: { repo: RepoModel name: string releaseFilter: string } ) => { if (!state.newApplicationDialog) return if ( Object.values(state.applicationsById).some( (app) => app.repo.id === repo.id && app.name === name ) ) { state.newApplicationDialog.warning = 'App with same name and repo already exists!' return } const appConfig = createApplicationConfig(repo, name, releaseFilter) state.applicationsById[appConfig.id] = appConfig state.selectedApplicationId = appConfig.id delete state.newApplicationDialog actions.editDeployment() } export const cancelNewApplication = ({ state }: Context) => { delete state.newApplicationDialog } export const selectApplication = ({ state }: Context, id: string) => { state.selectedApplicationId = id } export const editApplication = ({ state }: Context) => { state.editApplicationDialog = createApplicationDialogState() if (state.selectedApplication) { state.editApplicationDialog.repo = clone(state.selectedApplication.repo) state.editApplicationDialog.name = state.selectedApplication.name state.editApplicationDialog.releaseFilter = state.selectedApplication.releaseFilter } } export const editDeployment = ({ state }: Context) => { const deploySettings = state.selectedApplication?.deploySettings if (DeployWorkflowCodec.is(deploySettings)) state.deploymentDialog = clone(deploySettings) } export const saveDeployment = ({ state }: Context) => { if (state.selectedApplication && state.deploymentDialog) { state.selectedApplication.deploySettings = clone(state.deploymentDialog) } delete state.deploymentDialog } export const cancelEditDeployment = ({ state }: Context) => { delete state.deploymentDialog } export const cancelEditApplication = ({ state }: Context) => { delete state.editApplicationDialog } export const saveApplication = ( { state }: Context, { repo, name, releaseFilter, }: { repo: RepoModel name: string releaseFilter: string } ) => { if (!state.editApplicationDialog) return const id = state.selectedApplicationId if ( some( state.applicationsById, (app) => app.id !== id && app.repo.id === repo.id && app.name === name ) ) { state.editApplicationDialog.warning = 'App with same name and repo already exists!' return } state.applicationsById[id].repo = clone(repo) state.applicationsById[id].name = name state.applicationsById[id].releaseFilter = releaseFilter delete state.editApplicationDialog } export const updateApplicationDialog = ( { state }: Context, { newOrEdit, update, }: { newOrEdit: 'new' | 'edit' update: (state: ApplicationDialogState) => void } ) => { const dialogState = newOrEdit === 'new' ? state.newApplicationDialog : state.editApplicationDialog if (dialogState) { dialogState.warning = undefined update(dialogState) } } export const deleteApplication = async ({ state }: Context) => { if ( !!state.selectedApplication && (await showConfirm( 'Are you sure you want to delete ' + state.selectedApplication.name + '?' )) ) { delete state.applicationsById[state.selectedApplicationId] delete state.editApplicationDialog } } export const showAddEnvironmentModal = ({ state }: Context) => { state.addEnvironmentDialog = { environmentName: '', workflowInputValue: '', } } export const updateEnvironmentDialog = ( { state }: Context, { addOrEdit, update, }: { addOrEdit: 'add' | 'edit' update: (state: EnvironmentDialogState) => void } ) => { const dialogState = addOrEdit === 'add' ? state.addEnvironmentDialog : state.editEnvironmentDialog if (dialogState) { update(dialogState) } } export const cancelAddEnvironment = ({ state }: Context) => { delete state.addEnvironmentDialog } export const addEnvironment = ( { state }: Context, settings: EnvironmentSettings ) => { if ( state.selectedApplication && state.addEnvironmentDialog?.environmentName ) { state.selectedApplication.environmentSettingsByName[ state.addEnvironmentDialog.environmentName ] = settings } delete state.addEnvironmentDialog } export const removeEnvironment = async ({ state }: Context, name: string) => { if ( state.selectedApplication && (await showConfirm( `Are you sure you want to delete ${state.selectedApplication.environmentSettingsByName[name].name}?` )) ) { delete state.selectedApplication.environmentSettingsByName[name] } } export const exportApplications = async ({ state, effects }: Context) => { await effects.downloadJson(state.applicationsById, 'gdc-applications.json') } export const importApplications = async ({ state, effects }: Context) => { const json = await effects.uploadJson() if (json) { const imported = JSON.parse(json) const applications = pipe( ApplicationsByIdCodec.decode(imported), getOrElse((e) => { console.error(e) return {} }) ) state.applicationsById = { ...state.applicationsById, ...applications, } } } export { onInitializeOvermind } from './onInitialize'