WorkingGroupsCommandBase.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import ExitCodes from '../ExitCodes';
  2. import AccountsCommandBase from './AccountsCommandBase';
  3. import { flags } from '@oclif/command';
  4. import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupMember, GroupOpening } from '../Types';
  5. import { apiModuleByGroup } from '../Api';
  6. import { CLIError } from '@oclif/errors';
  7. import inquirer from 'inquirer';
  8. import { ApiMethodInputArg } from './ApiCommandBase';
  9. import fs from 'fs';
  10. import path from 'path';
  11. import _ from 'lodash';
  12. import { ApplicationStageKeys } from '@joystream/types/hiring';
  13. const DEFAULT_GROUP = WorkingGroups.StorageProviders;
  14. const DRAFTS_FOLDER = 'opening-drafts';
  15. /**
  16. * Abstract base class for commands related to working groups
  17. */
  18. export default abstract class WorkingGroupsCommandBase extends AccountsCommandBase {
  19. group: WorkingGroups = DEFAULT_GROUP;
  20. static flags = {
  21. group: flags.string({
  22. char: 'g',
  23. description:
  24. "The working group context in which the command should be executed\n" +
  25. `Available values are: ${AvailableGroups.join(', ')}.`,
  26. required: true,
  27. default: DEFAULT_GROUP
  28. }),
  29. };
  30. // Use when lead access is required in given command
  31. async getRequiredLead(): Promise<GroupMember> {
  32. let selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
  33. let lead = await this.getApi().groupLead(this.group);
  34. if (!lead || lead.roleAccount.toString() !== selectedAccount.address) {
  35. this.error('Lead access required for this command!', { exit: ExitCodes.AccessDenied });
  36. }
  37. return lead;
  38. }
  39. // Use when worker access is required in given command
  40. async getRequiredWorker(): Promise<GroupMember> {
  41. let selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
  42. let groupMembers = await this.getApi().groupMembers(this.group);
  43. let groupMembersByAccount = groupMembers.filter(m => m.roleAccount.toString() === selectedAccount.address);
  44. if (!groupMembersByAccount.length) {
  45. this.error('Worker access required for this command!', { exit: ExitCodes.AccessDenied });
  46. }
  47. else if (groupMembersByAccount.length === 1) {
  48. return groupMembersByAccount[0];
  49. }
  50. else {
  51. return await this.promptForWorker(groupMembersByAccount);
  52. }
  53. }
  54. async promptForWorker(groupMembers: GroupMember[]): Promise<GroupMember> {
  55. const { choosenWorkerIndex } = await inquirer.prompt([{
  56. name: 'chosenWorkerIndex',
  57. message: 'Choose the worker to execute the command as',
  58. type: 'list',
  59. choices: groupMembers.map((groupMember, index) => ({
  60. name: `Worker ID ${ groupMember.workerId.toString() }`,
  61. value: index
  62. }))
  63. }]);
  64. return groupMembers[choosenWorkerIndex];
  65. }
  66. async promptForApplicationsToAccept(opening: GroupOpening): Promise<number[]> {
  67. const acceptableApplications = opening.applications.filter(a => a.stage === ApplicationStageKeys.Active);
  68. const acceptedApplications = await this.simplePrompt({
  69. message: 'Select succesful applicants',
  70. type: 'checkbox',
  71. choices: acceptableApplications.map(a => ({
  72. name: ` ${a.wgApplicationId}: ${a.member?.handle.toString()}`,
  73. value: a.wgApplicationId,
  74. }))
  75. });
  76. return acceptedApplications;
  77. }
  78. async promptForNewOpeningDraftName() {
  79. let
  80. draftName: string = '',
  81. fileExists: boolean = false,
  82. overrideConfirmed: boolean = false;
  83. do {
  84. draftName = await this.simplePrompt({
  85. type: 'input',
  86. message: 'Provide the draft name',
  87. validate: val => (typeof val === 'string' && val.length >= 1) || 'Draft name is required!'
  88. });
  89. fileExists = fs.existsSync(this.getOpeningDraftPath(draftName));
  90. if (fileExists) {
  91. overrideConfirmed = await this.simplePrompt({
  92. type: 'confirm',
  93. message: 'Such draft already exists. Do you wish to override it?',
  94. default: false
  95. });
  96. }
  97. } while(fileExists && !overrideConfirmed);
  98. return draftName;
  99. }
  100. async promptForOpeningDraft() {
  101. let draftFiles: string[] = [];
  102. try {
  103. draftFiles = fs.readdirSync(this.getOpeingDraftsPath());
  104. }
  105. catch(e) {
  106. throw this.createDataReadError(DRAFTS_FOLDER);
  107. }
  108. if (!draftFiles.length) {
  109. throw new CLIError('No drafts available!', { exit: ExitCodes.FileNotFound });
  110. }
  111. const draftNames = draftFiles.map(fileName => _.startCase(fileName.replace('.json', '')));
  112. const selectedDraftName = await this.simplePrompt({
  113. message: 'Select a draft',
  114. type: 'list',
  115. choices: draftNames
  116. });
  117. return selectedDraftName;
  118. }
  119. loadOpeningDraftParams(draftName: string) {
  120. const draftFilePath = this.getOpeningDraftPath(draftName);
  121. const params = this.extrinsicArgsFromDraft(
  122. apiModuleByGroup[this.group],
  123. 'addOpening',
  124. draftFilePath
  125. );
  126. return params;
  127. }
  128. getOpeingDraftsPath() {
  129. return path.join(this.getAppDataPath(), DRAFTS_FOLDER);
  130. }
  131. getOpeningDraftPath(draftName: string) {
  132. return path.join(this.getOpeingDraftsPath(), _.snakeCase(draftName)+'.json');
  133. }
  134. saveOpeningDraft(draftName: string, params: ApiMethodInputArg[]) {
  135. const paramsJson = JSON.stringify(
  136. params.map(p => p.toJSON()),
  137. null,
  138. 2
  139. );
  140. try {
  141. fs.writeFileSync(this.getOpeningDraftPath(draftName), paramsJson);
  142. } catch(e) {
  143. throw this.createDataWriteError(DRAFTS_FOLDER);
  144. }
  145. }
  146. private initOpeningDraftsDir(): void {
  147. if (!fs.existsSync(this.getOpeingDraftsPath())) {
  148. fs.mkdirSync(this.getOpeingDraftsPath());
  149. }
  150. }
  151. async init() {
  152. await super.init();
  153. try {
  154. this.initOpeningDraftsDir();
  155. } catch (e) {
  156. throw this.createDataDirInitError();
  157. }
  158. const { flags } = this.parse(this.constructor as typeof WorkingGroupsCommandBase);
  159. if (!AvailableGroups.includes(flags.group as any)) {
  160. throw new CLIError(`Invalid group! Available values are: ${AvailableGroups.join(', ')}`, { exit: ExitCodes.InvalidInput });
  161. }
  162. this.group = flags.group as WorkingGroups;
  163. }
  164. }