iterative/vscode-dvc

View on GitHub
extension/src/experiments/workspace.ts

Summary

Maintainability
C
1 day
Test Coverage
B
82%
import { EventEmitter } from 'vscode'
import isEmpty from 'lodash.isempty'
import { Experiments } from '.'
import {
  getBranchExperimentCommand,
  getPushExperimentCommand
} from './commands'
import { TableData } from './webview/contract'
import { Args } from '../cli/dvc/constants'
import { AvailableCommands, CommandId } from '../commands/internal'
import { ResourceLocator } from '../resourceLocator'
import { Setup } from '../setup'
import { Toast } from '../vscode/toast'
import { getInput, getPositiveIntegerInput } from '../vscode/inputBox'
import { BaseWorkspaceWebviews } from '../webview/workspace'
import { Title } from '../vscode/title'
import { quickPickManyValues } from '../vscode/quickPick'
import { WorkspacePipeline } from '../pipeline/workspace'

export class WorkspaceExperiments extends BaseWorkspaceWebviews<
  Experiments,
  TableData
> {
  public readonly experimentsChanged = this.dispose.track(
    new EventEmitter<void>()
  )

  public readonly onDidChangeExperiments = this.experimentsChanged.event

  public readonly columnsChanged = this.dispose.track(new EventEmitter<void>())
  public readonly columnsOrderOrStatusChanged = this.dispose.track(
    new EventEmitter<void>()
  )

  private focusedFileDvcRoot: string | undefined

  public addFilter(overrideRoot?: string) {
    return this.getRepositoryThenUpdate('addFilter', overrideRoot)
  }

  public addStarredFilter(overrideRoot?: string) {
    return this.getRepositoryThenUpdate('addStarredFilter', overrideRoot)
  }

  public removeFilters() {
    return this.getRepositoryThenUpdate('removeFilters')
  }

  public addSort(overrideRoot?: string) {
    return this.getRepositoryThenUpdate('addSort', overrideRoot)
  }

  public addStarredSort(overrideRoot?: string) {
    return this.getRepositoryThenUpdate('addStarredSort', overrideRoot)
  }

  public removeSorts() {
    return this.getRepositoryThenUpdate('removeSorts')
  }

  public selectExperimentsToPlot(overrideRoot?: string) {
    return this.getRepositoryThenUpdate('selectExperimentsToPlot', overrideRoot)
  }

  public selectColumns(overrideRoot?: string) {
    return this.getRepositoryThenUpdate('selectColumns', overrideRoot)
  }

  public selectFirstColumns(overrideRoot?: string) {
    return this.getRepositoryThenUpdate('selectFirstColumns', overrideRoot)
  }

  public async selectExperimentsToStop() {
    const cwd = await this.getFocusedOrOnlyOrPickProject()
    if (!cwd) {
      return
    }

    const ids = await this.getRepository(cwd).pickRunningExperiments()

    if (!ids || isEmpty(ids)) {
      return
    }
    return this.stopExperiments(cwd, ...ids)
  }

  public stopExperiments(dvcRoot: string, ...ids: string[]) {
    return this.getRepository(dvcRoot).stopExperiments(ids)
  }

  public async selectExperimentsToPush(setup: Setup) {
    const dvcRoot = await this.getFocusedOrOnlyOrPickProject()
    if (!dvcRoot) {
      return
    }

    const ids = await this.getRepository(dvcRoot).pickExperimentsToPush()
    if (!ids || isEmpty(ids)) {
      return
    }

    const pushCommand = getPushExperimentCommand(
      this,
      this.internalCommands,
      setup
    )

    return pushCommand({ dvcRoot, ids })
  }

  public async selectExperimentsToRemove() {
    const cwd = await this.getFocusedOrOnlyOrPickProject()
    if (!cwd) {
      return
    }

    const ids = await this.getRepository(cwd).pickExperimentsToRemove()

    if (!ids || isEmpty(ids)) {
      return
    }
    return this.runCommand(AvailableCommands.EXP_REMOVE, cwd, ...ids)
  }

  public async modifyWorkspaceParamsAndRun(overrideRoot?: string) {
    const project = await this.getDvcRoot(overrideRoot)
    if (!project) {
      return
    }
    const repository = this.getRepository(project)
    if (!repository) {
      return
    }

    return await repository.modifyWorkspaceParamsAndRun()
  }

  public async modifyWorkspaceParamsAndQueue(overrideRoot?: string) {
    const project = await this.getDvcRoot(overrideRoot)
    if (!project) {
      return
    }
    const repository = this.getRepository(project)
    if (!repository) {
      return
    }

    return await repository.modifyWorkspaceParamsAndQueue()
  }

  public async getCwdThenRun(commandId: CommandId) {
    const cwd = await this.getCwd()

    if (!cwd) {
      return 'Could not run task'
    }

    return this.internalCommands.executeCommand(commandId, cwd)
  }

  public getCwdThenReport(commandId: CommandId) {
    return Toast.showOutput(this.getCwdThenRun(commandId))
  }

  public getCwdAndExpNameThenRun(commandId: CommandId) {
    return this.pickExpThenRun(commandId, cwd =>
      this.pickCommitOrExperiment(cwd)
    )
  }

  public async getCwdAndQuickPickThenRun(
    commandId: CommandId,
    quickPick: () => Thenable<string[] | undefined>
  ) {
    const cwd = await this.getCwd()
    if (!cwd) {
      return
    }
    const result = await quickPick()

    if (result) {
      return this.runCommand(commandId, cwd, ...result)
    }
  }

  public async createExperimentBranch() {
    const cwd = await this.getCwd()
    if (!cwd) {
      return
    }

    const experimentId = await this.pickCommitOrExperiment(cwd)

    if (!experimentId) {
      return
    }
    return this.getInputAndRun(
      getBranchExperimentCommand(this),
      Title.ENTER_BRANCH_NAME,
      `${experimentId}-branch`,
      cwd,
      experimentId
    )
  }

  public async getInputAndRun(
    runCommand: (...args: Args) => Promise<void | string>,
    title: Title,
    defaultValue: string,
    ...args: Args
  ) {
    const input = await getInput(title, defaultValue)
    if (!input) {
      return
    }
    return runCommand(...args, input)
  }

  public async getCwdIntegerInputAndRun(
    commandId: CommandId,
    title: Title,
    options: { prompt: string; value: string }
  ) {
    const cwd = await this.getCwd()
    if (!cwd) {
      return
    }

    const integer = await getPositiveIntegerInput(title, options)

    if (!integer) {
      return
    }

    return this.runCommand(commandId, cwd, integer)
  }

  public runCommand(commandId: CommandId, cwd: string, ...args: Args) {
    const output = this.internalCommands.executeCommand(commandId, cwd, ...args)

    void Toast.showOutput(output)
    return output
  }

  public createRepository(
    dvcRoot: string,
    subProjects: string[],
    pipeline: WorkspacePipeline,
    setup: Setup,
    resourceLocator: ResourceLocator
  ) {
    const experiments = this.dispose.track(
      new Experiments(
        dvcRoot,
        this.internalCommands,
        pipeline.getRepository(dvcRoot),
        resourceLocator,
        this.workspaceState,
        (branchesSelected: string[]) => this.selectBranches(branchesSelected),
        subProjects
      )
    )

    this.setRepository(dvcRoot, experiments)

    void experiments.setStudioValues(
      setup.getStudioUrl(),
      setup.getStudioAccessToken()
    )

    experiments.dispose.track(
      setup.onDidChangeStudioConnection(() => {
        void experiments.setStudioValues(
          setup.getStudioUrl(),
          setup.getStudioAccessToken()
        )
      })
    )

    experiments.dispose.track(
      experiments.onDidChangeIsWebviewFocused(
        dvcRoot => (this.focusedWebviewDvcRoot = dvcRoot)
      )
    )

    experiments.dispose.track(
      experiments.onDidChangeIsExperimentsFileFocused(
        dvcRoot => (this.focusedFileDvcRoot = dvcRoot)
      )
    )

    experiments.dispose.track(
      experiments.onDidChangeExperiments(() => {
        this.experimentsChanged.fire()
      })
    )

    experiments.dispose.track(
      experiments.onDidChangeColumns(() => {
        this.columnsChanged.fire()
      })
    )

    experiments.dispose.track(
      experiments.onDidChangeColumnOrderOrStatus(() => {
        this.experimentsChanged.fire()
      })
    )

    return experiments
  }

  public getFocusedOrOnlyOrPickProject() {
    return (
      this.focusedWebviewDvcRoot ||
      this.focusedFileDvcRoot ||
      this.getOnlyOrPickProject()
    )
  }

  public getHasData() {
    const allLoading = undefined

    const repositories = Object.values(this.repositories)

    if (repositories.some(repository => repository.getHasData())) {
      return true
    }

    if (repositories.some(repository => repository.getHasData() === false)) {
      return false
    }

    return allLoading
  }

  public getCliError() {
    const repositories = Object.values(this.repositories)
    const errors = []
    for (const repository of repositories) {
      const cliError = repository.getCliError()
      if (!cliError) {
        continue
      }
      errors.push(cliError)
    }
    return errors.length > 0 ? errors.join('\n') : undefined
  }

  public hasRunningExperiment() {
    return Object.values(this.repositories).some(experiments =>
      experiments.hasRunningExperiment()
    )
  }

  public async selectBranches(branchesSelected: string[]) {
    const dvcRoot = await this.getDvcRoot()
    if (!dvcRoot) {
      return
    }
    const repository = this.getRepository(dvcRoot)

    const allBranches = repository.getAvailableBranchesToSelect()

    return await quickPickManyValues(
      allBranches.map(branch => {
        return {
          label: branch,
          picked: branchesSelected.includes(branch),
          value: branch
        }
      }),
      {
        title: Title.SELECT_BRANCHES
      }
    )
  }

  private async getRepositoryThenUpdate(
    method:
      | 'addFilter'
      | 'addStarredFilter'
      | 'removeFilters'
      | 'addSort'
      | 'addStarredSort'
      | 'removeSorts'
      | 'selectExperimentsToPlot'
      | 'selectColumns'
      | 'selectFirstColumns',
    overrideRoot?: string
  ) {
    const dvcRoot = await this.getDvcRoot(overrideRoot)
    if (!dvcRoot) {
      return
    }
    return this.getRepository(dvcRoot)[method]()
  }

  private async getCwd(overrideRoot?: string) {
    const project = await this.getDvcRoot(overrideRoot)
    if (!project) {
      return
    }
    const repository = this.getRepository(project)
    return await repository.getPipelineCwd()
  }

  private async pickExpThenRun(
    commandId: CommandId,
    pickFunc: (cwd: string) => Thenable<string | undefined> | undefined
  ) {
    const cwd = await this.getFocusedOrOnlyOrPickProject()
    if (!cwd) {
      return
    }

    const experimentId = await pickFunc(cwd)

    if (!experimentId) {
      return
    }
    return this.runCommand(commandId, cwd, experimentId)
  }

  private pickCommitOrExperiment(cwd: string) {
    return this.getRepository(cwd).pickCommitOrExperiment()
  }
}