From 62d32687c294eae1a2eb560b62c4cb24a98aaa4a Mon Sep 17 00:00:00 2001 From: Max Shaposhnik Date: Mon, 3 Apr 2017 15:11:45 +0300 Subject: [PATCH] Factory migration (#4413) --- assembly/assembly-ide-war/pom.xml | 12 + .../src/main/webapp/WEB-INF/rewrite.config | 4 + assembly/assembly-wsmaster-war/pom.xml | 21 +- .../che/api/deploy/WsMasterModule.java | 17 + .../main/resources/META-INF/persistence.xml | 12 + .../che/api/core/model/factory/Policies.java | 5 - .../che/filter/CheCacheDisablingFilter.java | 1 + .../che/filter/CheCacheForcingFilter.java | 1 + .../filter/CheCacheDisablingFilterTest.java | 6 +- .../che/filter/CheCacheForcingFilterTest.java | 6 +- .../action/factory-action-box.controller.ts | 116 +++ .../action/factory-action-box.directive.ts | 55 ++ .../action/factory-action-box.html | 56 ++ .../action/factory-action-box.styl | 57 ++ .../action/factory-action-edit.controller.ts | 55 ++ .../action/factory-action-edit.html | 31 + .../factory-command-edit.controller.ts | 48 ++ .../command/factory-command-edit.html | 18 + .../command/factory-command.controller.ts | 104 +++ .../command/factory-command.directive.ts | 50 ++ .../command/factory-command.html | 74 ++ .../command/factory-command.styl | 53 ++ .../factory-from-file.controller.ts | 106 +++ .../factory-from-file.directive.ts | 59 ++ .../config-file-tab/factory-from-file.html | 31 + .../config-file-tab/factory-from-file.styl | 15 + .../create-factory/create-factory-config.ts | 72 ++ .../create-factory.controller.ts | 164 ++++ .../create-factory/create-factory.html | 117 +++ .../create-factory/create-factory.styl | 94 +++ .../git/create-factory-git.controller.ts | 23 + .../git/create-factory-git.directive.ts | 47 ++ .../git/create-factory-git.html | 28 + .../git/create-factory-git.styl | 2 + .../factory-from-template.controller.ts | 75 ++ .../factory-from-template.directive.ts | 52 ++ .../template-tab/factory-from-template.html | 38 + .../template-tab/factory-from-template.styl | 6 + .../factory-from-workpsace.controller.ts | 121 +++ .../factory-from-workspace.directive.ts | 42 + .../factory-from-workspace.html | 50 ++ .../factory-from-workspace.styl | 88 ++ .../src/app/factories/factories-config.ts | 63 ++ .../factory-details/factory-details-config.ts | 39 + .../factory-details.controller.ts | 52 ++ .../factory-details/factory-details.html | 31 + .../factory-details/factory-details.styl | 20 + .../factory-information.controller.ts | 277 +++++++ .../factory-information.directive.ts | 50 ++ .../factory-information.html | 204 +++++ .../factory-information.styl | 40 + .../information-tab/information-tab-config.ts | 23 + .../last-factories/last-factories-config.ts | 22 + .../last-factories.controller.ts | 67 ++ .../last-factories.directive.ts | 39 + .../last-factories/last-factories.html | 40 + .../last-factories/last-factories.styl | 3 + .../factory-item/factory-item.controller.ts | 81 ++ .../factory-item/factory-item.directive.ts | 48 ++ .../factory-item/factory-item.html | 59 ++ .../factory-item/factory-item.styl | 8 + .../list-factories.controller.ts | 279 +++++++ .../list-factories/list-factories.html | 108 +++ .../load-factory/load-factory.controller.ts | 689 ++++++++++++++++ .../factories/load-factory/load-factory.html | 68 ++ .../load-factory/load-factory.service.ts | 119 +++ .../factories/load-factory/load-factory.styl | 47 ++ dashboard/src/app/index.module.ts | 2 + dashboard/src/app/navbar/navbar.controller.ts | 6 + dashboard/src/app/navbar/navbar.html | 10 + .../api/builder/che-api-builder.factory.ts | 18 + .../api/builder/che-factory-builder.ts | 87 ++ .../api/builder/che-user-builder.ts | 66 ++ .../src/components/api/che-api-config.ts | 6 + .../src/components/api/che-api.factory.ts | 40 +- .../api/che-factory-template.factory.ts | 77 ++ .../src/components/api/che-factory.factory.ts | 645 +++++++++++++++ .../src/components/api/che-factory.spec.ts | 238 ++++++ .../src/components/api/che-user.factory.ts | 432 ++++++++++ dashboard/src/components/api/che-user.spec.ts | 275 +++++++ .../api/namespace/che.namespace.d.ts | 15 + .../components/api/test/che-http-backend.ts | 116 +++ dashboard/src/components/typings/che.d.ts | 18 + ide/che-core-ide-app/pom.xml | 4 + .../che/ide/CoreLocalizationConstant.java | 105 +++ .../eclipse/che/ide/core/CoreGinModule.java | 4 +- .../che/ide/factory/FactoryExtension.java | 86 ++ .../che/ide/factory/FactoryResources.java | 54 ++ .../factory/accept/AcceptFactoryHandler.java | 134 ++++ .../factory/action/CreateFactoryAction.java | 47 ++ .../configure/CreateFactoryPresenter.java | 117 +++ .../factory/configure/CreateFactoryView.java | 63 ++ .../configure/CreateFactoryViewImpl.java | 206 +++++ .../configure/CreateFactoryViewImpl.ui.xml | 105 +++ .../ide/factory/inject/FactoryGinModule.java | 44 + .../factory/json/ImportFromConfigAction.java | 47 ++ .../json/ImportFromConfigPresenter.java | 105 +++ .../factory/json/ImportFromConfigView.java | 47 ++ .../json/ImportFromConfigViewImpl.java | 176 ++++ .../json/ImportFromConfigViewImpl.ui.xml | 46 ++ .../factory/utils/FactoryProjectImporter.java | 343 ++++++++ .../welcome/GreetingPartPresenter.java | 114 +++ .../ide/factory/welcome/GreetingPartView.java | 41 + .../factory/welcome/GreetingPartViewImpl.java | 103 +++ .../welcome/OpenWelcomePageAction.java | 39 + .../che/ide/factory/welcome/TooltipHint.java | 96 +++ .../ide/factory/welcome/TooltipHint.ui.xml | 104 +++ .../ShowWelcomePreferencePagePresenter.java | 79 ++ .../ShowWelcomePreferencePageView.java | 28 + .../ShowWelcomePreferencePageViewImpl.java | 63 ++ .../ShowWelcomePreferencePageViewImpl.ui.xml | 34 + .../workspace/FactoryWorkspaceComponent.java | 42 +- .../org/eclipse/che/ide/Core.gwt.xml | 1 + .../ide/CoreLocalizationConstant.properties | 43 + .../org/eclipse/che/ide/factory/Factory.css | 39 + .../org/eclipse/che/ide/factory/cog-icon.svg | 18 + .../org/eclipse/che/ide/factory/execute.svg | 20 + .../eclipse/che/ide/factory/export-config.svg | 30 + .../eclipse/che/ide/factory/import-config.svg | 30 + .../pom.xml | 108 +++ .../factory/resolver/GitHubFactoryModule.java | 17 +- .../GithubFactoryParametersResolver.java | 108 +++ .../resolver/GithubSourceStorageBuilder.java | 47 ++ .../factory/resolver/GithubURLParser.java | 37 + .../factory/resolver/GithubURLParserImpl.java | 57 ++ .../github/factory/resolver/GithubUrl.java | 202 +++++ .../resolver/LegacyGithubURLParser.java | 43 + .../GithubFactoryParametersResolverTest.java | 236 ++++++ .../factory/resolver/GithubURLParserTest.java | 89 +++ .../factory/resolver/GithubUrlTest.java | 74 ++ .../che-plugin-github-pullrequest/pom.xml | 131 +++ .../client/GitHubContributionWorkflow.java | 146 ++++ .../client/GitHubHostingService.java | 513 ++++++++++++ .../pullrequest/client/GitHubTemplates.java | 32 + .../client/GithubStagesProvider.java | 97 +++ .../inject/GithubPullRequestGinModule.java | 51 ++ .../pullrequest/GithubPullRequest.gwt.xml | 27 + plugins/plugin-github/pom.xml | 2 + .../che-plugin-pullrequest-ide/pom.xml | 117 +++ .../client/ContributeMessages.java | 255 ++++++ .../client/ContributeResources.java | 53 ++ .../client/ContributionExtension.java | 39 + .../client/ContributionMixinProvider.java | 231 ++++++ .../dialogs/commit/CommitPresenter.java | 155 ++++ .../client/dialogs/commit/CommitView.java | 80 ++ .../client/dialogs/commit/CommitViewImpl.java | 138 ++++ .../dialogs/commit/CommitViewImpl.ui.xml | 44 + .../dialogs/commit/CommitViewUiBinder.java | 24 + .../dialogs/paste/PasteAwareTextBox.java | 69 ++ .../client/dialogs/paste/PasteEvent.java | 33 + .../client/dialogs/paste/PasteHandler.java | 20 + .../events/ContextInvalidatedEvent.java | 44 + .../events/ContextInvalidatedHandler.java | 30 + .../events/ContextPropertyChangeEvent.java | 67 ++ .../events/ContextPropertyChangeHandler.java | 28 + .../events/CurrentContextChangedEvent.java | 42 + .../events/CurrentContextChangedHandler.java | 30 + .../pullrequest/client/events/StepEvent.java | 69 ++ .../client/events/StepHandler.java | 38 + .../client/inject/PullRequestGinModule.java | 52 ++ .../contribute/ContributePartPresenter.java | 751 ++++++++++++++++++ .../parts/contribute/ContributePartView.java | 211 +++++ .../contribute/ContributePartViewImpl.java | 498 ++++++++++++ .../contribute/ContributePartViewImpl.ui.xml | 209 +++++ .../ContributePartViewUiBinder.java | 22 + .../parts/contribute/StagesProvider.java | 80 ++ .../parts/contribute/TextChangedHandler.java | 30 + .../client/steps/AddForkRemoteStep.java | 152 ++++ .../steps/AddForkRemoteStepFactory.java | 22 + .../client/steps/AddHttpForkRemoteStep.java | 40 + .../steps/AddReviewFactoryLinkStep.java | 39 + .../client/steps/AddSshForkRemoteStep.java | 40 + .../steps/AuthorizeCodenvyOnVCSHostStep.java | 112 +++ .../client/steps/CheckBranchToPush.java | 45 ++ .../client/steps/CommitWorkingTreeStep.java | 84 ++ .../client/steps/CreateForkStep.java | 101 +++ .../steps/DefineExecutionConfiguration.java | 29 + .../DefineForkRemoteUrlProtocolStep.java | 25 + .../client/steps/DefineWorkBranchStep.java | 72 ++ .../client/steps/DetectPullRequestStep.java | 78 ++ .../DetermineUpstreamRepositoryStep.java | 77 ++ .../steps/GenerateReviewFactoryStep.java | 122 +++ .../steps/InitializeWorkflowContextStep.java | 145 ++++ .../client/steps/IssuePullRequestStep.java | 85 ++ .../client/steps/PushBranchOnForkStep.java | 42 + .../client/steps/PushBranchOnOriginStep.java | 45 ++ .../client/steps/PushBranchStep.java | 187 +++++ .../client/steps/PushBranchStepFactory.java | 21 + .../client/steps/UpdatePullRequestStep.java | 163 ++++ .../client/steps/WaitForkOnRemoteStep.java | 82 ++ .../steps/WaitForkOnRemoteStepFactory.java | 20 + .../client/utils/FactoryHelper.java | 47 ++ .../client/vcs/BranchUpToDateException.java | 33 + .../pullrequest/client/vcs/GitVcsService.java | 292 +++++++ .../pullrequest/client/vcs/VcsService.java | 149 ++++ .../client/vcs/VcsServiceProvider.java | 47 ++ .../vcs/hosting/HostingServiceTemplates.java | 68 ++ .../NoCommitsInPullRequestException.java | 35 + .../hosting/NoHistoryInCommonException.java | 23 + .../vcs/hosting/NoPullRequestException.java | 28 + .../vcs/hosting/NoUserForkException.java | 31 + ...HostingServiceImplementationException.java | 29 + .../PullRequestAlreadyExistsException.java | 33 + .../client/vcs/hosting/ServiceUtil.java | 70 ++ .../client/vcs/hosting/VcsHostingService.java | 228 ++++++ .../hosting/VcsHostingServiceProvider.java | 80 ++ .../client/workflow/ChainExecutor.java | 62 ++ .../pullrequest/client/workflow/Context.java | 443 +++++++++++ .../client/workflow/ContributionWorkflow.java | 40 + .../pullrequest/client/workflow/Step.java | 42 + .../client/workflow/StepsChain.java | 219 +++++ .../client/workflow/SyntheticStep.java | 21 + .../client/workflow/WorkflowExecutor.java | 281 +++++++ .../client/workflow/WorkflowStatus.java | 87 ++ .../plugin/pullrequest/PullRequest.gwt.xml | 25 + .../plugin/pullrequest/client/Contribute.css | 95 +++ .../client/ContributeMessages.properties | 145 ++++ .../pullrequest/client/images/refresh.svg | 29 + .../che-plugin-pullrequest-server/pom.xml | 114 +++ .../server/ContributionProjectType.java | 37 + .../server/ContributionProjectTypeModule.java | 32 + .../che-plugin-pullrequest-shared/pom.xml | 122 +++ .../ContributionProjectTypeConstants.java | 34 + .../pullrequest/shared/dto/Configuration.java | 31 + .../pullrequest/shared/dto/HostUser.java | 32 + .../pullrequest/shared/dto/IssueComment.java | 28 + .../pullrequest/shared/dto/PullRequest.java | 52 ++ .../pullrequest/shared/dto/Repository.java | 36 + plugins/plugin-pullrequest-parent/pom.xml | 29 + plugins/plugin-urlfactory/pom.xml | 152 ++++ .../urlfactory/ProjectConfigDtoMerger.java | 63 ++ .../che/plugin/urlfactory/URLChecker.java | 111 +++ .../plugin/urlfactory/URLFactoryBuilder.java | 140 ++++ .../che/plugin/urlfactory/URLFetcher.java | 97 +++ .../ProjectConfigDtoMergerTest.java | 107 +++ .../che/plugin/urlfactory/URLCheckerTest.java | 197 +++++ .../urlfactory/URLFactoryBuilderTest.java | 186 +++++ .../che/plugin/urlfactory/URLFetcherTest.java | 139 ++++ .../src/test/resources/logback-test.xml | 27 + .../eclipse/che/plugin/urlfactory/.che.json | 1 + plugins/pom.xml | 2 + pom.xml | 50 ++ .../api/factory/shared/dto/PoliciesDto.java | 11 - wsmaster/che-core-api-factory/pom.xml | 14 +- .../che/api/factory/server/DtoConverter.java | 1 - .../che/api/factory/server/FactoryImage.java | 4 +- .../api/factory/server/FactoryManager.java | 10 +- .../server/impl/FactoryBaseValidator.java | 5 + .../factory/server/model/impl/ActionImpl.java | 10 +- .../factory/server/model/impl/AuthorImpl.java | 2 +- .../factory/server/model/impl/ButtonImpl.java | 2 +- .../server/model/impl/FactoryImpl.java | 8 +- .../factory/server/model/impl/IdeImpl.java | 8 +- .../server/model/impl/OnAppClosedImpl.java | 8 +- .../server/model/impl/OnAppLoadedImpl.java | 8 +- .../model/impl/OnProjectsLoadedImpl.java | 8 +- .../server/model/impl/PoliciesImpl.java | 21 +- .../factory/server/FactoryManagerTest.java | 49 ++ .../factory/server/jpa/FactoryTckModule.java | 92 +++ .../server/spi/tck/FactoryDaoTest.java | 43 +- ...org.eclipse.che.commons.test.tck.TckModule | 1 + wsmaster/che-core-api-machine/pom.xml | 18 - .../api/machine/server/jpa/JpaTckModule.java | 19 +- .../test/resources/META-INF/persistence.xml | 37 - .../che-schema/5.7.0/1__add_factory.sql | 166 ++++ .../5.7.0/2__remove_match_policy.sql | 16 + 266 files changed, 21375 insertions(+), 207 deletions(-) create mode 100644 dashboard/src/app/factories/create-factory/action/factory-action-box.controller.ts create mode 100644 dashboard/src/app/factories/create-factory/action/factory-action-box.directive.ts create mode 100644 dashboard/src/app/factories/create-factory/action/factory-action-box.html create mode 100644 dashboard/src/app/factories/create-factory/action/factory-action-box.styl create mode 100644 dashboard/src/app/factories/create-factory/action/factory-action-edit.controller.ts create mode 100644 dashboard/src/app/factories/create-factory/action/factory-action-edit.html create mode 100644 dashboard/src/app/factories/create-factory/command/factory-command-edit.controller.ts create mode 100644 dashboard/src/app/factories/create-factory/command/factory-command-edit.html create mode 100644 dashboard/src/app/factories/create-factory/command/factory-command.controller.ts create mode 100644 dashboard/src/app/factories/create-factory/command/factory-command.directive.ts create mode 100644 dashboard/src/app/factories/create-factory/command/factory-command.html create mode 100644 dashboard/src/app/factories/create-factory/command/factory-command.styl create mode 100644 dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.controller.ts create mode 100644 dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.directive.ts create mode 100644 dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.html create mode 100644 dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.styl create mode 100644 dashboard/src/app/factories/create-factory/create-factory-config.ts create mode 100644 dashboard/src/app/factories/create-factory/create-factory.controller.ts create mode 100644 dashboard/src/app/factories/create-factory/create-factory.html create mode 100644 dashboard/src/app/factories/create-factory/create-factory.styl create mode 100644 dashboard/src/app/factories/create-factory/git/create-factory-git.controller.ts create mode 100644 dashboard/src/app/factories/create-factory/git/create-factory-git.directive.ts create mode 100644 dashboard/src/app/factories/create-factory/git/create-factory-git.html create mode 100644 dashboard/src/app/factories/create-factory/git/create-factory-git.styl create mode 100644 dashboard/src/app/factories/create-factory/template-tab/factory-from-template.controller.ts create mode 100644 dashboard/src/app/factories/create-factory/template-tab/factory-from-template.directive.ts create mode 100644 dashboard/src/app/factories/create-factory/template-tab/factory-from-template.html create mode 100644 dashboard/src/app/factories/create-factory/template-tab/factory-from-template.styl create mode 100644 dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workpsace.controller.ts create mode 100644 dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.directive.ts create mode 100644 dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.html create mode 100644 dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.styl create mode 100644 dashboard/src/app/factories/factories-config.ts create mode 100644 dashboard/src/app/factories/factory-details/factory-details-config.ts create mode 100644 dashboard/src/app/factories/factory-details/factory-details.controller.ts create mode 100644 dashboard/src/app/factories/factory-details/factory-details.html create mode 100644 dashboard/src/app/factories/factory-details/factory-details.styl create mode 100644 dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts create mode 100644 dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.directive.ts create mode 100644 dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.html create mode 100644 dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.styl create mode 100644 dashboard/src/app/factories/factory-details/information-tab/information-tab-config.ts create mode 100644 dashboard/src/app/factories/last-factories/last-factories-config.ts create mode 100644 dashboard/src/app/factories/last-factories/last-factories.controller.ts create mode 100644 dashboard/src/app/factories/last-factories/last-factories.directive.ts create mode 100644 dashboard/src/app/factories/last-factories/last-factories.html create mode 100644 dashboard/src/app/factories/last-factories/last-factories.styl create mode 100644 dashboard/src/app/factories/list-factories/factory-item/factory-item.controller.ts create mode 100644 dashboard/src/app/factories/list-factories/factory-item/factory-item.directive.ts create mode 100644 dashboard/src/app/factories/list-factories/factory-item/factory-item.html create mode 100644 dashboard/src/app/factories/list-factories/factory-item/factory-item.styl create mode 100644 dashboard/src/app/factories/list-factories/list-factories.controller.ts create mode 100644 dashboard/src/app/factories/list-factories/list-factories.html create mode 100644 dashboard/src/app/factories/load-factory/load-factory.controller.ts create mode 100644 dashboard/src/app/factories/load-factory/load-factory.html create mode 100644 dashboard/src/app/factories/load-factory/load-factory.service.ts create mode 100644 dashboard/src/app/factories/load-factory/load-factory.styl create mode 100644 dashboard/src/components/api/builder/che-factory-builder.ts create mode 100644 dashboard/src/components/api/builder/che-user-builder.ts create mode 100644 dashboard/src/components/api/che-factory-template.factory.ts create mode 100644 dashboard/src/components/api/che-factory.factory.ts create mode 100644 dashboard/src/components/api/che-factory.spec.ts create mode 100644 dashboard/src/components/api/che-user.factory.ts create mode 100644 dashboard/src/components/api/che-user.spec.ts create mode 100644 dashboard/src/components/api/namespace/che.namespace.d.ts create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryExtension.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryResources.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/accept/AcceptFactoryHandler.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/action/CreateFactoryAction.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryPresenter.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryView.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryViewImpl.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryViewImpl.ui.xml create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/inject/FactoryGinModule.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigAction.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigPresenter.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigView.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigViewImpl.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigViewImpl.ui.xml create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/utils/FactoryProjectImporter.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartPresenter.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartView.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartViewImpl.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/OpenWelcomePageAction.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/TooltipHint.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/TooltipHint.ui.xml create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePagePresenter.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageView.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageViewImpl.java create mode 100644 ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageViewImpl.ui.xml create mode 100644 ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/Factory.css create mode 100644 ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/cog-icon.svg create mode 100644 ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/execute.svg create mode 100644 ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/export-config.svg create mode 100644 ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/import-config.svg create mode 100644 plugins/plugin-github/che-plugin-github-factory-resolver/pom.xml rename ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryApiModule.java => plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GitHubFactoryModule.java (57%) create mode 100644 plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubFactoryParametersResolver.java create mode 100644 plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubSourceStorageBuilder.java create mode 100644 plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParser.java create mode 100644 plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParserImpl.java create mode 100644 plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubUrl.java create mode 100644 plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/LegacyGithubURLParser.java create mode 100644 plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubFactoryParametersResolverTest.java create mode 100644 plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParserTest.java create mode 100644 plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubUrlTest.java create mode 100644 plugins/plugin-github/che-plugin-github-pullrequest/pom.xml create mode 100644 plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubContributionWorkflow.java create mode 100644 plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubHostingService.java create mode 100644 plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubTemplates.java create mode 100644 plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GithubStagesProvider.java create mode 100644 plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/inject/GithubPullRequestGinModule.java create mode 100644 plugins/plugin-github/che-plugin-github-pullrequest/src/main/resources/org/eclipse/che/plugin/pullrequest/GithubPullRequest.gwt.xml create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/pom.xml create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributeMessages.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributeResources.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributionExtension.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributionMixinProvider.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitPresenter.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitView.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewImpl.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewImpl.ui.xml create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewUiBinder.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteAwareTextBox.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteEvent.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteHandler.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextInvalidatedEvent.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextInvalidatedHandler.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextPropertyChangeEvent.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextPropertyChangeHandler.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/CurrentContextChangedEvent.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/CurrentContextChangedHandler.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/StepEvent.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/StepHandler.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/inject/PullRequestGinModule.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartPresenter.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartView.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewImpl.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewImpl.ui.xml create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewUiBinder.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/StagesProvider.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/TextChangedHandler.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddForkRemoteStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddForkRemoteStepFactory.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddHttpForkRemoteStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddReviewFactoryLinkStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddSshForkRemoteStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AuthorizeCodenvyOnVCSHostStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CheckBranchToPush.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CommitWorkingTreeStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CreateForkStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineExecutionConfiguration.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineForkRemoteUrlProtocolStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineWorkBranchStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DetectPullRequestStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DetermineUpstreamRepositoryStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/GenerateReviewFactoryStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/InitializeWorkflowContextStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/IssuePullRequestStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchOnForkStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchOnOriginStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchStepFactory.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/UpdatePullRequestStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/WaitForkOnRemoteStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/WaitForkOnRemoteStepFactory.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/utils/FactoryHelper.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/BranchUpToDateException.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/GitVcsService.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/VcsService.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/VcsServiceProvider.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/HostingServiceTemplates.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoCommitsInPullRequestException.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoHistoryInCommonException.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoPullRequestException.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoUserForkException.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoVcsHostingServiceImplementationException.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/PullRequestAlreadyExistsException.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/ServiceUtil.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/VcsHostingService.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/VcsHostingServiceProvider.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/ChainExecutor.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/Context.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/ContributionWorkflow.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/Step.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/StepsChain.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/SyntheticStep.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/WorkflowExecutor.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/WorkflowStatus.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/PullRequest.gwt.xml create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/Contribute.css create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/ContributeMessages.properties create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/images/refresh.svg create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/pom.xml create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/src/main/java/org/eclipse/che/plugin/pullrequest/server/ContributionProjectType.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/src/main/java/org/eclipse/che/plugin/pullrequest/server/ContributionProjectTypeModule.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/pom.xml create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/ContributionProjectTypeConstants.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/Configuration.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/HostUser.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/IssueComment.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/PullRequest.java create mode 100644 plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/Repository.java create mode 100644 plugins/plugin-pullrequest-parent/pom.xml create mode 100644 plugins/plugin-urlfactory/pom.xml create mode 100644 plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/ProjectConfigDtoMerger.java create mode 100644 plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLChecker.java create mode 100644 plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLFactoryBuilder.java create mode 100644 plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLFetcher.java create mode 100644 plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/ProjectConfigDtoMergerTest.java create mode 100644 plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLCheckerTest.java create mode 100644 plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLFactoryBuilderTest.java create mode 100644 plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLFetcherTest.java create mode 100644 plugins/plugin-urlfactory/src/test/resources/logback-test.xml create mode 100644 plugins/plugin-urlfactory/src/test/resources/org/eclipse/che/plugin/urlfactory/.che.json create mode 100644 wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/FactoryManagerTest.java create mode 100644 wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/jpa/FactoryTckModule.java create mode 100644 wsmaster/che-core-api-factory/src/test/resources/META-INF/services/org.eclipse.che.commons.test.tck.TckModule delete mode 100644 wsmaster/che-core-api-machine/src/test/resources/META-INF/persistence.xml create mode 100644 wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.7.0/1__add_factory.sql create mode 100644 wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.7.0/2__remove_match_policy.sql diff --git a/assembly/assembly-ide-war/pom.xml b/assembly/assembly-ide-war/pom.xml index 54e1e496ca8..2476d68fd9b 100644 --- a/assembly/assembly-ide-war/pom.xml +++ b/assembly/assembly-ide-war/pom.xml @@ -155,6 +155,10 @@ org.eclipse.che.plugin che-plugin-github-oauth2 + + org.eclipse.che.plugin + che-plugin-github-pullrequest + org.eclipse.che.plugin che-plugin-github-shared @@ -243,6 +247,14 @@ org.eclipse.che.plugin che-plugin-product-info + + org.eclipse.che.plugin + che-plugin-pullrequest-ide + + + org.eclipse.che.plugin + che-plugin-pullrequest-shared + org.eclipse.che.plugin che-plugin-python-lang-ide diff --git a/assembly/assembly-ide-war/src/main/webapp/WEB-INF/rewrite.config b/assembly/assembly-ide-war/src/main/webapp/WEB-INF/rewrite.config index 90fb3d1a251..ec0e000eb91 100644 --- a/assembly/assembly-ide-war/src/main/webapp/WEB-INF/rewrite.config +++ b/assembly/assembly-ide-war/src/main/webapp/WEB-INF/rewrite.config @@ -1,2 +1,6 @@ RewriteRule ^/api/(.*)$ /wsmaster/api/$1 [L] +RewriteRule ^/factory\\?(.*)$ /dashboard/#/load-factory/$1 [R,NE] +RewriteRule ^/factory/(.*)$ /dashboard/#/load-factory/$1 [R,NE] +RewriteRule ^/f/?(.*)$ /dashboard/#/load-factory/$1 [R,NE] +RewriteRule ^/f\\?(.*)$ /dashboard/#/load-factory/$1 [R,NE] diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml index 0aac21c022a..a53c9aa1268 100644 --- a/assembly/assembly-wsmaster-war/pom.xml +++ b/assembly/assembly-wsmaster-war/pom.xml @@ -106,6 +106,10 @@ org.eclipse.che.core che-core-api-core + + org.eclipse.che.core + che-core-api-factory + org.eclipse.che.core che-core-api-machine @@ -178,6 +182,10 @@ org.eclipse.che.plugin che-plugin-docker-machine + + org.eclipse.che.plugin + che-plugin-github-factory-resolver + org.eclipse.che.plugin che-plugin-github-oauth2 @@ -198,10 +206,18 @@ org.eclipse.che.plugin che-plugin-openshift-client + + org.eclipse.che.plugin + che-plugin-pullrequest-server + org.eclipse.che.plugin che-plugin-ssh-machine + + org.eclipse.che.plugin + che-plugin-url-factory + org.eclipse.persistence eclipselink @@ -237,11 +253,6 @@ logback-classic test - - org.eclipse.che.core - che-core-api-factory - test - org.eclipse.che.core che-core-commons-test diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java index caa6e11e47b..8ee2554ac9a 100644 --- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java +++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java @@ -29,6 +29,10 @@ import org.eclipse.che.api.core.rest.CheJsonProvider; import org.eclipse.che.api.core.rest.MessageBodyAdapter; import org.eclipse.che.api.core.rest.MessageBodyAdapterInterceptor; +import org.eclipse.che.api.factory.server.FactoryAcceptValidator; +import org.eclipse.che.api.factory.server.FactoryCreateValidator; +import org.eclipse.che.api.factory.server.FactoryEditValidator; +import org.eclipse.che.api.factory.server.FactoryParametersResolver; import org.eclipse.che.api.machine.shared.Constants; import org.eclipse.che.api.user.server.TokenValidator; import org.eclipse.che.api.workspace.server.WorkspaceConfigMessageBodyAdapter; @@ -36,6 +40,7 @@ import org.eclipse.che.api.workspace.server.stack.StackMessageBodyAdapter; import org.eclipse.che.core.db.schema.SchemaInitializer; import org.eclipse.che.inject.DynaModule; +import org.eclipse.che.plugin.github.factory.resolver.GithubFactoryParametersResolver; import org.flywaydb.core.internal.util.PlaceholderReplacer; import javax.sql.DataSource; @@ -62,6 +67,18 @@ protected void configure() { bind(org.eclipse.che.core.db.DBInitializer.class).asEagerSingleton(); bind(PlaceholderReplacer.class).toProvider(org.eclipse.che.core.db.schema.impl.flyway.PlaceholderReplacerProvider.class); + //factory + bind(FactoryAcceptValidator.class).to(org.eclipse.che.api.factory.server.impl.FactoryAcceptValidatorImpl.class); + bind(FactoryCreateValidator.class).to(org.eclipse.che.api.factory.server.impl.FactoryCreateValidatorImpl.class); + bind(FactoryEditValidator.class).to(org.eclipse.che.api.factory.server.impl.FactoryEditValidatorImpl.class); + bind(org.eclipse.che.api.factory.server.FactoryService.class); + install(new org.eclipse.che.api.factory.server.jpa.FactoryJpaModule()); + + Multibinder factoryParametersResolverMultibinder = + Multibinder.newSetBinder(binder(), FactoryParametersResolver.class); + factoryParametersResolverMultibinder.addBinding() + .to(GithubFactoryParametersResolver.class); + install(new org.eclipse.che.plugin.docker.compose.ComposeModule()); bind(org.eclipse.che.api.user.server.CheUserCreator.class); diff --git a/assembly/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml b/assembly/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml index a31f36d1c7f..c9aeb18f8dd 100644 --- a/assembly/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml +++ b/assembly/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml @@ -38,6 +38,18 @@ org.eclipse.che.api.machine.server.model.impl.SnapshotImpl org.eclipse.che.api.machine.server.recipe.RecipeImpl + org.eclipse.che.api.factory.server.model.impl.FactoryImpl + org.eclipse.che.api.factory.server.model.impl.OnAppClosedImpl + org.eclipse.che.api.factory.server.model.impl.OnProjectsLoadedImpl + org.eclipse.che.api.factory.server.model.impl.OnAppLoadedImpl + org.eclipse.che.api.factory.server.model.impl.PoliciesImpl + org.eclipse.che.api.factory.server.model.impl.ActionImpl + org.eclipse.che.api.factory.server.model.impl.AuthorImpl + org.eclipse.che.api.factory.server.model.impl.ButtonAttributesImpl + org.eclipse.che.api.factory.server.model.impl.ButtonImpl + org.eclipse.che.api.factory.server.model.impl.IdeImpl + org.eclipse.che.api.factory.server.FactoryImage + org.eclipse.che.api.ssh.server.model.impl.SshPairImpl true diff --git a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/factory/Policies.java b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/factory/Policies.java index 252dd1036f6..b0b6de57c39 100644 --- a/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/factory/Policies.java +++ b/core/che-core-api-model/src/main/java/org/eclipse/che/api/core/model/factory/Policies.java @@ -32,11 +32,6 @@ public interface Policies { */ Long getUntil(); - /** - * Re-open projects on factory 2-nd click - */ - String getMatch(); - /** * Workspace creation strategy */ diff --git a/core/commons/che-core-commons-j2ee/src/main/java/org/eclipse/che/filter/CheCacheDisablingFilter.java b/core/commons/che-core-commons-j2ee/src/main/java/org/eclipse/che/filter/CheCacheDisablingFilter.java index 83deadc6df5..8c902d1fc79 100644 --- a/core/commons/che-core-commons-j2ee/src/main/java/org/eclipse/che/filter/CheCacheDisablingFilter.java +++ b/core/commons/che-core-commons-j2ee/src/main/java/org/eclipse/che/filter/CheCacheDisablingFilter.java @@ -33,6 +33,7 @@ public class CheCacheDisablingFilter extends CacheDisablingFilter { private Set actionPatterns = new HashSet<>(); + @Override public void init(FilterConfig filterConfig) { Enumeration names = filterConfig.getInitParameterNames(); while (names.hasMoreElements()) { diff --git a/core/commons/che-core-commons-j2ee/src/main/java/org/eclipse/che/filter/CheCacheForcingFilter.java b/core/commons/che-core-commons-j2ee/src/main/java/org/eclipse/che/filter/CheCacheForcingFilter.java index a1a1c8ee678..245d757dea0 100644 --- a/core/commons/che-core-commons-j2ee/src/main/java/org/eclipse/che/filter/CheCacheForcingFilter.java +++ b/core/commons/che-core-commons-j2ee/src/main/java/org/eclipse/che/filter/CheCacheForcingFilter.java @@ -33,6 +33,7 @@ public class CheCacheForcingFilter extends CacheForcingFilter { private Set actionPatterns = new HashSet<>(); + @Override public void init(FilterConfig filterConfig) { Enumeration names = filterConfig.getInitParameterNames(); while (names.hasMoreElements()) { diff --git a/core/commons/che-core-commons-j2ee/src/test/java/org/eclipse/che/filter/CheCacheDisablingFilterTest.java b/core/commons/che-core-commons-j2ee/src/test/java/org/eclipse/che/filter/CheCacheDisablingFilterTest.java index f30acf1b7c2..1ad3b1e8ad5 100644 --- a/core/commons/che-core-commons-j2ee/src/test/java/org/eclipse/che/filter/CheCacheDisablingFilterTest.java +++ b/core/commons/che-core-commons-j2ee/src/test/java/org/eclipse/che/filter/CheCacheDisablingFilterTest.java @@ -107,23 +107,27 @@ public Object[][] cachedPathProvider() { private class MockFilterConfig implements FilterConfig { private final Map filterParams = new HashMap<>(); - public MockFilterConfig() { + MockFilterConfig() { this.filterParams.put("pattern1", "^.*\\.nocache\\..*$"); this.filterParams.put("pattern2", "^.*/_app/.*$"); } + @Override public String getFilterName() { return this.getClass().getName(); } + @Override public ServletContext getServletContext() { throw new UnsupportedOperationException("The method does not supported in " + this.getClass()); } + @Override public String getInitParameter(String key) { return this.filterParams.get(key); } + @Override public Enumeration getInitParameterNames() { return Collections.enumeration(filterParams.keySet()); } diff --git a/core/commons/che-core-commons-j2ee/src/test/java/org/eclipse/che/filter/CheCacheForcingFilterTest.java b/core/commons/che-core-commons-j2ee/src/test/java/org/eclipse/che/filter/CheCacheForcingFilterTest.java index c7aa4e2692e..7f19edbe832 100644 --- a/core/commons/che-core-commons-j2ee/src/test/java/org/eclipse/che/filter/CheCacheForcingFilterTest.java +++ b/core/commons/che-core-commons-j2ee/src/test/java/org/eclipse/che/filter/CheCacheForcingFilterTest.java @@ -108,23 +108,27 @@ public Object[][] nonCachedPathProvider() { private class MockFilterConfig implements FilterConfig { private final Map filterParams = new HashMap<>(); - public MockFilterConfig() { + MockFilterConfig() { this.filterParams.put("pattern1", "^.*\\.cache\\..*$"); this.filterParams.put("pattern2", "^.*/_app/.*$"); } + @Override public String getFilterName() { return this.getClass().getName(); } + @Override public ServletContext getServletContext() { throw new UnsupportedOperationException("The method does not supported in " + this.getClass()); } + @Override public String getInitParameter(String key) { return this.filterParams.get(key); } + @Override public Enumeration getInitParameterNames() { return Collections.enumeration(filterParams.keySet()); } diff --git a/dashboard/src/app/factories/create-factory/action/factory-action-box.controller.ts b/dashboard/src/app/factories/create-factory/action/factory-action-box.controller.ts new file mode 100644 index 00000000000..4166a997ff1 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/action/factory-action-box.controller.ts @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Defines controller of directive for displaying action box. + * @ngdoc controller + * @name factory.directive:FactoryActionBoxController + * @author Florent Benoit + */ +export class FactoryActionBoxController { + private $mdDialog: ng.material.IDialogService; + private actions: Array; + private selectedAction: string; + private factoryObject: any; + private lifecycle: any; + private onChange: Function; + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor($mdDialog: ng.material.IDialogService) { + this.$mdDialog = $mdDialog; + + this.actions = []; + this.actions.push({name : 'RunCommand', id: 'runcommand'}); + this.actions.push({name : 'openFile', id: 'openfile'}); + this.selectedAction = this.actions[0].id; + } + + /** + * Edit the action based on the provided index + * @param $event the mouse event + * @param index the index in the array of factory actions + */ + editAction($event: any, index: number): void { + let action = this.factoryObject.ide[this.lifecycle].actions[index]; + this.$mdDialog.show({ + targetEvent: $event, + controller: 'FactoryActionDialogEditController', + controllerAs: 'factoryActionDialogEditCtrl', + bindToController: true, + clickOutsideToClose: true, + locals: { + callbackController: this, + index: index, + // selectedAction: action + selectedValue: action.properties + }, + templateUrl: 'app/factories/create-factory/action/factory-action-edit.html' + }); + } + + /** + * Edit action callback. + * + * @param index the index in the array of factory actions + * @param newValue new value + */ + callbackEditAction(index: number, newValue: any): void { + this.factoryObject.ide[this.lifecycle].actions[index].properties = newValue; + + this.onChange(); + } + + addAction(): void { + if (!this.factoryObject.ide) { + this.factoryObject.ide = {}; + } + if (!this.factoryObject.ide[this.lifecycle]) { + this.factoryObject.ide[this.lifecycle] = {}; + this.factoryObject.ide[this.lifecycle].actions = []; + } + + let actionToAdd; + if ('openfile' === this.selectedAction) { + actionToAdd = { + "properties": { + "file": this.selectedParam + }, + "id": "openFile" + }; + } else if ('runcommand' === this.selectedAction) { + actionToAdd = { + "properties": { + "name": this.selectedParam + }, + "id": "runCommand" + }; + } + if (actionToAdd) { + this.factoryObject.ide[this.lifecycle].actions.push(actionToAdd); + } + + this.onChange(); + } + + /** + * Remove action based on the provided index + * @param index the index in the array of factory actions + */ + removeAction(index: number): void { + this.factoryObject.ide[this.lifecycle].actions.splice(index, 1); + + this.onChange(); + } +} diff --git a/dashboard/src/app/factories/create-factory/action/factory-action-box.directive.ts b/dashboard/src/app/factories/create-factory/action/factory-action-box.directive.ts new file mode 100644 index 00000000000..39a2f30990f --- /dev/null +++ b/dashboard/src/app/factories/create-factory/action/factory-action-box.directive.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Defines a directive for displaying action box. + * @author Florent Benoit + */ +export class FactoryActionBox { + private restrict: string; + private templateUrl: string; + private replace: boolean; + private controller: string; + private controllerAs: string; + private bindToController: boolean; + + private scope: { + [propName: string]: string; + }; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor() { + this.restrict = 'E'; + + this.templateUrl = 'app/factories/create-factory/action/factory-action-box.html'; + this.replace = false; + + this.controller = 'FactoryActionBoxController'; + this.controllerAs = 'factoryActionBoxCtrl'; + + this.bindToController = true; + + // scope values + this.scope = { + lifecycle: '@cdvyLifecycle', + actionTitle: '@?cdvyActionTitle', + callbackController: '=cdvyCallbackController', + factoryObject: '=cdvyFactoryObject', + onChange: '&cdvyOnChange' + }; + } + + +} diff --git a/dashboard/src/app/factories/create-factory/action/factory-action-box.html b/dashboard/src/app/factories/create-factory/action/factory-action-box.html new file mode 100644 index 00000000000..3293ba7cd7b --- /dev/null +++ b/dashboard/src/app/factories/create-factory/action/factory-action-box.html @@ -0,0 +1,56 @@ +
+
+
+
+ + + +
Param is required.
+
+
+ +
+ +
+ + +
+
+ {{action.id}} +
+
+ {{action.properties.name ? action.properties.name : action.properties.file}} +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
diff --git a/dashboard/src/app/factories/create-factory/action/factory-action-box.styl b/dashboard/src/app/factories/create-factory/action/factory-action-box.styl new file mode 100644 index 00000000000..03385cab279 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/action/factory-action-box.styl @@ -0,0 +1,57 @@ +.factory-actions-panel + margin-top -6px + + & > div + margin-bottom 10px + + .factory-actions-input + min-height 50px + height 50px + + .che-select + margin-top 4px + margin-right 20px + + .factory-actions-list + max-width 600px + + md-list + margin 0 + + div + outline none + + md-icon + font-size 18px + height auto + width auto + color $clear-foggy-sky-color + line-height 18px + + .factory-commands-widget-actions + display inline-block + line-height inherit + min-height 100% + cursor pointer + outline none + + span + line-height inherit + + .fa-times-circle::before, + .fa-edit::before + position relative + top 3px + + .factory-actions-row-action-name + min-width 220px + width 220px + + span + padding 2px + + .factory-actions-row-action-param + cursor pointer + +.factory-edit-action-input + margin-top -6px diff --git a/dashboard/src/app/factories/create-factory/action/factory-action-edit.controller.ts b/dashboard/src/app/factories/create-factory/action/factory-action-edit.controller.ts new file mode 100644 index 00000000000..dba1b9778e6 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/action/factory-action-edit.controller.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {FactoryActionBoxController} from './factory-action-box.controller'; + +/** + * @ngdoc controller + * @name factory.directive:FactoryActionDialogEditController + * @description This class is handling the controller for editing action of a factory + * @author Florent Benoit + */ +export class FactoryActionDialogEditController { + isName: boolean; + isFile: boolean; + selectedValue: { name: string; file: string }; + + private $mdDialog: ng.material.IDialogService; + private index: number; + private callbackController: FactoryActionBoxController; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor($mdDialog: ng.material.IDialogService) { + this.$mdDialog = $mdDialog; + + this.isName = angular.isDefined(this.selectedValue.name); + this.isFile = angular.isDefined(this.selectedValue.file); + } + + /** + * Callback of the edit button of the dialog. + */ + edit() { + this.$mdDialog.hide(); + this.callbackController.callbackEditAction(this.index, this.selectedValue); + } + + + /** + * Callback of the cancel button of the dialog. + */ + abort() { + this.$mdDialog.hide(); + } +} diff --git a/dashboard/src/app/factories/create-factory/action/factory-action-edit.html b/dashboard/src/app/factories/create-factory/action/factory-action-edit.html new file mode 100644 index 00000000000..8c426faec56 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/action/factory-action-edit.html @@ -0,0 +1,31 @@ + +
+
+ +
Param is required
+
+
+
+ +
Param is required
+
+
+ + + +
+
+ diff --git a/dashboard/src/app/factories/create-factory/command/factory-command-edit.controller.ts b/dashboard/src/app/factories/create-factory/command/factory-command-edit.controller.ts new file mode 100644 index 00000000000..ef99aa1d803 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/command/factory-command-edit.controller.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * @ngdoc controller + * @name factory.directive:FactoryCommandDialogEditController + * @description This class is handling the controller for editing command of a factory + * @author Florent Benoit + */ +export class FactoryCommandDialogEditController { + private $mdDialog: ng.material.IDialogService; + private callbackController: any; + private index: number; + private selectedValue: any; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor($mdDialog: ng.material.IDialogService) { + this.$mdDialog = $mdDialog; + } + + /** + * Callback of the edit button of the dialog. + */ + edit(): void { + this.$mdDialog.hide(); + this.callbackController.callbackEditAction(this.index, this.selectedValue); + } + + + /** + * Callback of the cancel button of the dialog. + */ + abort(): void { + this.$mdDialog.hide(); + } +} diff --git a/dashboard/src/app/factories/create-factory/command/factory-command-edit.html b/dashboard/src/app/factories/create-factory/command/factory-command-edit.html new file mode 100644 index 00000000000..ef56ec34ea9 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/command/factory-command-edit.html @@ -0,0 +1,18 @@ + +
+ +
Param is required
+
+ + + +
+
diff --git a/dashboard/src/app/factories/create-factory/command/factory-command.controller.ts b/dashboard/src/app/factories/create-factory/command/factory-command.controller.ts new file mode 100644 index 00000000000..42b0e1fd220 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/command/factory-command.controller.ts @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Defines controller of directive for displaying factory command. + * @ngdoc controller + * @name factory.directive:FactoryCommandController + * @author Florent Benoit + */ +export class FactoryCommandController { + private $mdDialog: ng.material.IDialogService; + private factoryObject: any; + private onChange: Function; + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor($mdDialog: ng.material.IDialogService) { + this.$mdDialog = $mdDialog; + } + + /** + * User clicked on the add button to add a new command + * @param $event + */ + addCommand(): void { + if (!this.factoryObject) { + this.factoryObject = {}; + } + + if (!this.factoryObject.workspace) { + this.factoryObject.workspace = {}; + } + + if (!this.factoryObject.workspace.commands) { + this.factoryObject.workspace.commands = []; + } + let command = { + "commandLine": this.commandLine, + "name": this.commandLineName, + "attributes": { + "previewUrl": "" + }, + "type": "custom" + }; + + this.factoryObject.workspace.commands.push(command); + + this.onChange(); + } + + /** + * Remove command based on the provided index + * @param index the index in the array of workspace commands + */ + removeCommand(index: number): void { + this.factoryObject.workspace.commands.splice(index, 1); + + this.onChange(); + } + + /** + * Edit the command based on the provided index + * @param $event the mouse event + * @param index the index in the array of workspace commands + */ + editCommand($event: any, index: number): void { + this.$mdDialog.show({ + targetEvent: $event, + controller: 'FactoryCommandDialogEditController', + controllerAs: 'factoryCommandDialogEditCtrl', + bindToController: true, + clickOutsideToClose: true, + locals: { + callbackController: this, + index: index, + selectedValue: this.factoryObject.workspace.commands[index].commandLine + }, + templateUrl: 'app/factories/create-factory/command/factory-command-edit.html' + }); + } + + /** + * Callback on edit action. + * + * @param index commands index + * @param newValue value to update with + */ + callbackEditAction(index: number, newValue: string) { + this.factoryObject.workspace.commands[index].commandLine = newValue; + + this.onChange(); + } +} diff --git a/dashboard/src/app/factories/create-factory/command/factory-command.directive.ts b/dashboard/src/app/factories/create-factory/command/factory-command.directive.ts new file mode 100644 index 00000000000..6dac01b972e --- /dev/null +++ b/dashboard/src/app/factories/create-factory/command/factory-command.directive.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Defines a directive for displaying factory commands. + * @author Florent Benoit + */ +export class FactoryCommand { + private restrict: string; + private templateUrl: string; + private replace: boolean; + private controller: string; + private controllerAs: string; + private bindToController: boolean; + + private scope: { + [propName: string]: string; + }; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor() { + this.restrict = 'E'; + + this.templateUrl = 'app/factories/create-factory/command/factory-command.html'; + this.replace = false; + + this.controller = 'FactoryCommandController'; + this.controllerAs = 'factoryCommandCtrl'; + + this.bindToController = true; + + // scope values + this.scope = { + factoryObject: '=cdvyFactoryObject', + onChange: '&cdvyOnChange' + }; + } +} diff --git a/dashboard/src/app/factories/create-factory/command/factory-command.html b/dashboard/src/app/factories/create-factory/command/factory-command.html new file mode 100644 index 00000000000..603c07a652f --- /dev/null +++ b/dashboard/src/app/factories/create-factory/command/factory-command.html @@ -0,0 +1,74 @@ +
+
+
+
+ +
A name is required.
+
Workspace name may contain digits, latin letters, _ , . , - and should start only + with digits, latin + letters or underscores +
+
The name has to be more than 1 character long.
+
The name has to be less than 20 characters long.
+
+ +
A name is required.
+
The name has to be more than 1 character long.
+
The name has to be less than 500 characters long.
+
+
+ +
+ +
+ + +
+
+ {{command.name}} +
+
+ {{command.commandLine}} +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
diff --git a/dashboard/src/app/factories/create-factory/command/factory-command.styl b/dashboard/src/app/factories/create-factory/command/factory-command.styl new file mode 100644 index 00000000000..5f1769343cd --- /dev/null +++ b/dashboard/src/app/factories/create-factory/command/factory-command.styl @@ -0,0 +1,53 @@ +.factory-commands-panel + margin-top -6px + + & > div + margin-bottom 10px + + .factory-commands-input + min-height 50px + height 50px + + .che-input + margin-right 20px + + .factory-commands-list + max-width 600px + + md-list + margin 0 + + div + outline none + + md-icon + font-size 18px + height auto + width auto + color $clear-foggy-sky-color + line-height 18px + + .factory-commands-widget-actions + display inline-block + line-height inherit + min-height 100% + cursor pointer + outline none + + span + line-height inherit + + .fa-times-circle::before, + .fa-edit::before + position relative + top 3px + + .factory-commands-row-command-name + min-width 220px + width 220px + + span + padding 2px + + .factory-commands-row-command + cursor pointer diff --git a/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.controller.ts b/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.controller.ts new file mode 100644 index 00000000000..d61e85b7bf5 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.controller.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheNotification} from '../../../../components/notification/che-notification.factory'; +import {CheAPI} from '../../../../components/api/che-api.factory'; + +/* global FileReader */ + +/** + * Controller for upload factory from the file. + * @author Oleksii Orel + */ +export class FactoryFromFileCtrl { + private cheAPI: CheAPI; + private cheNotification: CheNotification; + private uploader: any; + private isImporting: boolean; + private factoryContent: any; + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor($filter: ng.IFilterService, cheAPI: CheAPI, cheNotification: CheNotification, FileUploader: any) { + 'ngInject'; + + this.cheAPI = cheAPI; + this.cheNotification = cheNotification; + + // if you want select just one file, you won't need to clear the input + FileUploader.FileSelect.prototype.isEmptyAfterSelection = function () { + return true; + }; + + this.uploader = new FileUploader(); + + // settings + this.uploader.queueLimit = 1; // maximum count of files + this.uploader.autoUpload = true; // automatically upload files after adding them to the queue + this.uploader.removeAfterUpload = true; // automatically remove files from the queue after uploading + + this.isImporting = this.uploader.isUploading; + + var ctrl = this; + + // filters + this.uploader.filters.push({ + name: 'sizeFilter', + fn: (item: any) => { + // file must not be smaller then some size + let isValidSize = item.size > 0 && item.size < 500000; + + if (!isValidSize) { + ctrl.cheNotification.showError('File size error.'); + } + return isValidSize; + } + }); + + this.uploader.filters.push({ + name: 'typeFilter', + fn: (item: any) => { + // file must be json + let isValidItem = item.type === 'application/json' || item.type === ''; + + if (!isValidItem) { + ctrl.cheNotification.showError('File type error.'); + } + return isValidItem; + } + }); + + // callback + this.uploader.onAfterAddingFile = function (fileItem) { + let uploadedFileName = fileItem._file.name; + let reader = new FileReader(); + + reader.readAsText(fileItem._file); + reader.onload = function () { + try { + ctrl.factoryContent = $filter('json')(angular.fromJson(reader.result), 2); + ctrl.cheNotification.showInfo('Successfully loaded file\'s configuration ' + uploadedFileName + '.'); + } catch (e) { + // invalid JSON + ctrl.factoryContent = null; + ctrl.cheNotification.showError('Invalid JSON.'); + } + }; + reader.onerror = function (error: any) { + ctrl.cheNotification.showError(error.data.message ? error.data.message : 'Error reading file.'); + console.log('Error reading file'); + }; + }; + + this.factoryContent = null; + } + +} diff --git a/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.directive.ts b/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.directive.ts new file mode 100644 index 00000000000..e1eea304790 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.directive.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Defines a directive for configuring factory from file. + * @author Oleksii Orel + */ +export class FactoryFromFile { + private restrict: string; + private templateUrl: string; + private replace: boolean; + private controller: string; + private controllerAs: string; + private bindToController: boolean; + + private scope: { + [propName: string]: string; + }; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor() { + this.restrict = 'E'; + + this.templateUrl = 'app/factories/create-factory/config-file-tab/factory-from-file.html'; + this.replace = false; + + this.controller = 'FactoryFromFileCtrl'; + this.controllerAs = 'factoryFromFileCtrl'; + + this.bindToController = true; + + // scope values + this.scope = { + isImporting: '=cdvyIsImporting', + factoryContent: '=cdvyFactoryContent' + }; + } + + link($scope: ng.IScope, element: ng.IAugmentedJQuery) { + $scope.clickUpload = () => { + // search the input fields + let inputElements = element.find('input'); + inputElements.eq(0).click(); + }; + } + +} diff --git a/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.html b/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.html new file mode 100644 index 00000000000..4307c984109 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.html @@ -0,0 +1,31 @@ + +
+ +
+ + +
+
+
diff --git a/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.styl b/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.styl new file mode 100644 index 00000000000..0ae22507a95 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/config-file-tab/factory-from-file.styl @@ -0,0 +1,15 @@ +.factory-from-file + min-height 80px + +.factory-from-file label + font-size inherit + +.factory-from-file input + display none + +.factory-from-file button + box-shadow 0 2px 5px 0 $disabled-color + border-radius 2px !important + padding-right 20px + padding-left 20px + font-size 1.5em diff --git a/dashboard/src/app/factories/create-factory/create-factory-config.ts b/dashboard/src/app/factories/create-factory/create-factory-config.ts new file mode 100644 index 00000000000..ee990bce394 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/create-factory-config.ts @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + + +import {CreateFactoryCtrl} from '../create-factory/create-factory.controller'; + +import {FactoryFromWorkspaceCtrl} from '../create-factory/workspaces-tab/factory-from-workpsace.controller'; +import {FactoryFromWorkspace} from '../create-factory/workspaces-tab/factory-from-workspace.directive'; +import {FactoryFromFileCtrl} from '../create-factory/config-file-tab/factory-from-file.controller'; +import {FactoryFromFile} from '../create-factory/config-file-tab/factory-from-file.directive'; +import {FactoryFromTemplateCtrl} from '../create-factory/template-tab/factory-from-template.controller'; +import {FactoryFromTemplate} from '../create-factory/template-tab/factory-from-template.directive'; +import {FactoryActionBoxController} from './action/factory-action-box.controller'; +import {FactoryActionBox} from './action/factory-action-box.directive'; +import {FactoryActionDialogEditController} from './action/factory-action-edit.controller'; +import {FactoryCommandController} from './command/factory-command.controller'; +import {FactoryCommand} from './command/factory-command.directive'; +import {FactoryCommandDialogEditController} from './command/factory-command-edit.controller'; +import {CreateFactoryGitController} from './git/create-factory-git.controller'; +import {CreateFactoryGit} from './git/create-factory-git.directive'; + +export class CreateFactoryConfig { + + constructor(register: che.IRegisterService) { + + register.controller('CreateFactoryCtrl', CreateFactoryCtrl); + + register.controller('FactoryFromWorkspaceCtrl', FactoryFromWorkspaceCtrl); + register.directive('cdvyFactoryFromWorkspace', FactoryFromWorkspace); + + register.controller('FactoryFromFileCtrl', FactoryFromFileCtrl); + register.directive('cdvyFactoryFromFile', FactoryFromFile); + + register.controller('FactoryFromTemplateCtrl', FactoryFromTemplateCtrl); + register.directive('cdvyFactoryFromTemplate', FactoryFromTemplate); + + register.controller('FactoryActionBoxController', FactoryActionBoxController); + register.directive('cdvyFactoryActionBox', FactoryActionBox); + + register.controller('FactoryCommandController', FactoryCommandController); + register.directive('cdvyFactoryCommand', FactoryCommand); + + register.controller('CreateFactoryGitController', CreateFactoryGitController); + register.directive('cdvyCreateFactoryGit', CreateFactoryGit); + + register.controller('FactoryActionDialogEditController', FactoryActionDialogEditController); + register.controller('FactoryCommandDialogEditController', FactoryCommandDialogEditController); + + + + // config routes + register.app.config(($routeProvider: any) => { + $routeProvider.accessWhen('/factories/create-factory', { + title: 'New Factory', + templateUrl: 'app/factories/create-factory/create-factory.html', + controller: 'CreateFactoryCtrl', + controllerAs: 'createFactoryCtrl' + }); + + }); + + } +} diff --git a/dashboard/src/app/factories/create-factory/create-factory.controller.ts b/dashboard/src/app/factories/create-factory/create-factory.controller.ts new file mode 100644 index 00000000000..f56df52b5d4 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/create-factory.controller.ts @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheAPI} from '../../../components/api/che-api.factory'; +import {CheNotification} from '../../../components/notification/che-notification.factory'; + +/** + * Controller for a create factory. + * @author Oleksii Orel + * @author Florent Benoit + */ +export class CreateFactoryCtrl { + private $location: ng.ILocationService; + private $log: ng.ILogService; + private cheAPI: CheAPI; + private cheNotification: CheNotification; + private lodash: _.LoDashStatic; + private $filter: ng.IFilterService; + private $document: ng.IDocumentService; + private isLoading: boolean; + private isImporting: boolean; + private stackRecipeMode: string; + private factoryContent: any; + private factoryObject: any; + private form: any; + private name: string; + private factoryId: string; + private factoryLink: string; + private factoryBadgeUrl: string; + private markdown: string; + + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor($location: ng.ILocationService, cheAPI: CheAPI, $log: ng.ILogService, cheNotification: CheNotification, $scope: ng.IScope, + $filter: ng.IFilterService, lodash: _.LoDashStatic, $document: ng.IDocumentService) { + this.$location = $location; + this.cheAPI = cheAPI; + this.$log = $log; + this.cheNotification = cheNotification; + this.$filter = $filter; + this.lodash = lodash; + this.$document = $document; + + this.isLoading = false; + this.isImporting = false; + + this.stackRecipeMode = 'current-recipe'; + + this.factoryContent = null; + + $scope.$watch('createFactoryCtrl.factoryObject', () => { + this.factoryContent = this.$filter('json')(angular.fromJson(this.factoryObject)); + }, true); + + $scope.$watch('createFactoryCtrl.gitLocation', (newValue: string) => { + // update underlying model + // updating first project item + if (!this.factoryObject) { + let templateName = 'git'; + let promise = this.cheAPI.getFactoryTemplate().fetchFactoryTemplate(templateName); + + promise.then(() => { + let factoryContent = this.cheAPI.getFactoryTemplate().getFactoryTemplate(templateName); + this.factoryObject = angular.fromJson(factoryContent); + this.updateGitProjectLocation(newValue); + }); + } else { + this.updateGitProjectLocation(newValue); + } + + }, true); + } + + /** + * Clear factory content + */ + clearFactoryContent(): void { + this.factoryContent = null; + } + + setForm(form: any): void { + this.form = form; + } + + isFormInvalid(): boolean { + return this.form ? this.form.$invalid: false; + } + + /** + * Update the source project location for git + * @param location the new location + */ + updateGitProjectLocation(location: string): void { + let project = this.factoryObject.workspace.projects[0]; + project.source.type = 'git'; + project.source.location = location; + } + + /** + * Create a new factory by factory content + * @param factoryContent + */ + createFactoryByContent(factoryContent: any): void { + if (!factoryContent) { + return; + } + + // try to set factory name + try { + let factoryObject = angular.fromJson(factoryContent); + factoryObject.name = this.name; + factoryContent = angular.toJson(factoryObject); + } catch (e) { + this.$log.error(e); + } + + this.isImporting = true; + + let promise = this.cheAPI.getFactory().createFactoryByContent(factoryContent); + + promise.then((factory: che.IFactory) => { + this.isImporting = false; + + this.lodash.find(factory.links, (link: any) => { + if (link.rel === 'accept' || link.rel === 'accept-named') { + this.factoryLink = link.href; + } + }); + + var parser = this.$document[0].createElement('a'); + parser.href = this.factoryLink; + this.factoryId = factory.id; + this.factoryBadgeUrl = parser.protocol + '//' + parser.hostname + '/factory/resources/codenvy-contribute.svg'; + + this.markdown = '[![Contribute](' + this.factoryBadgeUrl + ')](' + this.factoryLink + ')'; + }, (error: any) => { + this.isImporting = false; + this.cheNotification.showError(error.data.message ? error.data.message : 'Create factory failed.'); + this.$log.error(error); + }).then(() => { + this.finishFlow(); + }); + } + + /* + * Flow of creating a factory is finished, we can redirect to details of factory + */ + finishFlow(): void { + this.clearFactoryContent(); + this.$location.path('/factory/' + this.factoryId); + } + +} diff --git a/dashboard/src/app/factories/create-factory/create-factory.html b/dashboard/src/app/factories/create-factory/create-factory.html new file mode 100644 index 00000000000..7141c94e3b9 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/create-factory.html @@ -0,0 +1,117 @@ + + + + + +
+ + + + +
+ +
A name is required.
+
Factory name may contain digits, latin letters, spaces, _ , . , - and should start + only + with digits, latin letters or underscores +
+
The name has to be more than 3 characters long.
+
The name has to be less than 20 characters long.
+
+
+
+
+ + + + + + + + Workspace + + + + + + + + + Git + + +
+ +
+
+
+ + + + Config + + +
+ +
+
+
+ + + + Template + + +
+ +
+
+
+
+
+ + + +
+
diff --git a/dashboard/src/app/factories/create-factory/create-factory.styl b/dashboard/src/app/factories/create-factory/create-factory.styl new file mode 100644 index 00000000000..982ee198e0e --- /dev/null +++ b/dashboard/src/app/factories/create-factory/create-factory.styl @@ -0,0 +1,94 @@ +md-content .create-factory + overflow auto + min-height calc(100vh - 152px) + background-color $white-color !important + padding 0 14px + + button + margin-left 0 + margin-right 0 + margin-bottom 16px + + .che-label-container-content .create-factory-input + margin -6px 0 + +md-progress-linear.create-factory-progress + z-index 1 + height 10px + position absolute + +.create-factory .factory-configuration-panel p + font-weight bold + margin-bottom 20px + color $red-lipstick-color + +.create-factory md-tabs-content-wrapper + display inline + +che-button-primary#create-factory-next-button button + margin 10px 0 100px + font-size 1.2em + width 100% !important + +.create-factory-share-header-widget + border-top 1px solid $primary-color + box-shadow-simple() + padding-left 24px + padding-right 24px + padding-bottom 0px + min-height 80px !important + max-height 80px !important + background-color $background-color + z-index 4 + +.create-factory-share-header-widget-badge + margin-left 20px + margin-right 20px + +.create-factory-share-header-widget-markdown + text-align center + height 18px + font-size 0.9em + margin-right 15px + padding-left 10px + padding-right 10px + border 1px solid $label-info-color + border-radius 2px + +.create-factory-share-header-widget-clipboard + font-size 1.3em + +.create-factory-share-header-widget-icon + font-size 3em + color $default-dark-color + margin-right 20px + +md-card.create-factory-factory-information-card + margin 24px + background-color $white-color + font-size 1.1em + font-family sans-serif + +md-tabs.factory-select-source-details + margin -15px 0 + +md-tabs.factory-select-source-details:not(.md-no-tab-content):not(.md-dynamic-height) + min-height 448px + +md-tabs.factory-select-source-details md-tab-content > div:first-child + height 100% !important + + +.create-factory-share-header-small-button + margin-right 15px + padding 0 15px + font-size 9pt + cursor pointer + height 18px + background-color $primary-color + color $white-color + border-radius 2px + +.create-factory-share-header-small-button:hover + color $white-color + text-decoration none diff --git a/dashboard/src/app/factories/create-factory/git/create-factory-git.controller.ts b/dashboard/src/app/factories/create-factory/git/create-factory-git.controller.ts new file mode 100644 index 00000000000..090114ca0fa --- /dev/null +++ b/dashboard/src/app/factories/create-factory/git/create-factory-git.controller.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * This class is handling the controller for the git part of Factory + * @ngdoc controller + * @name factory.directive:CreateFactoryGitController + * @author Florent Benoit + */ +export class CreateFactoryGitController { + + + +} diff --git a/dashboard/src/app/factories/create-factory/git/create-factory-git.directive.ts b/dashboard/src/app/factories/create-factory/git/create-factory-git.directive.ts new file mode 100644 index 00000000000..4acad839a97 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/git/create-factory-git.directive.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Defines a directive for creating factory from git. + * @author Florent Benoit + */ +export class CreateFactoryGit { + private restrict: string; + private templateUrl: string; + private controller: string; + private controllerAs: string; + private bindToController: boolean; + + private scope: { + [propName: string]: string; + }; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor() { + this.controller = 'CreateFactoryGitController'; + this.controllerAs = 'createFactoryGitCtrl'; + this.bindToController = true; + + this.restrict = 'E'; + this.templateUrl = 'app/factories/create-factory/git/create-factory-git.html'; + + + // scope values + this.scope = { + location: '=cdvyGitLocation' + }; + } + +} diff --git a/dashboard/src/app/factories/create-factory/git/create-factory-git.html b/dashboard/src/app/factories/create-factory/git/create-factory-git.html new file mode 100644 index 00000000000..11161638b4e --- /dev/null +++ b/dashboard/src/app/factories/create-factory/git/create-factory-git.html @@ -0,0 +1,28 @@ + + +
+
+ +
Invalid Git URL
+
A repository URL is required.
+
+
+
+
diff --git a/dashboard/src/app/factories/create-factory/git/create-factory-git.styl b/dashboard/src/app/factories/create-factory/git/create-factory-git.styl new file mode 100644 index 00000000000..0aa7ec30976 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/git/create-factory-git.styl @@ -0,0 +1,2 @@ +.create-factory-git-input + margin -6px 0 diff --git a/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.controller.ts b/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.controller.ts new file mode 100644 index 00000000000..34d26815206 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.controller.ts @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheAPI} from '../../../../components/api/che-api.factory'; +import {CheNotification} from '../../../../components/notification/che-notification.factory'; + +/** + * Controller for creating factory from a template. + * @author Oleksii Orel + */ +export class FactoryFromTemplateCtrl { + private $filter: ng.IFilterService; + private cheAPI: CheAPI; + private cheNotification: CheNotification; + private isImporting: boolean; + private factoryContent: any; + private editorOptions: any; + private templateName: string; + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor($filter: ng.IFilterService, cheAPI: CheAPI, cheNotification: CheNotification, $timeout: ng.ITimeoutService) { + this.$filter = $filter; + this.cheAPI = cheAPI; + this.cheNotification = cheNotification; + + this.isImporting = false; + this.factoryContent = null; + this.templateName = 'minimal'; + this.getFactoryTemplate(this.templateName); + + this.editorOptions = { + mode: 'application/json', + onLoad: (editor: any) => { + $timeout(() => { + editor.refresh(); + }, 1000); + } + }; + } + + // gets factory template. + getFactoryTemplate(templateName: string) { + let factoryContent = this.cheAPI.getFactoryTemplate().getFactoryTemplate(templateName); + + if (factoryContent) { + this.factoryContent = this.$filter('json')(factoryContent, 2); + return; + } + + this.isImporting = true; + + // fetch it: + let promise = this.cheAPI.getFactoryTemplate().fetchFactoryTemplate(templateName); + + promise.then((factoryContent: any) => { + this.isImporting = false; + this.factoryContent = this.$filter('json')(factoryContent, 2); + }, (error: any) => { + this.isImporting = false; + this.cheNotification.showError(error.data.message ? error.data.message : 'Fail to get factory template.'); + }); + } + +} diff --git a/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.directive.ts b/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.directive.ts new file mode 100644 index 00000000000..6e1bc5b60d9 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.directive.ts @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Defines a directive for displaying factory from template widget. + * @author Oleksii Orel + */ +export class FactoryFromTemplate { + private restrict: string; + private templateUrl: string; + private controller: string; + private controllerAs: string; + private bindToController: boolean; + private replace: boolean; + + private scope: { + [propName: string]: string; + }; + + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor() { + this.restrict = 'E'; + + this.templateUrl = 'app/factories/create-factory/template-tab/factory-from-template.html'; + this.replace = false; + + this.controller = 'FactoryFromTemplateCtrl'; + this.controllerAs = 'factoryFromTemplateCtrl'; + + this.bindToController = true; + + // scope values + this.scope = { + factoryContent: '=cdvyFactoryContent', + isImporting: '=cdvyIsImporting' + }; + } + +} diff --git a/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.html b/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.html new file mode 100644 index 00000000000..398994a1fa8 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.html @@ -0,0 +1,38 @@ + +
+ +
+ + + + + + +
+
+
diff --git a/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.styl b/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.styl new file mode 100644 index 00000000000..7fc040b4846 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/template-tab/factory-from-template.styl @@ -0,0 +1,6 @@ +.factory-from-template + min-height 80px + +.factory-from-template .CodeMirror + border 1px solid $list-separator-color + min-height 500px diff --git a/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workpsace.controller.ts b/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workpsace.controller.ts new file mode 100644 index 00000000000..146a4d502f5 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workpsace.controller.ts @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheAPI} from '../../../../components/api/che-api.factory'; +import {CheNotification} from '../../../../components/notification/che-notification.factory'; + +/** + * Controller for creating factory from a workspace. + * @author Oleksii Orel + * @author Michail Kuznyetsov + */ +export class FactoryFromWorkspaceCtrl { + private $filter: ng.IFilterService; + private cheAPI: CheAPI; + private cheNotification: CheNotification; + private workspaces: Array; + private workspacesById: Map; + private filtersWorkspaceSelected: any; + private workspaceFilter: any; + private isLoading: boolean; + private isImporting: boolean; + private factoryContent: any; + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor($filter: ng.IFilterService, cheAPI: CheAPI, cheNotification: CheNotification) { + this.$filter = $filter; + this.cheAPI = cheAPI; + this.cheNotification = cheNotification; + + this.workspaces = cheAPI.getWorkspace().getWorkspaces(); + this.workspacesById = cheAPI.getWorkspace().getWorkspacesById(); + + this.filtersWorkspaceSelected = {}; + + this.workspaceFilter = {config: {name: ''}}; + + this.isLoading = true; + + // fetch workspaces when initializing + let promise = cheAPI.getWorkspace().fetchWorkspaces(); + + promise.then(() => { + this.isLoading = false; + this.updateData(); + }, (error: any) => { + this.isLoading = false; + if (error.status === 304) { + this.updateData(); + } + }); + + } + + updateData(): void { + this.setAllFiltersWorkspaces(true); + } + + /** + * Get factory content from workspace + * @param workspace is selected workspace + */ + getFactoryContentFromWorkspace(workspace: che.IWorkspace) { + let factoryContent = this.cheAPI.getFactory().getFactoryContentFromWorkspace(workspace); + if (factoryContent) { + this.factoryContent = this.$filter('json')(factoryContent, 2); + return; + } + + this.isImporting = true; + + let promise = this.cheAPI.getFactory().fetchFactoryContentFromWorkspace(workspace); + + promise.then((factoryContent: any) => { + this.isImporting = false; + this.factoryContent = this.$filter('json')(factoryContent, 2); + }, (error: any) => { + let message = (error.data && error.data.message) ? error.data.message : 'Get factory configuration failed.' + if (error.status === 400) { + message = 'Factory can\'t be created. The selected workspace has no projects defined. Project sources must be available from an external storage.'; + } + + this.isImporting = false; + this.factoryContent = null; + this.cheNotification.showError(message); + }); + } + + /** + * Set all workspaces in the filters of workspaces + * @param isChecked is setting value + */ + setAllFiltersWorkspaces(isChecked: boolean) { + this.workspaces.forEach((workspace: che.IWorkspace) => { + this.filtersWorkspaceSelected[workspace.id] = isChecked; + }); + } + + /** + * Get the workspace name by ID + * @param workspaceId + * @returns {String} workspace name + */ + getWorkspaceName(workspaceId: string) { + let workspace = this.workspacesById.get(workspaceId); + if (workspace && workspace.config.name) { + return workspace.config.name; + } + return ''; + } +} diff --git a/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.directive.ts b/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.directive.ts new file mode 100644 index 00000000000..1a46e66a78c --- /dev/null +++ b/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.directive.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Defines a directive for configuring factory form workspace. + * @author Oleksii Orel + */ +export class FactoryFromWorkspace { + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor() { + this.restrict = 'E'; + + this.templateUrl = 'app/factories/create-factory/workspaces-tab/factory-from-workspace.html'; + this.replace = false; + + this.controller = 'FactoryFromWorkspaceCtrl'; + this.controllerAs = 'factoryFromWorkspaceCtrl'; + + this.bindToController = true; + + // scope values + this.scope = { + isLoading: '=cdvyIsLoading', + isImporting: '=cdvyIsImporting', + factoryContent: '=cdvyFactoryContent' + }; + } + +} diff --git a/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.html b/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.html new file mode 100644 index 00000000000..cfe781d0a00 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.html @@ -0,0 +1,50 @@ + + + + {{factoryFromWorkspaceCtrl.getWorkspaceName(workspaceId)}} + No workspaces found + + + +
+
+
+ +
+
+ +
+
+
+
diff --git a/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.styl b/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.styl new file mode 100644 index 00000000000..61667166021 --- /dev/null +++ b/dashboard/src/app/factories/create-factory/workspaces-tab/factory-from-workspace.styl @@ -0,0 +1,88 @@ +.factory-from-workspace-search + margin-top 20px + +.factory-from-workspace che-search + color $label-primary-color + +.factory-from-workspace che-search .search-component + border-bottom 1px solid $disabled-color + +.factory-from-workspace che-search .search-input + color $label-primary-color + +.factory-from-workspace che-search md-icon + font-size 14px + +.factory-from-workspace che-search .search-input::-webkit-input-placeholder + color $label-primary-color + font-style italic + font-size 14px + opacity 0.5 + +.factory-from-workspace che-search .search-input:-moz-placeholder + /* Mozilla Firefox 4 to 18 */ + color $label-primary-color + font-style italic + font-size 14px + opacity 0.5 + +.factory-from-workspace che-search .search-input::-moz-placeholder + /* Mozilla Firefox 19+ */ + color $label-primary-color + font-style italic + font-size 14px + opacity 0.5 + +.factory-from-project-workspace-filter .workspace-selector md-checkbox:after + che-separator() + margin-top 17px + margin-left 10px + position relative + +.factory-from-project-workspace-filter .workspace-selector:last-child md-checkbox:after + display none + +.factory-from-project-workspace-filter .greyed .md-icon + background-color $disabled-color !important + +.factory-from-workspace .workspace-list-static-icon + flex 0 0 86px + text-align center + +.factory-from-workspace .workspace-list-static-icon project-type-icon + margin 0 24px + padding 10px 0 + width 36px + height 36px + line-height 36px + font-size 36px + color $label-secondary-color + +.factory-from-workspace md-icon + width auto + height auto + font-size 24px + line-height 24px + color $label-info-color + +.factory-from-workspace .workspace-icon, +.factory-from-workspace .workspace-icon:hover, +.factory-from-workspace .workspace-icon md-icon, +.factory-from-workspace .workspace-icon md-icon:hover + color $label-secondary-color + +.factory-from-workspace .project-list-row + outline none + margin-left 0 + margin-right 0 + cursor pointer + min-height 56px + +.factory-from-workspace .project-name + font-size 16px + line-height 19px + margin-bottom 3px + color $label-primary-color + +.factory-from-workspace .project-list-row:focus + background-color $focus-on-list-color diff --git a/dashboard/src/app/factories/factories-config.ts b/dashboard/src/app/factories/factories-config.ts new file mode 100644 index 00000000000..c30bb3a8817 --- /dev/null +++ b/dashboard/src/app/factories/factories-config.ts @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + + +import {FactoryDetailsConfig} from './factory-details/factory-details-config'; +import {CreateFactoryConfig} from './create-factory/create-factory-config'; +import {LastFactoriesConfig} from './last-factories/last-factories-config'; +import {ListFactoriesController} from './list-factories/list-factories.controller'; +import {FactoryItemController} from './list-factories/factory-item/factory-item.controller'; +import {CheFactoryItem} from './list-factories/factory-item/factory-item.directive'; +import {LoadFactoryController} from './load-factory/load-factory.controller'; +import {LoadFactoryService} from './load-factory/load-factory.service'; + +export class FactoryConfig { + + constructor(register: che.IRegisterService) { + register.controller('ListFactoriesController', ListFactoriesController); + + register.controller('FactoryItemController', FactoryItemController); + register.directive('cdvyFactoryItem', CheFactoryItem); + + register.controller('LoadFactoryController', LoadFactoryController); + register.service('loadFactoryService', LoadFactoryService); + + // config routes + register.app.config(function ($routeProvider) { + $routeProvider.accessWhen('/factories', { + title: 'Factories', + templateUrl: 'app/factories/list-factories/list-factories.html', + controller: 'ListFactoriesController', + controllerAs: 'listFactoriesCtrl' + }) + .accessWhen('/load-factory', { + title: 'Load Factory', + templateUrl: 'app/factories/load-factory/load-factory.html', + controller: 'LoadFactoryController', + controllerAs: 'loadFactoryController' + }) + .accessWhen('/load-factory/:id', { + title: 'Load Factory', + templateUrl: 'app/factories/load-factory/load-factory.html', + controller: 'LoadFactoryController', + controllerAs: 'loadFactoryController' + }); + + }); + + // config files + new FactoryDetailsConfig(register); + new CreateFactoryConfig(register); + new LastFactoriesConfig(register); + } +} + diff --git a/dashboard/src/app/factories/factory-details/factory-details-config.ts b/dashboard/src/app/factories/factory-details/factory-details-config.ts new file mode 100644 index 00000000000..1b7c0ee99b8 --- /dev/null +++ b/dashboard/src/app/factories/factory-details/factory-details-config.ts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +import {FactoryDetailsController} from '../factory-details/factory-details.controller'; +import {InformationTabConfig} from './information-tab/information-tab-config'; + + +export class FactoryDetailsConfig { + + constructor(register: che.IRegisterService) { + register.controller('FactoryDetailsController', FactoryDetailsController); + + // config routes + register.app.config(($routeProvider: any) => { + let locationProvider = { + title: 'Factory', + templateUrl: 'app/factories/factory-details/factory-details.html', + controller: 'FactoryDetailsController', + controllerAs: 'factoryDetailsController' + }; + + $routeProvider.accessWhen('/factory/:id', locationProvider) + .accessWhen('/factory/:id/:tabName', locationProvider); + + }); + + // config files + new InformationTabConfig(register); + } +} diff --git a/dashboard/src/app/factories/factory-details/factory-details.controller.ts b/dashboard/src/app/factories/factory-details/factory-details.controller.ts new file mode 100644 index 00000000000..e98cf92c28b --- /dev/null +++ b/dashboard/src/app/factories/factory-details/factory-details.controller.ts @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheNotification} from '../../../components/notification/che-notification.factory'; +import {CheFactory} from '../../../components/api/che-factory.factory'; + +/** + * Controller for a factory details. + * @author Florent Benoit + */ +export class FactoryDetailsController { + private cheFactory: CheFactory; + private factory: che.IFactory; + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor($route: ng.route.IRouteService, cheFactory: CheFactory, cheNotification: CheNotification) { + 'ngInject'; + + this.cheFactory = cheFactory; + let factoryId = $route.current.params.id; + this.factory = this.cheFactory.getFactoryById(factoryId); + + cheFactory.fetchFactoryById(factoryId).then((factory: che.IFactory) => { + this.factory = factory; + }, (error: any) => { + cheNotification.showError(error.data.message ? error.data.message : 'Get factory failed.'); + }); + } + + /** + * Returns the factory url based on id. + * @returns {link.href|*} link value + */ + getFactoryIdUrl(): string { + if (!this.factory) { + return null; + } + return this.cheFactory.getFactoryIdUrl(this.factory); + } +} + diff --git a/dashboard/src/app/factories/factory-details/factory-details.html b/dashboard/src/app/factories/factory-details/factory-details.html new file mode 100644 index 00000000000..86ac0421526 --- /dev/null +++ b/dashboard/src/app/factories/factory-details/factory-details.html @@ -0,0 +1,31 @@ + + + + + + diff --git a/dashboard/src/app/factories/factory-details/factory-details.styl b/dashboard/src/app/factories/factory-details/factory-details.styl new file mode 100644 index 00000000000..96bc1c72c4b --- /dev/null +++ b/dashboard/src/app/factories/factory-details/factory-details.styl @@ -0,0 +1,20 @@ +md-content.factory-details + overflow auto + min-height calc(100vh - 152px) + +md-progress-linear.factory-details-progress + z-index 1 + height 10px + position absolute + +.factory-details-content + background-color $white-color !important + padding 0 14px + + button + margin-left 0 + margin-right 0 + margin-bottom 16px + + .factory-delete-label label + color $che-delete-label-color diff --git a/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts new file mode 100644 index 00000000000..ad93bd87c58 --- /dev/null +++ b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheAPI} from '../../../../../components/api/che-api.factory'; +import {CheNotification} from '../../../../../components/notification/che-notification.factory'; + +/** + * Controller for a factory information. + * @author Oleksii Orel + */ +export class FactoryInformationController { + + private confirmDialogService: any; + private cheAPI: CheAPI; + private cheNotification: CheNotification; + private $location: ng.ILocationService; + private $log: ng.ILogService; + private $timeout: ng.ITimeoutService; + private lodash: _.LoDashStatic; + private $filter: ng.IFilterService; + + private timeoutPromise: ng.IPromise; + private editorLoadedPromise: ng.IPromise; + private editorOptions: any; + private factoryInformationForm: any; + private stackRecipeMode: string; + private factory: che.IFactory; + private copyOriginFactory: che.IFactory; + private factoryContent: any; + private workspaceImportedRecipe: any; + private environmentName: string; + private workspaceName: string; + private stackId: string; + private workspaceConfig: any; + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor($scope: ng.IScope, cheAPI: CheAPI, cheNotification: CheNotification, $location: ng.ILocationService, $log: ng.ILogService, + $timeout: ng.ITimeoutService, lodash: _.LoDashStatic, $filter: ng.IFilterService, $q: ng.IQService, confirmDialogService: any) { + this.cheAPI = cheAPI; + this.cheNotification = cheNotification; + this.$location = $location; + this.$log = $log; + this.$timeout = $timeout; + this.lodash = lodash; + this.$filter = $filter; + this.confirmDialogService = confirmDialogService; + + this.timeoutPromise = null; + $scope.$on('$destroy', () => { + if (this.timeoutPromise) { + $timeout.cancel(this.timeoutPromise); + } + }); + + let editorLoadedDefer = $q.defer(); + this.editorLoadedPromise = editorLoadedDefer.promise; + this.editorOptions = { + onLoad: ((instance: any) => { + editorLoadedDefer.resolve(instance); + }) + }; + + this.stackRecipeMode = 'current-recipe'; + + this.updateData(); + $scope.$watch(() => { + return this.factory; + }, () => { + this.updateData(); + }); + } + + /** + * Update factory content data for editor + */ + updateData(): void { + if (!this.factory) { + return; + } + + this.workspaceName = this.factory.workspace.name; + this.environmentName = this.factory.workspace.defaultEnv; + + this.copyOriginFactory = angular.copy(this.factory); + if (this.copyOriginFactory.links) { + delete this.copyOriginFactory.links; + } + + let factoryContent = this.$filter('json')(this.copyOriginFactory); + if (factoryContent !== this.factoryContent) { + if (!this.factoryContent) { + this.editorLoadedPromise.then((instance) => { + this.$timeout(() => { + instance.refresh(); + }, 500); + }); + } + this.factoryContent = factoryContent; + } + } + + /** + * Returns object's attributes. + * + * @param targetObject object to process + * @returns {string[]} + */ + getObjectKeys(targetObject: any): Array { + return Object.keys(targetObject); + } + + /** + * Returns the factory's data changed state. + * + * @returns {boolean} + */ + isFactoryChanged(): boolean { + if (!this.copyOriginFactory) { + return false; + } + + let testFactory = angular.copy(this.factory); + if (testFactory.links) { + delete testFactory.links; + } + + return angular.equals(this.copyOriginFactory, testFactory) !== true; + } + + /** + * Update factory data. + */ + updateFactory(): void { + this.factoryContent = this.$filter('json')(this.copyOriginFactory); + + if (this.factoryInformationForm.$invalid || !this.isFactoryChanged()) { + return; + } + + this.$timeout.cancel(this.timeoutPromise); + this.timeoutPromise = this.$timeout(() => { + this.doUpdateFactory(this.copyOriginFactory); + }, 500); + } + + /** + * Returns the factory url based on id. + * + * @returns {link.href|*} link value + */ + getFactoryIdUrl(): string { + return this.cheAPI.getFactory().getFactoryIdUrl(this.factory); + } + + /** + * Returns the factory url based on name. + * + * @returns {link.href|*} link value + */ + getFactoryNamedUrl(): string { + return this.cheAPI.getFactory().getFactoryNamedUrl(this.factory); + } + + /** + * Callback to update factory + */ + doUpdateFactory(factory: che.IFactory): void { + let promise = this.cheAPI.getFactory().setFactory(factory); + + promise.then((factory: che.IFactory) => { + this.factory = factory; + this.cheNotification.showInfo('Factory information successfully updated.'); + }, (error: any) => { + this.cheNotification.showError(error.data.message ? error.data.message : 'Update factory failed.'); + this.$log.log(error); + }); + } + + /** + * Handler for factory editor focus event. + */ + factoryEditorOnFocus(): void { + if (this.timeoutPromise) { + this.$timeout.cancel(this.timeoutPromise); + this.doUpdateFactory(this.copyOriginFactory); + } + } + + /** + * Resets factory editor. + */ + factoryEditorReset(): void { + this.factoryContent = this.$filter('json')(this.copyOriginFactory, 2); + } + + /** + * Updates factory's content. + */ + updateFactoryContent(): void { + let promise = this.cheAPI.getFactory().setFactoryContent(this.factory.id, this.factoryContent); + + promise.then((factory: che.IFactory) => { + this.factory = factory; + this.cheNotification.showInfo('Factory information successfully updated.'); + }, (error: any) => { + this.factoryContent = this.$filter('json')(this.copyOriginFactory, 2); + this.cheNotification.showError(error.data.message ? error.data.message : 'Update factory failed.'); + this.$log.error(error); + }); + } + + /** + * Deletes factory with confirmation. + */ + deleteFactory(): void { + let content = 'Please confirm removal for the factory \'' + (this.factory.name ? this.factory.name : this.factory.id) + '\'.'; + let promise = this.confirmDialogService.showConfirmDialog('Remove the factory', content, 'Delete'); + + promise.then(() => { + // remove it ! + let promise = this.cheAPI.getFactory().deleteFactoryById(this.factory.id); + promise.then(() => { + this.$location.path('/factories'); + }, (error: any) => { + this.cheNotification.showError(error.data.message ? error.data.message : 'Delete failed.'); + this.$log.log(error); + }); + }); + } + + /** + * Returns the recipe value of the environment. + * + * @returns {any} + */ + getRecipe(): string { + if (this.copyOriginFactory && this.copyOriginFactory.workspace) { + let environement = this.copyOriginFactory.workspace.environments[this.copyOriginFactory.workspace.defaultEnv]; + return environement.recipe.location || environement.recipe.content; + } + return null; + } + + /** + * Handles stack and workspace config changes. + * + * @param config workspace config + * @param stackId stack id + */ + onWorkspaceStackChanged(config: any, stackId: string): void { + this.stackId = stackId; + this.workspaceConfig = config; + } + + /** + * Saves stacks changes in workspace config inside factory. + */ + saveStack(): void { + if (!this.copyOriginFactory) { + return; + } + this.copyOriginFactory.workspace.environments[this.factory.workspace.defaultEnv] = this.workspaceConfig.environments[this.workspaceConfig.defaultEnv]; + + this.updateFactory(); + } +} diff --git a/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.directive.ts b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.directive.ts new file mode 100644 index 00000000000..3ab3f6f3249 --- /dev/null +++ b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.directive.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Defines a directive for displaying factory-information widget. + * @author Oleksii Orel + */ +export class FactoryInformation { + private restrict: string; + private templateUrl: string; + private replace: boolean; + private controller: string; + private controllerAs: string; + private bindToController: boolean; + + private scope: { + [propName: string]: string; + }; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor() { + this.restrict = 'E'; + + this.templateUrl = 'app/factories/factory-details/information-tab/factory-information/factory-information.html'; + this.replace = false; + + this.controller = 'FactoryInformationController'; + this.controllerAs = 'factoryInformationController'; + + this.bindToController = true; + + // scope values + this.scope = { + factory: '=cdvyFactory' + }; + } + +} diff --git a/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.html b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.html new file mode 100644 index 00000000000..c5c93560ba6 --- /dev/null +++ b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.html @@ -0,0 +1,204 @@ + +
+ + +
+ + +
A name is required.
+
Factory name may contain digits, latin letters, spaces, _ , . , - and should start + only + with digits, latin letters or underscores +
+
The name has to be more than 3 characters long.
+
The name has to be less than 20 characters long.
+
+
+
+
+ + + + + + + + + + + + + + + + + Use current recipe +
+
+ {{factoryInformationController.getRecipe()}} +
+
+ none +
+
+ Configure recipe from a stack +
+
+ + +
+
+ + + + + +
+ +
A name is required.
+
Workspace name may contain digits, latin letters, _ , . , - and should start + only with digits, latin + letters or underscores +
+
The name has to be more than 3 characters long.
+
The name has to be less than 20 characters long.
+
+
+
+
+ + + +
+ ENVIRONMENT: {{environmentKey}} + +
+
+
+ MACHINE: {{machineKey}} + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
+ +
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + + + + + +
diff --git a/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.styl b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.styl new file mode 100644 index 00000000000..e31136d7198 --- /dev/null +++ b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.styl @@ -0,0 +1,40 @@ +.factory-information-panel-item:not(:last-child) + margin-bottom 10px + +.factory-information .che-label-container-content .factory-information-input + margin-top -6px + +.factory-information .disabled-state + cursor not-allowed + background-color transparent + +.factory-information + overflow auto + min-height calc(100vh - 152px) + background-color $white-color !important + padding 0 14px + + button + margin 0 30px 0 0 + + .che-label-container-content .factory-information-input + margin-top -6px + + .factory-information-update-button button + margin-right 0 + + .json-editor + font-size 12px + margin-bottom 20px + + .workspace-environment-name + div + margin-top 8px + + .workspace-machine + margin-left 45px + + .che-ram-allocation-slider .slider-wrapper + margin-bottom -15px + + .save-stack-button button + margin-top 20px diff --git a/dashboard/src/app/factories/factory-details/information-tab/information-tab-config.ts b/dashboard/src/app/factories/factory-details/information-tab/information-tab-config.ts new file mode 100644 index 00000000000..9878c2b2edb --- /dev/null +++ b/dashboard/src/app/factories/factory-details/information-tab/information-tab-config.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +import {FactoryInformationController} from '../information-tab/factory-information/factory-information.controller'; +import {FactoryInformation} from '../information-tab/factory-information/factory-information.directive'; + + +export class InformationTabConfig { + + constructor(register: che.IRegisterService) { + register.controller('FactoryInformationController', FactoryInformationController); + register.directive('cdvyFactoryInformation', FactoryInformation); + } +} diff --git a/dashboard/src/app/factories/last-factories/last-factories-config.ts b/dashboard/src/app/factories/last-factories/last-factories-config.ts new file mode 100644 index 00000000000..b6a943628fd --- /dev/null +++ b/dashboard/src/app/factories/last-factories/last-factories-config.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +import {LastFactoriesController} from './last-factories.controller'; +import {LastFactories} from './last-factories.directive'; + +export class LastFactoriesConfig { + + constructor(register: che.IRegisterService) { + register.controller('LastFactoriesController', LastFactoriesController); + register.directive('cdvyLastFactories', LastFactories); + } +} diff --git a/dashboard/src/app/factories/last-factories/last-factories.controller.ts b/dashboard/src/app/factories/last-factories/last-factories.controller.ts new file mode 100644 index 00000000000..27f52a6dc34 --- /dev/null +++ b/dashboard/src/app/factories/last-factories/last-factories.controller.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheFactory} from '../../../components/api/che-factory.factory'; + +/** + * @ngdoc controller + * @name factories.controller:LastFactoriesController + * @description This class is handling the controller of the last factories to display in the dashboard + * @author Oleksii Orel + */ +export class LastFactoriesController { + + private cheFactory: CheFactory; + private factories: Array; + private factoriesOrderBy: string; + private maxItems: number; + private isLoading: boolean; + + + /** + * Default constructor + * @ngInject for Dependency injection + */ + constructor(cheFactory: CheFactory) { + this.cheFactory = cheFactory; + + this.factories = this.cheFactory.getPageFactories(); + + // todo we should change to modificationDate after model's change + this.factoriesOrderBy = '-creator.created'; + this.maxItems = 5; + + // todo add OrderBy to condition in fetch API + let promise = this.cheFactory.fetchFactories(this.maxItems, 0); + + this.isLoading = true; + promise.finally(() => { + this.isLoading = false; + this.updateFactories(); + }); + } + + /** + * Update factories array + */ + updateFactories(): void { + this.factories = this.cheFactory.getPageFactories(); + } + + /** + * Returns the list of factories. + * + * @returns {Array} + */ + getFactories(): Array { + return this.factories; + } +} diff --git a/dashboard/src/app/factories/last-factories/last-factories.directive.ts b/dashboard/src/app/factories/last-factories/last-factories.directive.ts new file mode 100644 index 00000000000..c05c69728e8 --- /dev/null +++ b/dashboard/src/app/factories/last-factories/last-factories.directive.ts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * @ngdoc directive + * @name factories.directive:LastFactories + * @description This class is handling the directive of the listing last opened factories + * @author Oleksii Orel + */ +export class LastFactories { + private restrict: string; + private templateUrl: string; + private replace: boolean; + private controller: string; + private controllerAs: string; + private bindToController: boolean; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor() { + this.restrict = 'E'; + this.templateUrl = 'app/factories/last-factories/last-factories.html'; + this.replace = false; + this.controller = 'LastFactoriesController'; + this.controllerAs = 'lastFactoriesController'; + this.bindToController = true; + } +} diff --git a/dashboard/src/app/factories/last-factories/last-factories.html b/dashboard/src/app/factories/last-factories/last-factories.html new file mode 100644 index 00000000000..84e84174372 --- /dev/null +++ b/dashboard/src/app/factories/last-factories/last-factories.html @@ -0,0 +1,40 @@ + + + + + +
+
+ +
+ + No factories found + + + + +
+
diff --git a/dashboard/src/app/factories/last-factories/last-factories.styl b/dashboard/src/app/factories/last-factories/last-factories.styl new file mode 100644 index 00000000000..4d5287c09a8 --- /dev/null +++ b/dashboard/src/app/factories/last-factories/last-factories.styl @@ -0,0 +1,3 @@ +.last-factories-empty-label + margin-left 20px + color $label-info-color diff --git a/dashboard/src/app/factories/list-factories/factory-item/factory-item.controller.ts b/dashboard/src/app/factories/list-factories/factory-item/factory-item.controller.ts new file mode 100644 index 00000000000..bcd537c5dde --- /dev/null +++ b/dashboard/src/app/factories/list-factories/factory-item/factory-item.controller.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheFactory} from '../../../../components/api/che-factory.factory'; +import {CheEnvironmentRegistry} from '../../../../components/api/environment/che-environment-registry.factory'; + +/** + * Controller for a factory item. + * @author Oleksii Orel + */ +export class FactoryItemController { + private $location: ng.ILocationService; + private cheFactory: CheFactory; + private cheEnvironmentRegistry: CheEnvironmentRegistry; + private lodash: _.LoDashStatic; + private factory: che.IFactory; + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor($location: ng.ILocationService, cheFactory: CheFactory, cheEnvironmentRegistry: CheEnvironmentRegistry, lodash: _.LoDashStatic) { + this.$location = $location; + this.cheFactory = cheFactory; + this.cheEnvironmentRegistry = cheEnvironmentRegistry; + this.lodash = lodash; + } + + /** + * Returns the list of factory links. + * + * @returns {Array} + */ + getFactoryLinks(): Array { + return this.cheFactory.detectLinks(this.factory); + } + + /** + * Redirect to factory details. + */ + redirectToFactoryDetails(): void { + this.$location.path('/factory/' + this.factory.id); + } + + /** + * Returns display value of memory limit. + * + * @returns {string} display value of memory limit + */ + getMemoryLimit(): string { + if (!this.factory.workspace) { + return '-'; + } + + let defaultEnvName = this.factory.workspace.defaultEnv; + let environment = this.factory.workspace.environments[defaultEnvName]; + + let recipeType = environment.recipe.type; + let environmentManager = this.cheEnvironmentRegistry.getEnvironmentManager(recipeType); + let machines = environmentManager.getMachines(environment); + + let limits = this.lodash.pluck(machines, 'attributes.memoryLimitBytes'); + let total = 0; + limits.forEach((limit: number) => { + if (limit) { + total += limit / (1024 * 1024); + } + }); + + return (total > 0) ? total + ' MB' : '-'; + } +} + diff --git a/dashboard/src/app/factories/list-factories/factory-item/factory-item.directive.ts b/dashboard/src/app/factories/list-factories/factory-item/factory-item.directive.ts new file mode 100644 index 00000000000..b830ac7e0c9 --- /dev/null +++ b/dashboard/src/app/factories/list-factories/factory-item/factory-item.directive.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Defines a directive for factory item in list. + * @author Oleksii Orel + */ +export class CheFactoryItem { + restrict: string = 'E'; + + templateUrl: string = 'app/factories/list-factories/factory-item/factory-item.html'; + replace = false; + + controller: string = 'FactoryItemController'; + controllerAs: string = 'factoryItemController'; + + bindToController: boolean = true; + + // we require ngModel as we want to use it inside our directive + require: Array = ['ngModel']; + scope: { + [propName: string]: string; + }; + + /** + * Default constructor. + */ + constructor() { + this.scope = { + factory: '=cdvyFactory', + isChecked: '=cdvyChecked', + isSelectable: '=cdvyIsSelectable', + isSelect: '=?ngModel', + onCheckboxClick: '&?cdvyOnCheckboxClick' + }; + + } + +} diff --git a/dashboard/src/app/factories/list-factories/factory-item/factory-item.html b/dashboard/src/app/factories/list-factories/factory-item/factory-item.html new file mode 100644 index 00000000000..ab69f0cf112 --- /dev/null +++ b/dashboard/src/app/factories/list-factories/factory-item/factory-item.html @@ -0,0 +1,59 @@ + + +
+
+ +
+
+
+ Factory + {{factoryItemController.factory.name ? factoryItemController.factory.name : factoryItemController.getFactoryLinks()[0]}} +
+
+ RAM + {{factoryItemController.getMemoryLimit()}} +
+
+ Actions + + + + + +
+
+
+
diff --git a/dashboard/src/app/factories/list-factories/factory-item/factory-item.styl b/dashboard/src/app/factories/list-factories/factory-item/factory-item.styl new file mode 100644 index 00000000000..7a35e4646e1 --- /dev/null +++ b/dashboard/src/app/factories/list-factories/factory-item/factory-item.styl @@ -0,0 +1,8 @@ +.che-list-item-row .factory-consumed-value + line-height initial + font-size 12px + font-weight bold + color $che-medium-blue-color + +.che-list-actions .factory-action + min-width 16px diff --git a/dashboard/src/app/factories/list-factories/list-factories.controller.ts b/dashboard/src/app/factories/list-factories/list-factories.controller.ts new file mode 100644 index 00000000000..06449542e47 --- /dev/null +++ b/dashboard/src/app/factories/list-factories/list-factories.controller.ts @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {ConfirmDialogService} from '../../../components/service/confirm-dialog/confirm-dialog.service'; +import {CheAPI} from "../../../components/api/che-api.factory"; +import {CheNotification} from "../../../components/notification/che-notification.factory"; + +/** + * Controller for the factories. + * @author Florent Benoit + * @author Oleksii Orel + */ +export class ListFactoriesController { + + private confirmDialogService: ConfirmDialogService; + private cheAPI: CheAPI; + private cheNotification: CheNotification; + private $q: ng.IQService; + private $log: ng.ILogService; + + private maxItems: number; + private skipCount: number; + + private factoriesOrderBy: string; + private factoriesFilter: any; + private factoriesSelectedStatus: any; + private isNoSelected: boolean; + private isAllSelected: boolean; + private isBulkChecked: boolean; + + private isLoading: boolean; + private factories: any; + private pagesInfo: any; + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor($q: ng.IQService, $log: ng.ILogService, cheAPI: CheAPI, cheNotification: CheNotification, $rootScope: che.IRootScopeService, + confirmDialogService: ConfirmDialogService) { + this.$q = $q; + this.$log = $log; + this.cheAPI = cheAPI; + this.cheNotification = cheNotification; + this.confirmDialogService = confirmDialogService; + + this.maxItems = 15; + this.skipCount = 0; + + this.factoriesOrderBy = ''; + this.factoriesFilter = {name: ''}; + this.factoriesSelectedStatus = {}; + this.isNoSelected = true; + this.isAllSelected = false; + this.isBulkChecked = false; + + this.isLoading = true; + this.factories = cheAPI.getFactory().getPageFactories(); + + let promise = cheAPI.getFactory().fetchFactories(this.maxItems, this.skipCount); + promise.then(() => { + this.isLoading = false; + }, (error: any) => { + this.isLoading = false; + if (error.status !== 304) { + this.cheNotification.showError(error.data && error.data.message ? error.data.message : 'Failed to retrieve the list of factories.'); + } + }); + + this.pagesInfo = cheAPI.getFactory().getPagesInfo(); + + $rootScope.showIDE = false; + } + + /** + * Make all factories in list checked. + */ + selectAllFactories(): void { + this.factories.forEach((factory: che.IFactory) => { + this.factoriesSelectedStatus[factory.id] = true; + }); + } + + /** + * Make all factories in list unchecked. + */ + deselectAllFactories(): any { + this.factories.forEach((factory: che.IFactory) => { + this.factoriesSelectedStatus[factory.id] = false; + }); + } + + /** + * Change bulk selection value. + */ + changeBulkSelection(): void { + if (this.isBulkChecked) { + this.deselectAllFactories(); + this.isBulkChecked = false; + } else { + this.selectAllFactories(); + this.isBulkChecked = true; + } + this.updateSelectedStatus(); + } + + /** + * Update factories selected status. + */ + updateSelectedStatus(): void { + this.isNoSelected = true; + this.isAllSelected = true; + + this.factories.forEach((factory: che.IFactory) => { + if (this.factoriesSelectedStatus[factory.id]) { + this.isNoSelected = false; + } else { + this.isAllSelected = false; + } + }); + + if (this.isNoSelected) { + this.isBulkChecked = false; + return; + } + + if (this.isAllSelected) { + this.isBulkChecked = true; + } + } + + /** + * Delete all selected factories + */ + deleteSelectedFactories(): void { + let factoriesSelectedStatusKeys = Object.keys(this.factoriesSelectedStatus); + let checkedFactoriesKeys = []; + + if (!factoriesSelectedStatusKeys.length) { + this.cheNotification.showError('No such factories.'); + return; + } + + factoriesSelectedStatusKeys.forEach((key: string) => { + if (this.factoriesSelectedStatus[key] === true) { + checkedFactoriesKeys.push(key); + } + }); + + let numberToDelete = checkedFactoriesKeys.length; + if (!numberToDelete) { + this.cheNotification.showError('No such factory.'); + return; + } + + let confirmationPromise = this.showDeleteFactoriesConfirmation(numberToDelete); + + confirmationPromise.then(() => { + let isError = false; + let deleteFactoryPromises = []; + + checkedFactoriesKeys.forEach((factoryId: string) => { + this.factoriesSelectedStatus[factoryId] = false; + + let promise = this.cheAPI.getFactory().deleteFactoryById(factoryId); + + promise.then(() => { + }, (error: any) => { + isError = true; + this.$log.error('Cannot delete factory: ', error); + }); + deleteFactoryPromises.push(promise); + }); + + this.$q.all(deleteFactoryPromises).finally(() => { + this.isLoading = true; + + let promise = this.cheAPI.getFactory().fetchFactories(this.maxItems, this.skipCount); + + promise.then(() => { + this.isLoading = false; + }, (error) => { + this.isLoading = false; + if (error.status !== 304) { + this.cheNotification.showError(error.data.message ? error.data.message : 'Update information failed.'); + } + }); + + if (isError) { + this.cheNotification.showError('Delete failed.'); + } else { + this.cheNotification.showInfo('Selected ' + (numberToDelete === 1 ? 'factory' : 'factories') + ' has been removed.'); + } + }); + }); + } + + /** + * Ask for loading the users page in asynchronous way + * @param pageKey - the key of page + */ + fetchFactoriesPage(pageKey: string): void { + this.isLoading = true; + let promise = this.cheAPI.getFactory().fetchFactoryPage(pageKey); + + promise.then(() => { + this.isLoading = false; + }, (error) => { + this.isLoading = false; + if (error.status !== 304) { + this.cheNotification.showError(error.data && error.data.message ? error.data.message : 'Update information failed.'); + } + }); + } + + /** + * Returns true if the next page is exist. + * @returns {boolean} + */ + hasNextPage(): boolean { + if (this.pagesInfo.countOfPages) { + return this.pagesInfo.currentPageNumber < this.pagesInfo.countOfPages; + } + return this.factories.length === this.maxItems; + } + + /** + * Returns true if the last page is exist. + * @returns {boolean} + */ + hasLastPage(): boolean { + if (this.pagesInfo.countOfPages) { + return this.pagesInfo.currentPageNumber < this.pagesInfo.countOfPages; + } + return false; + } + + /** + * Returns true if the previous page is exist. + * @returns {boolean} + */ + hasPreviousPage(): boolean { + return this.pagesInfo.currentPageNumber > 1; + } + + /** + * Returns true if we have more then one page. + * @returns {boolean} + */ + isPagination(): boolean { + if (this.pagesInfo.countOfPages) { + return this.pagesInfo.countOfPages > 1; + } + return this.factories.length === this.maxItems || this.pagesInfo.currentPageNumber > 1; + } + + /** + * Show confirmation popup before delete + * @param numberToDelete {number} + * @returns {ng.IPromise} + */ + showDeleteFactoriesConfirmation(numberToDelete: number): ng.IPromise { + let content = 'Would you like to delete '; + if (numberToDelete > 1) { + content += 'these ' + numberToDelete + ' factories?'; + } else { + content += 'this selected factory?'; + } + return this.confirmDialogService.showConfirmDialog('Remove factories', content, 'Delete'); + } +} diff --git a/dashboard/src/app/factories/list-factories/list-factories.html b/dashboard/src/app/factories/list-factories/list-factories.html new file mode 100644 index 00000000000..882e82e9b25 --- /dev/null +++ b/dashboard/src/app/factories/list-factories/list-factories.html @@ -0,0 +1,108 @@ + + + + Factories enable workspace automation and are packaged as a consumer-friendly URL. Create new Factories to onboard your team, or integrate + with your toolchain. + + + + + +
+
+
+ +
+
+
+ + + +
+
+
+ + + +
+ + << + + + < + + + {{listFactoriesCtrl.pagesInfo.currentPageNumber}} + + + > + + + >> + +
+
+ + No factories found. + + There are no factories. +
+
+
diff --git a/dashboard/src/app/factories/load-factory/load-factory.controller.ts b/dashboard/src/app/factories/load-factory/load-factory.controller.ts new file mode 100644 index 00000000000..af6f058d55b --- /dev/null +++ b/dashboard/src/app/factories/load-factory/load-factory.controller.ts @@ -0,0 +1,689 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheAPI} from '../../../components/api/che-api.factory'; +import {LoadFactoryService} from './load-factory.service'; +import {CheNotification} from '../../../components/notification/che-notification.factory'; +import {RouteHistory} from '../../../components/routing/route-history.service'; + +/** + * This class is handling the controller for the factory loading. + * @author Ann Shumilova + */ +export class LoadFactoryController { + private cheAPI: CheAPI; + private $websocket: ng.websocket.IWebSocketProvider; + private $timeout: ng.ITimeoutService; + private $mdDialog: ng.material.IDialogService; + private loadFactoryService: LoadFactoryService; + private lodash: _.LoDashStatic; + private cheNotification: CheNotification; + private $location: ng.ILocationService; + private routeHistory: RouteHistory; + private $window: ng.IWindowService; + private routeParams: any; + + private workspaces: Array; + private workspace: che.IWorkspace; + private projectsToImport: number; + + private websocketReconnect: number; + + private factory: che.IFactory; + + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor(cheAPI: CheAPI, $websocket: ng.websocket.IWebSocketProvider, $route: ng.route.IRouteService, $timeout: ng.ITimeoutService, + $mdDialog: ng.material.IDialogService, loadFactoryService: LoadFactoryService, lodash: _.LoDashStatic, cheNotification: CheNotification, + $location: ng.ILocationService, routeHistory: RouteHistory, $window: ng.IWindowService) { + this.cheAPI = cheAPI; + this.$websocket = $websocket; + this.$timeout = $timeout; + this.$mdDialog = $mdDialog; + this.loadFactoryService = loadFactoryService; + this.lodash = lodash; + this.cheNotification = cheNotification; + this.$location = $location; + this.routeHistory = routeHistory; + this.$window = $window; + + this.workspaces = []; + this.workspace = {}; + + this.websocketReconnect = 50; + + this.hideMenuAndFooter(); + + this.loadFactoryService.resetLoadProgress(); + this.loadFactoryService.setLoadFactoryInProgress(true); + + this.routeParams = $route.current.params; + this.getFactoryData(); + } + + /** + * Hides menu and footer to maximize view. + */ + hideMenuAndFooter(): void { + angular.element(document.querySelectorAll('[id*=navmenu]')).hide(); + angular.element(document.querySelectorAll('.che-footer')).hide(); + } + + /** + * Restores the menu and footer. + */ + restoreMenuAndFooter(): void { + angular.element(document.querySelectorAll('[id*=navmenu]')).show(); + angular.element(document.querySelectorAll('.che-footer')).show(); + } + + /** + * Retrieves factory data. + */ + getFactoryData(): void { + let promise; + if (this.routeParams.id) { + this.factory = this.cheAPI.getFactory().getFactoryById(this.routeParams.id); + promise = this.cheAPI.getFactory().fetchFactoryById(this.routeParams.id); + } else if (this.routeParams) { + promise = this.processFactoryParameters(this.routeParams); + } else { + this.getLoadingSteps()[this.getCurrentProgressStep()].hasError = true; + this.getLoadingSteps()[this.getCurrentProgressStep()].logs = 'Required parameters for loading factory are not there.'; + } + if (promise) { + promise.then((factory: che.IFactory) => { + this.factory = factory; + + // check factory polices: + if (!this.checkPolicies(this.factory)) { + return; + } + + // check factory contains workspace config: + if (!this.factory.workspace) { + this.getLoadingSteps()[this.getCurrentProgressStep()].hasError = true; + this.getLoadingSteps()[this.getCurrentProgressStep()].logs = 'Factory has no workspace config.'; + } else { + this.fetchWorkspaces(); + } + }, (error: any) => { + this.handleError(error); + }); + } + } + + /** + * Processes factory parameters. + * + * @param parameters + * @returns {any} + */ + processFactoryParameters(parameters: any): ng.IPromise { + // user name and factory name should be handled differently: + if (parameters.name || parameters.user) { + if (Object.keys(parameters).length === 2) { + return this.processUser(parameters.user, parameters.name); + } else { + let paramName = parameters.name ? 'Factory name' : 'User name'; + this.getLoadingSteps()[this.getCurrentProgressStep()].logs = 'Invalid factory URL. ' + paramName + ' is missed or misspelled.'; + this.getLoadingSteps()[this.getCurrentProgressStep()].hasError = true; + return null; + } + } + + return this.cheAPI.getFactory().fetchParameterFactory(parameters); + } + + /** + * Processes factory's user. Checks user with such name exists. + * + * @param name user name + * @param factoryName + * @returns {IPromise>} + */ + processUser(name: string, factoryName: string): ng.IPromise { + return this.cheAPI.getUser().fetchUserByName(name).then((user: che.IUser) => { + return this.cheAPI.getFactory().fetchFactoryByName(factoryName, user.id); + }, (error: any) => { + this.getLoadingSteps()[this.getCurrentProgressStep()].logs = 'Invalid factory URL. User with name ' + name + ' does not exist.'; + this.getLoadingSteps()[this.getCurrentProgressStep()].hasError = true; + return null; + }); + } + + /** + * Checks factory's policies. + * + * @param factory factory to be checked + * @returns {boolean} true if factory policies validation has passed + */ + checkPolicies(factory: che.IFactory): boolean { + if (!factory.policies || !factory.policies.referer) { + return true; + } + // process referrer: + let factoryReferrer = factory.policies.referer; + let referrer = document.referrer; + if (referrer && (referrer.indexOf(factoryReferrer) >= 0)) { + return true; + } else { + this.getLoadingSteps()[this.getCurrentProgressStep()].logs = 'Factory referrer policy does not match the current one.'; + this.getLoadingSteps()[this.getCurrentProgressStep()].hasError = true; + return false; + } + } + + /** + * Handles pointed error - prints it on the proper screen. + * + * @param error error to be handled + */ + handleError(error: any): void { + if (error.data.message) { + this.getLoadingSteps()[this.getCurrentProgressStep()].logs = error.data.message; + this.cheNotification.showError(error.data.message); + } + this.getLoadingSteps()[this.getCurrentProgressStep()].hasError = true; + } + + /** + * Detect workspace to start: create new one or get created one. + */ + getWorkspaceToStart(): void { + let createPolicy = (this.factory.policies) ? this.factory.policies.create : 'perClick'; + var workspace = null; + switch (createPolicy) { + case 'perUser' : + workspace = this.lodash.find(this.workspaces, (w: che.IWorkspace) => { + return this.factory.id === w.attributes.factoryId; + }); + break; + case 'perAccount' : + // todo when account is ready + workspace = this.lodash.find(this.workspaces, (w: che.IWorkspace) => { + return this.factory.workspace.name === w.config.name; + }); + break; + case 'perClick' : + break; + } + + if (workspace) { + this.startWorkspace(workspace); + } else { + this.createWorkspace(); + } + } + + /** + * Fetches workspaces. + */ + fetchWorkspaces(): any { + this.loadFactoryService.goToNextStep(); + + let promise = this.cheAPI.getWorkspace().fetchWorkspaces(); + promise.then(() => { + this.workspaces = this.cheAPI.getWorkspace().getWorkspaces(); + this.getWorkspaceToStart(); + }, () => { + this.workspaces = this.cheAPI.getWorkspace().getWorkspaces(); + this.getWorkspaceToStart(); + }); + } + + /** + * Create workspace from factory config. + */ + createWorkspace(): any { + let config = this.factory.workspace; + // set factory attribute: + let attrs = {factoryId: this.factory.id}; + config.name = this.getWorkspaceName(config.name); + + // todo: fix account when ready: + let creationPromise = this.cheAPI.getWorkspace().createWorkspaceFromConfig(null, config, attrs); + creationPromise.then((data: any) => { + this.$timeout(() => { + this.startWorkspace(data); + }, 1000); + }, (error: any) => { + this.handleError(error); + }); + } + + /** + * Get workspace name by detecting the existing names + * and generate new name if necessary. + * + * @param name workspace name + * @returns {string} generated name + */ + getWorkspaceName(name: string): string { + if (this.workspaces.length === 0) { + return name; + } + let existingNames = this.lodash.pluck(this.workspaces, 'config.name'); + + if (existingNames.indexOf(name) < 0) { + return name; + } + + let generatedName = name; + let counter = 1; + while (existingNames.indexOf(generatedName) >= 0) { + generatedName = name + '_' + counter++; + } + return generatedName; + } + + /** + * Checks workspace status and starts it if necessary, + * + * @param workspace workspace to process + */ + startWorkspace(workspace: che.IWorkspace): void { + this.workspace = workspace; + var bus = this.cheAPI.getWebsocket().getBus(); + + if (workspace.status === 'RUNNING') { + this.loadFactoryService.setCurrentProgressStep(4); + this.importProjects(bus); + return; + } + + this.subscribeOnEvents(workspace, bus); + + this.$timeout(() => { + this.doStartWorkspace(workspace); + }, 2000); + } + + /** + * Performs workspace start. + * + * @param workspace + */ + doStartWorkspace(workspace: che.IWorkspace): void { + let startWorkspacePromise = this.cheAPI.getWorkspace().startWorkspace(workspace.id, workspace.config.defaultEnv); + this.loadFactoryService.goToNextStep(); + + startWorkspacePromise.then((data: any) => { + console.log('Workspace started', data); + }, (error) => { + let errorMessage; + + if (!error || !error.data) { + errorMessage = 'This factory is unable to start a new workspace.'; + } else if (error.data.errorCode === 10000 && error.data.attributes) { + let attributes = error.data.attributes; + + errorMessage = 'This factory is unable to start a new workspace.' + + ' Your running workspaces are consuming ' + + attributes.used_ram + attributes.ram_unit + ' RAM.' + + ' Your current RAM limit is ' + attributes.limit_ram + attributes.ram_unit + + '. This factory requested an additional ' + + attributes.required_ram + attributes.ram_unit + '.' + + ' You can stop other workspaces to free resources.'; + } else { + errorMessage = error.data.message; + } + + this.handleError({data: {message: errorMessage}}); + }); + } + + subscribeOnEvents(data: any, bus: any): void { + // get channels + let statusLink = this.lodash.find(data.links, (link: any) => { + return link.rel === 'environment.status_channel'; + }); + + let outputLink = this.lodash.find(data.links, (link: any) => { + return link.rel === 'environment.output_channel'; + }); + + let workspaceId = data.id; + + let agentChannel = 'workspace:' + data.id + ':ext-server:output'; + let statusChannel = statusLink ? statusLink.parameters[0].defaultValue : null; + let outputChannel = outputLink ? outputLink.parameters[0].defaultValue : null; + + bus.subscribe(outputChannel, (message: any) => { + message = this.getDisplayMachineLog(message); + if (this.getLoadingSteps()[this.getCurrentProgressStep()].logs.length > 0) { + this.getLoadingSteps()[this.getCurrentProgressStep()].logs = this.getLoadingSteps()[this.getCurrentProgressStep()].logs + '\n' + message; + } else { + this.getLoadingSteps()[this.getCurrentProgressStep()].logs = message; + } + }); + + // for now, display log of status channel in case of errors + bus.subscribe(statusChannel, (message: any) => { + if (message.eventType === 'DESTROYED' && message.workspaceId === data.id) { + this.getLoadingSteps()[this.getCurrentProgressStep()].hasError = true; + + // need to show the error + this.$mdDialog.show( + this.$mdDialog.alert() + .title('Unable to start workspace') + .content('Unable to start workspace. It may be linked to OutOfMemory or the container has been destroyed') + .ariaLabel('Workspace start') + .ok('OK') + ); + } + if (message.eventType === 'ERROR' && message.workspaceId === data.id) { + this.getLoadingSteps()[this.getCurrentProgressStep()].hasError = true; + // need to show the error + this.$mdDialog.show( + this.$mdDialog.alert() + .title('Error when starting workspace') + .content('Unable to start workspace. Error when trying to start the workspace: ' + message.error) + .ariaLabel('Workspace start') + .ok('OK') + ); + } + console.log('Status channel of workspaceID', workspaceId, message); + }); + + // subscribe to workspace events + bus.subscribe('workspace:' + workspaceId, (message: any) => { + + if (message.eventType === 'ERROR' && message.workspaceId === workspaceId) { + // need to show the error + this.$mdDialog.show( + this.$mdDialog.alert() + .title('Error when starting agent') + .content('Unable to start workspace agent. Error when trying to start the workspace agent: ' + message.error) + .ariaLabel('Workspace agent start') + .ok('OK') + ); + this.getLoadingSteps()[this.getCurrentProgressStep()].hasError = true; + } + + if (message.eventType === 'RUNNING' && message.workspaceId === workspaceId) { + this.finish(); + } + }); + + bus.subscribe(agentChannel, (message: any) => { + let agentStep = 3; + if (this.loadFactoryService.getCurrentProgressStep() < agentStep) { + this.loadFactoryService.setCurrentProgressStep(agentStep); + } + + if (this.getLoadingSteps()[agentStep].logs.length > 0) { + this.getLoadingSteps()[agentStep].logs = this.getLoadingSteps()[agentStep].logs + '\n' + message; + } else { + this.getLoadingSteps()[agentStep].logs = message; + } + }); + + } + + /** + * Gets the log to be displayed per machine. + * + * @param log origin log content + * @returns {*} parsed log + */ + getDisplayMachineLog(log: any): string { + log = angular.fromJson(log); + if (angular.isObject(log)) { + return '[' + log.machineName + '] ' + log.content; + } else { + return log; + } + } + + /** + * Performs importing projects. + * + * @param bus + */ + importProjects(bus: any): void { + let promise = this.cheAPI.getWorkspace().fetchWorkspaceDetails(this.workspace.id); + promise.then(() => { + let projects = this.cheAPI.getWorkspace().getWorkspacesById().get(this.workspace.id).config.projects; + this.detectProjectsToImport(projects, bus); + }, (error: any) => { + if (error.status !== 304) { + let projects = this.cheAPI.getWorkspace().getWorkspacesById().get(this.workspace.id).config.projects; + this.detectProjectsToImport(projects, bus); + } else { + this.handleError(error); + } + }); + } + + /** + * Detects the projects to be imported. + * + * @param projects projects list + * @param bus + */ + detectProjectsToImport(projects: Array, bus: any): void { + this.projectsToImport = 0; + + projects.forEach((project: che.IProject) => { + if (!this.isProjectOnFileSystem(project)) { + this.projectsToImport++; + this.importProject(this.workspace.id, project, bus); + } + }); + + if (this.projectsToImport === 0) { + this.finish(); + } + } + + /** + * Project is on file system if there is no errors except code=9. + */ + isProjectOnFileSystem(project: che.IProject): boolean { + let problems = project.problems; + if (!problems || problems.length === 0) { + return true; + } + + for (var i = 0; i < problems.length; i++) { + if (problems[i].code === 9) { + return true; + } + } + + return false; + } + + /** + * Performs project import to pointed workspace. + * + * @param workspaceId workspace id, where project should be imported to + * @param project project to be imported + * @param bus + */ + importProject(workspaceId: string, project: che.IProject, bus: any): void { + var promise; + // websocket channel + var channel = 'importProject:output'; + + // on import + bus.subscribe(channel, (message: any) => { + this.getLoadingSteps()[this.getCurrentProgressStep()].logs = message.line; + }); + + let projectService = this.cheAPI.getWorkspace().getWorkspaceAgent(workspaceId).getProject(); + promise = projectService.importProject(project.name, project.source); + + // needs to update configuration of the project + let updatePromise = promise.then(() => { + projectService.updateProject(project.name, project); + }, (error) => { + this.handleError(error); + }); + + updatePromise.then(() => { + this.projectsToImport--; + if (this.projectsToImport === 0) { + this.finish(); + } + bus.unsubscribe(channel); + }, (error: any) => { + bus.unsubscribe(channel); + this.handleError(error); + + // need to show the error + this.$mdDialog.show( + this.$mdDialog.alert() + .title('Error while importing project') + .content(error.statusText + ': ' + error.data.message) + .ariaLabel('Import project') + .ok('OK') + ); + }); + } + + /** + * Performs operations at the end of accepting factory. + */ + finish(): void { + this.loadFactoryService.setCurrentProgressStep(4); + + // people should go back to the dashboard after factory is initialized + this.routeHistory.pushPath('/'); + + var ideParams = []; + if (this.routeParams) { + if (this.routeParams.id || (this.routeParams.name && this.routeParams.user)) { + ideParams.push('factory-id:' + this.factory.id); + } else { + // add every factory parameter by prefix + Object.keys(this.routeParams).forEach((key: string) => { + ideParams.push('factory-' + key + ':' + this.$window.encodeURIComponent(this.routeParams[key])); + }); + } + + // add factory mode + ideParams.push('factory:' + 'true'); + } + // add workspace Id + ideParams.push('workspaceId:' + this.workspace.id); + + this.$location.path(this.getIDELink()).search('ideParams', ideParams); + + // restore elements + this.restoreMenuAndFooter(); + } + + /** + * Returns workspace name. + * + * @returns {string} + */ + getWorkspace(): string { + return this.workspace.config.name; + } + + /** + * Returns the text(logs) of pointed step. + * + * @param stepNumber number of step + * @returns {string} step's text + */ + getStepText(stepNumber: number): string { + return this.loadFactoryService.getStepText(stepNumber); + } + + /** + * Returns loading steps of the factory. + * + * @returns {any} + */ + getLoadingSteps(): any { + return this.loadFactoryService.getFactoryLoadingSteps(); + } + + /** + * Returns the current step, which is in progress. + * + * @returns {any} the info of current step, which is in progress + */ + getCurrentProgressStep(): any { + return this.loadFactoryService.getCurrentProgressStep(); + } + + /** + * Returns the loading factory in progress state. + * + * @returns {boolean} + */ + isLoadFactoryInProgress(): boolean { + return this.loadFactoryService.isLoadFactoryInProgress(); + } + + /** + * Set the loading factory process in progress. + */ + setLoadFactoryInProgress(): void { + this.loadFactoryService.setLoadFactoryInProgress(true); + } + + /** + * Reset the loading factory process. + */ + resetLoadFactoryInProgress(): void { + this.restoreMenuAndFooter(); + let newLocation = this.isResourceProblem() ? '/workspaces' : '/factories'; + this.$location.path(newLocation); + this.loadFactoryService.resetLoadProgress(); + } + + /** + * Returns IDE link. + * + * @returns {string} IDE application link + */ + getIDELink() { + return '/ide/' + this.workspace.namespace + '/' + this.workspace.config.name; + } + + /** + * Performs navigation back to dashboard. + */ + backToDashboard(): void { + this.restoreMenuAndFooter(); + this.$location.path('/'); + } + + /** + * Performs downloading of the logs. + */ + downloadLogs(): void { + let logs = ''; + this.getLoadingSteps().forEach((step) => { + logs += step.logs + '\n'; + }); + window.open('data:text/csv,' + encodeURIComponent(logs)); + } + + /** + * Returns whether there was problem with resources. + * + * @returns {any|boolean} + */ + isResourceProblem(): boolean { + let currentCreationStep = this.getLoadingSteps()[this.getCurrentProgressStep()]; + return currentCreationStep.hasError && currentCreationStep.logs.includes('You can stop other workspaces'); + } +} diff --git a/dashboard/src/app/factories/load-factory/load-factory.html b/dashboard/src/app/factories/load-factory/load-factory.html new file mode 100644 index 00000000000..7fe69d4f396 --- /dev/null +++ b/dashboard/src/app/factories/load-factory/load-factory.html @@ -0,0 +1,68 @@ + +
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+ +
+
+
+ + +
+
+
+
+
+
+ + +
+
diff --git a/dashboard/src/app/factories/load-factory/load-factory.service.ts b/dashboard/src/app/factories/load-factory/load-factory.service.ts new file mode 100644 index 00000000000..54daf2de18e --- /dev/null +++ b/dashboard/src/app/factories/load-factory/load-factory.service.ts @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * This class is handling the service for the factory loading. + * @author Ann Shumilova + */ +export class LoadFactoryService { + private loadFactoryInProgress: boolean; + private currentProgressStep: number; + private loadingSteps: Array; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor () { + this.loadFactoryInProgress = false; + this.currentProgressStep = 0; + + + this.loadingSteps = [ + {text: 'Loading factory', inProgressText: '', logs: '', hasError: false}, + {text: 'Initializing workspace', inProgressText: 'Provision workspace and associating it with the existing user', logs: '', hasError: false}, + {text: 'Starting workspace runtime', inProgressText: 'Retrieving the stack\'s image and launching it', logs: '', hasError: false}, + {text: 'Starting workspace agent', inProgressText: 'Agents provide RESTful services like intellisense and SSH', logs: '', hasError: false}, + {text: 'Open IDE', inProgressText: '', logs: '', hasError: false} + ]; + } + + /** + * Get the text of the pointed step depending on it's state. + * + * @param stepNumber number of the step. + * @returns {string} steps's text + */ + getStepText(stepNumber: number): string { + let entry = this.loadingSteps[stepNumber]; + if (this.currentProgressStep >= stepNumber) { + return entry.inProgressText; + } else { + return entry.text; + } + } + + /** + * Returns the information of the factory's loading steps. + * + * @returns {Array} loading steps of the factory + */ + getFactoryLoadingSteps(): Array { + return this.loadingSteps; + } + + /** + * Sets the number of the step, which has to be in progress. + * + * @param currentProgressStep step number + */ + setCurrentProgressStep(currentProgressStep: number): void { + this.currentProgressStep = currentProgressStep; + } + + /** + * Proceeds the flow to the next step. + */ + goToNextStep(): void { + this.currentProgressStep++; + } + + /** + * Returns the number of the current step. + * + * @returns {number} current step's number + */ + getCurrentProgressStep(): number { + return this.currentProgressStep; + } + + /** + * Reset the loading progress. + */ + resetLoadProgress(): void { + this.loadingSteps.forEach((step) => { + step.logs = ''; + step.hasError = false; + }); + this.currentProgressStep = 0; + + this.loadFactoryInProgress = false; + } + + /** + * Returns the in-progress state of the whole factory loading flow. + * + * @returns {boolean} + */ + isLoadFactoryInProgress(): boolean { + return this.loadFactoryInProgress; + } + + /** + * Sets the in-progress state of the whole factory loading flow. + * + * @param value + */ + setLoadFactoryInProgress(value: boolean): void { + this.loadFactoryInProgress = value; + } +} diff --git a/dashboard/src/app/factories/load-factory/load-factory.styl b/dashboard/src/app/factories/load-factory/load-factory.styl new file mode 100644 index 00000000000..a010d4bdf27 --- /dev/null +++ b/dashboard/src/app/factories/load-factory/load-factory.styl @@ -0,0 +1,47 @@ +.load-factory-main-container + position absolute + width 100% + height 100% + + .ide-loader + height 100% + width 100% + background-color #1c253f + +.load-factory-content + width 100% + +.load-factory-working-log + margin 35px 0 + +.load-factory-bottom-bar + height 90px + + .load-factory-bottom-bar-left + margin-left 100px + + .load-factory-bottom-bar-right + margin-right 35px + +.load-factory-back-link + margin-top 20px + +.load-factory-download-link + margin-top 20px + margin-right 8px + +@media (max-width:959px) + .load-factory-bottom-bar + .load-factory-bottom-bar-left + margin-left 8px + .load-factory-bottom-bar-right + margin-right 8px + +.load-factory-logo + height 35px + position fixed + left 0 + right 0 + bottom 15px + margin auto + z-index 100 diff --git a/dashboard/src/app/index.module.ts b/dashboard/src/app/index.module.ts index e59f467b7a3..4bab58d0149 100644 --- a/dashboard/src/app/index.module.ts +++ b/dashboard/src/app/index.module.ts @@ -11,6 +11,7 @@ 'use strict'; import {Register} from '../components/utils/register'; +import {FactoryConfig} from './factories/factories-config'; import {ComponentsConfig} from '../components/components-config'; @@ -383,3 +384,4 @@ new WorkspacesConfig(instanceRegister); new DashboardConfig(instanceRegister); new StacksConfig(instanceRegister); new DocsConfig(instanceRegister); +new FactoryConfig(instanceRegister); diff --git a/dashboard/src/app/navbar/navbar.controller.ts b/dashboard/src/app/navbar/navbar.controller.ts index 879e71d15cd..4353fd8daf9 100644 --- a/dashboard/src/app/navbar/navbar.controller.ts +++ b/dashboard/src/app/navbar/navbar.controller.ts @@ -22,6 +22,7 @@ export class CheNavBarController { administration: '#/administration', // subsections plugins: '#/admin/plugins', + factories: '#/factories', account: '#/account', stacks: '#/stacks' }; @@ -81,6 +82,11 @@ export class CheNavBarController { getWorkspacesNumber(): number { return this.cheAPI.getWorkspace().getWorkspaces().length; } + + getFactoriesNumber(): number { + let pagesInfo = this.cheAPI.getFactory().getPagesInfo(); + return pagesInfo && pagesInfo.count ? pagesInfo.count : this.cheAPI.getFactory().factoriesById.size; + } openLinkInNewTab(url: string): void { this.$window.open(url, '_blank'); diff --git a/dashboard/src/app/navbar/navbar.html b/dashboard/src/app/navbar/navbar.html index 0fba05d2573..fa3c6b8fbe0 100644 --- a/dashboard/src/app/navbar/navbar.html +++ b/dashboard/src/app/navbar/navbar.html @@ -49,6 +49,16 @@ + + + + + diff --git a/dashboard/src/components/api/builder/che-api-builder.factory.ts b/dashboard/src/components/api/builder/che-api-builder.factory.ts index b271bc45989..021e08c220b 100644 --- a/dashboard/src/components/api/builder/che-api-builder.factory.ts +++ b/dashboard/src/components/api/builder/che-api-builder.factory.ts @@ -12,12 +12,14 @@ import {CheWorkspaceBuilder} from './che-workspace-builder'; import {CheProjectReferenceBuilder} from './che-projectreference-builder'; +import {CheFactoryBuilder} from './che-factory-builder'; import {CheProjectDetailsBuilder} from './che-projectdetails-builder'; import {CheProjectTypeBuilder} from './che-projecttype-builder'; import {CheProjectTemplateBuilder} from './che-projecttemplate-builder'; import {CheProjectTypeAttributeDescriptorBuilder} from './che-projecttype-attribute-descriptor-builder'; import {CheProfileBuilder} from './che-profile-builder'; import {CheStackBuilder} from './che-stack-builder'; +import {CheUserBuilder} from './che-user-builder'; /** * This class is providing the entry point for accessing the builders @@ -96,4 +98,20 @@ export class CheAPIBuilder { getStackBuilder() { return new CheStackBuilder(); } + + /*** + * The Che Factory builder + * @returns {CheFactoryBuilder} + */ + getFactoryBuilder() { + return new CheFactoryBuilder(); + } + + /*** + * The Che User builder + * @returns {CheUserBuilder} + */ + getUserBuilder() { + return new CheUserBuilder(); + } } diff --git a/dashboard/src/components/api/builder/che-factory-builder.ts b/dashboard/src/components/api/builder/che-factory-builder.ts new file mode 100644 index 00000000000..2aacc093fb5 --- /dev/null +++ b/dashboard/src/components/api/builder/che-factory-builder.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + + +/** + * This class is providing a builder for factory + * @author Oleksii Orel + */ +export class CheFactoryBuilder { + + private factory: che.IFactory; + + /** + * Default constructor. + */ + constructor() { + this.factory = {}; + this.factory.creator = {}; + } + + /** + * Sets the creator email + * @param email + * @returns {CheFactoryBuilder} + */ + withCreatorEmail(email) { + this.factory.creator.email = email; + return this; + } + + /** + * Sets the creator name + * @param name + * @returns {CheFactoryBuilder} + */ + withCreatorName(name) { + this.factory.creator.name = name; + return this; + } + + /** + * Sets the id of the factory + * @param id + * @returns {CheFactoryBuilder} + */ + withId(id) { + this.factory.id = id; + return this; + } + + /** + * Sets the name of the factory + * @param name + * @returns {CheFactoryBuilder} + */ + withName(name) { + this.factory.name = name; + return this; + } + + /** + * Sets the workspace of the factory + * @param workspace + * @returns {CheFactoryBuilder} + */ + withWorkspace(workspace) { + this.factory.workspace = workspace; + return this; + } + + /** + * Build the factory + * @returns {CheFactoryBuilder|*} + */ + build() { + return this.factory; + } +} diff --git a/dashboard/src/components/api/builder/che-user-builder.ts b/dashboard/src/components/api/builder/che-user-builder.ts new file mode 100644 index 00000000000..bb80bda5d73 --- /dev/null +++ b/dashboard/src/components/api/builder/che-user-builder.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + + +/** + * This class is providing a builder for User + * @author Florent Benoit + * @author Oleksii Orel + */ +export class CheUserBuilder { + private user: che.IUser; + + /** + * Default constructor. + */ + constructor() { + this.user = {}; + } + + /** + * Sets the email of the user + * @param email the email to use + * @returns {CodenvyUserBuilder} + */ + withEmail(email) { + this.user.email = email; + return this; + } + + /** + * Sets the id of the user + * @param id the id to use + * @returns {CodenvyUserBuilder} + */ + withId(id) { + this.user.id = id; + return this; + } + + /** + * Sets the aliases of the user + * @param aliases the aliases to use + * @returns {CodenvyUserBuilder} + */ + withAliases(aliases) { + this.user.aliases = aliases; + return this; + } + + /** + * Build the user + * @returns {CodenvyUserBuilder.user|*} + */ + build() { + return this.user; + } +} diff --git a/dashboard/src/components/api/che-api-config.ts b/dashboard/src/components/api/che-api-config.ts index 075896d86b8..146f5375351 100644 --- a/dashboard/src/components/api/che-api-config.ts +++ b/dashboard/src/components/api/che-api-config.ts @@ -15,6 +15,7 @@ import {CheWorkspace} from './che-workspace.factory'; import {CheProjectTemplate} from './che-project-template.factory'; import {CheRecipe} from './che-recipe.factory'; import {CheRecipeTemplate} from './che-recipe-template.factory'; +import {CheFactory} from './che-factory.factory'; import {CheStack} from './che-stack.factory'; import {CheWebsocket} from './che-websocket.factory'; import {CheProfile} from './che-profile.factory'; @@ -22,6 +23,7 @@ import {ChePreferences} from './che-preferences.factory'; import {CheService} from './che-service.factory'; import {CheHttpBackend} from './test/che-http-backend'; import {CheHttpBackendProviderFactory} from './test/che-http-backend-provider.factory' +import {CheFactoryTemplate} from './che-factory-template.factory'; import {CheHttpBackendFactory} from './test/che-http-backend.factory'; import {CheAPIBuilder} from './builder/che-api-builder.factory'; import {CheAdminPlugins} from './che-admin-plugins.factory'; @@ -32,12 +34,14 @@ import {CheEnvironmentRegistry} from './environment/che-environment-registry.fac import {CheAgent} from './che-agent.factory'; import {CheSsh} from './che-ssh.factory'; import {CheNamespaceRegistry} from './namespace/che-namespace-registry.factory'; +import {CheUser} from './che-user.factory'; export class ApiConfig { constructor(register) { register.factory('cheWorkspace', CheWorkspace); register.factory('cheProjectTemplate', CheProjectTemplate); + register.factory('cheFactory', CheFactory); register.factory('cheProfile', CheProfile); register.factory('chePreferences', ChePreferences); register.factory('cheWebsocket', CheWebsocket); @@ -49,6 +53,7 @@ export class ApiConfig { register.factory('cheAPIBuilder', CheAPIBuilder); register.factory('cheAdminPlugins', CheAdminPlugins); register.factory('cheAdminService', CheAdminService); + register.factory('cheFactoryTemplate', CheFactoryTemplate); register.factory('cheService', CheService); register.factory('cheAPI', CheAPI); register.factory('cheRemote', CheRemote); @@ -57,5 +62,6 @@ export class ApiConfig { register.factory('cheAgent', CheAgent); register.factory('cheSsh', CheSsh); register.factory('cheNamespaceRegistry', CheNamespaceRegistry); + register.factory('cheUser', CheUser); } } diff --git a/dashboard/src/components/api/che-api.factory.ts b/dashboard/src/components/api/che-api.factory.ts index f083e756afd..e066b345c53 100644 --- a/dashboard/src/components/api/che-api.factory.ts +++ b/dashboard/src/components/api/che-api.factory.ts @@ -12,6 +12,8 @@ import {CheSsh} from './che-ssh.factory'; 'use strict'; import {CheWorkspace} from './che-workspace.factory'; import {CheProfile} from './che-profile.factory'; +import {CheFactory} from './che-factory.factory'; +import {CheFactoryTemplate} from './che-factory-template.factory'; import {ChePreferences} from './che-preferences.factory'; import {CheProjectTemplate} from './che-project-template.factory'; import {CheWebsocket} from './che-websocket.factory'; @@ -23,6 +25,7 @@ import {CheRecipeTemplate} from './che-recipe-template.factory'; import {CheStack} from './che-stack.factory'; import {CheOAuthProvider} from './che-o-auth-provider.factory'; import {CheAgent} from './che-agent.factory'; +import {CheUser} from './che-user.factory'; /** @@ -37,6 +40,8 @@ export class CheAPI { private chePreferences: ChePreferences; private cheProjectTemplate: CheProjectTemplate; private cheWebsocket: CheWebsocket; + private cheFactory: CheFactory; + private cheFactoryTemplate: CheFactoryTemplate; private cheService: CheService; private cheAdminPlugins: CheAdminPlugins; private cheAdminService: CheAdminService; @@ -46,14 +51,20 @@ export class CheAPI { private cheOAuthProvider: CheOAuthProvider; private cheAgent: CheAgent; private cheSsh: CheSsh; + private cheUser: CheUser; /** * Default constructor that is using resource * @ngInject for Dependency injection */ - constructor(cheWorkspace: CheWorkspace, cheProfile: CheProfile, chePreferences: ChePreferences, cheProjectTemplate: CheProjectTemplate, cheWebsocket: CheWebsocket, cheService: CheService, cheAdminPlugins: CheAdminPlugins, cheAdminService: CheAdminService, cheRecipe: CheRecipe, cheRecipeTemplate: CheRecipeTemplate, cheStack: CheStack, cheOAuthProvider: CheOAuthProvider, cheAgent: CheAgent, cheSsh: CheSsh) { + constructor(cheWorkspace: CheWorkspace, cheFactory: CheFactory, cheFactoryTemplate: CheFactoryTemplate, cheProfile: CheProfile, + chePreferences: ChePreferences, cheProjectTemplate: CheProjectTemplate, cheWebsocket: CheWebsocket, cheService: CheService, + cheAdminPlugins: CheAdminPlugins, cheAdminService: CheAdminService, cheRecipe: CheRecipe, cheRecipeTemplate: CheRecipeTemplate, + cheStack: CheStack, cheOAuthProvider: CheOAuthProvider, cheAgent: CheAgent, cheSsh: CheSsh, cheUser: CheUser) { this.cheWorkspace = cheWorkspace; this.cheProfile = cheProfile; + this.cheFactory = cheFactory; + this.cheFactoryTemplate = cheFactoryTemplate; this.chePreferences = chePreferences; this.cheProjectTemplate = cheProjectTemplate; this.cheWebsocket = cheWebsocket; @@ -66,6 +77,7 @@ export class CheAPI { this.cheOAuthProvider = cheOAuthProvider; this.cheAgent = cheAgent; this.cheSsh = cheSsh; + this.cheUser = cheUser; } @@ -129,7 +141,7 @@ export class CheAPI { * The Che Admin Services API * @returns {CheAdminService} */ - getAdminService() { + getAdminService(): CheAdminService { return this.cheAdminService; } @@ -182,4 +194,28 @@ export class CheAPI { return this.cheSsh; } + /** + * The Che Factory API + * @returns {CheFactory|*} + */ + getFactory(): CheFactory { + return this.cheFactory; + } + + /** + * The Che Factory Template API + * @returns {CheFactoryTemplate|*} + */ + getFactoryTemplate(): CheFactoryTemplate { + return this.cheFactoryTemplate; + } + + /** + * The Che use API. + * + * @returns {CheUser} + */ + getUser(): CheUser { + return this.cheUser; + } } diff --git a/dashboard/src/components/api/che-factory-template.factory.ts b/dashboard/src/components/api/che-factory-template.factory.ts new file mode 100644 index 00000000000..4ee60203e40 --- /dev/null +++ b/dashboard/src/components/api/che-factory-template.factory.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * This class is handling the factory template retrieval + * It sets to the Map factory templates + * @author Oleksii Orel + */ +export class CheFactoryTemplate { + + private $resource: ng.resource.IResourceService; + private $q: ng.IQService; + private factoryTemplatesByName: Map; + private remoteFactoryTemplateAPI: ng.resource.IResourceClass; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor($resource: ng.resource.IResourceService, $q: ng.IQService) { + // keep resource + this.$resource = $resource; + this.$q = $q; + + // factory templates map + this.factoryTemplatesByName = new Map(); + + // remote call + this.remoteFactoryTemplateAPI = this.$resource('https://dockerfiles.codenvycorp.com/templates-4.8/factory/:fileName'); + } + + /** + * Ask for loading the factory template in asynchronous way + * If there are no changes, it's not updated + * @param templateName the template name + * @returns {*} the promise + */ + fetchFactoryTemplate(templateName: string): ng.IPromise { + var deferred = this.$q.defer(); + + let templateFileName = templateName + '.json'; + + let promise = this.remoteFactoryTemplateAPI.get({fileName: templateFileName}).$promise; + + promise.then((factoryTemplateContent) => { + //update factory template map + this.factoryTemplatesByName.set(templateName, factoryTemplateContent); + deferred.resolve(factoryTemplateContent); + }, (error) => { + if (error.status === 304) { + let findFactoryTemplateContent = this.factoryTemplatesByName.get(templateName); + deferred.resolve(findFactoryTemplateContent); + } else { + deferred.reject(error); + } + }); + return deferred.promise; + } + + /** + * Gets factory template by template name + * @param templateName the template name + * @returns factory template content + */ + getFactoryTemplate(templateName: string): any { + return this.factoryTemplatesByName.get(templateName); + } +} diff --git a/dashboard/src/components/api/che-factory.factory.ts b/dashboard/src/components/api/che-factory.factory.ts new file mode 100644 index 00000000000..f47c12f5b94 --- /dev/null +++ b/dashboard/src/components/api/che-factory.factory.ts @@ -0,0 +1,645 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheUser} from './che-user.factory'; + +/* global FormData */ + +interface IFactoriesResource extends ng.resource.IResourceClass { + updateFactory: any; + getFactoryContentFromWorkspace: any; + getFactoryParameters: any; + createFactoryByContent: any; + getFactories: any; + getFactoryByName: any; +} + + +/** + * This class is handling the factory retrieval + * @author Florent Benoit + * @author Oleksii Orel + */ +export class CheFactory { + private $resource: ng.resource.IResourceService; + private $q: ng.IQService; + private lodash: _.LoDashStatic; + private cheUser: CheUser; + + private remoteFactoryAPI: IFactoriesResource; + + private factoriesById: Map; + private factoriesByName: Map; + private parametersFactories: Map; + private factoryContentsByWorkspaceId: Map; + private pageFactories: Array; + private factoryPagesMap: Map; + private pageInfo: any; + private itemsPerPage: number; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor($resource: ng.resource.IResourceService, $q: ng.IQService, lodash: _.LoDashStatic, cheUser: CheUser) { + // keep resource + this.$resource = $resource; + this.$q = $q; + this.cheUser = cheUser; + + this.lodash = lodash; + + // factories details by id + this.factoriesById = new Map(); + // factories details by key: userID:factoryName + this.factoriesByName = new Map(); + this.parametersFactories = new Map(); + this.factoryContentsByWorkspaceId = new Map(); + + //paging + this.pageFactories = [];// all current page factories + this.factoryPagesMap = new Map();// page factories by relative link + this.pageInfo = {};//pages info + + this.remoteFactoryAPI = >this.$resource('/api/factory/:factoryId', {factoryId: '@id'}, { + updateFactory: {method: 'PUT', url: '/api/factory/:factoryId'}, + getFactoryContentFromWorkspace: {method: 'GET', url: '/api/factory/workspace/:workspaceId'}, + getFactoryParameters: {method: 'POST', url: '/api/factory/resolver/'}, + createFactoryByContent: { + method: 'POST', + url: '/api/factory', + isArray: false, + headers: {'Content-Type': undefined}, + transformRequest: angular.identity + }, + getFactories: { + method: 'GET', + url: '/api/factory/find?creator.userId=:userId&maxItems=:maxItems&skipCount=:skipCount', + isArray: false, + responseType: 'json', + transformResponse: (data, headersGetter) => { + return this._getPageFromResponse(data, headersGetter('link')); + } + }, + getFactoryByName: { + method: 'GET', + url: '/api/factory/find?creator.userId=:userId&name=:factoryName', + isArray: true + } + }); + } + + _getPageFromResponse(data: any, headersLink: any): any { + let links = new Map(); + if (!headersLink) { + //TODO remove it after adding headers paging links on server side + let user = this.cheUser.getUser().id; + if (!this.itemsPerPage || !user) { + return {factories: data}; + } + this.pageInfo.currentPageNumber = this.pageInfo.currentPageNumber ? this.pageInfo.currentPageNumber : 1; + let link = '/api/factory/find?creator.userId=' + user + '&maxItems=' + this.itemsPerPage; + links.set('first', link + '&skipCount=0'); + if (data && data.length > 0) { + links.set('next', link + '&skipCount=' + this.pageInfo.currentPageNumber * this.itemsPerPage); + } + if (this.pageInfo.currentPageNumber > 1) { + links.set('prev', link + '&skipCount=' + (this.pageInfo.currentPageNumber - 2) * this.itemsPerPage); + } + return { + factories: data, + links: links + }; + } + let pattern = new RegExp('<([^>]+?)>.+?rel="([^"]+?)"', 'g'); + let result; + while (result = pattern.exec(headersLink)) { //look for pattern + links.set(result[2], result[1]);//add link + } + return { + factories: data, + links: links + }; + } + + _getPageParamByLink(pageLink: string): any { + let pageParamMap = new Map(); + let pattern = new RegExp('([_\\w]+)=([\\w]+)', 'g'); + let result; + while (result = pattern.exec(pageLink)) { + pageParamMap.set(result[1], result[2]); + } + + let skipCount = pageParamMap.get('skipCount'); + let maxItems = pageParamMap.get('maxItems'); + if (!maxItems || maxItems === 0) { + return null; + } + return { + maxItems: maxItems, + skipCount: skipCount ? skipCount : 0 + }; + } + + _updateCurrentPage(): void { + let pageData = this.factoryPagesMap.get(this.pageInfo.currentPageNumber); + if (!pageData) { + return; + } + this.pageFactories.length = 0; + if (!pageData.factories) { + return; + } + pageData.factories.forEach((factory: che.IFactory) => { + factory.name = factory.name ? factory.name : ''; + this.pageFactories.push(factory); + }); + } + + _updateCurrentPageFactories(factories: Array): void { + this.pageFactories.length = 0; + if (!factories) { + return; + } + factories.forEach((factory: any) => { + factory.name = factory.name ? factory.name : ''; + this.pageFactories.push(factory); + }); + } + + /** + * Update factory page links by relative direction ('first', 'prev', 'next', 'last') + */ + _updatePagesData(data: any): void { + if (!data.links) { + return; + } + + let firstPageLink = data.links.get('first'); + if (firstPageLink) { + let firstPageData = {link: firstPageLink, factories: null}; + if (this.pageInfo.currentPageNumber === 1) { + firstPageData.factories = data.factories; + } + if (!this.factoryPagesMap.get(1) || firstPageData.factories) { + this.factoryPagesMap.set(1, firstPageData); + } + } + let lastPageLink = data.links.get('last'); + if (lastPageLink) { + let pageParam = this._getPageParamByLink(lastPageLink); + this.pageInfo.countOfPages = pageParam.skipCount / pageParam.maxItems + 1; + this.pageInfo.count = pageParam.skipCount; + let lastPageData = {link: lastPageLink, factories: null}; + if (this.pageInfo.currentPageNumber === this.pageInfo.countOfPages) { + lastPageData.factories = data.factories; + } + if (!this.factoryPagesMap.get(this.pageInfo.countOfPages) || lastPageData.factories) { + this.factoryPagesMap.set(this.pageInfo.countOfPages, lastPageData); + } + } + let prevPageLink = data.links.get('prev'); + let prevPageNumber = this.pageInfo.currentPageNumber - 1; + if (prevPageNumber > 0 && prevPageLink) { + let prevPageData = {link: prevPageLink}; + if (!this.factoryPagesMap.get(prevPageNumber)) { + this.factoryPagesMap.set(prevPageNumber, prevPageData); + } + } + let nextPageLink = data.links.get('next'); + let nextPageNumber = this.pageInfo.currentPageNumber + 1; + if (nextPageNumber) { + let nextPageData = {link: nextPageLink}; + if (!this.factoryPagesMap.get(nextPageNumber)) { + this.factoryPagesMap.set(nextPageNumber, nextPageData); + } + } + } + + /** + * Returns the page information. + * @returns {Object} + */ + getPagesInfo(): any { + return this.pageInfo; + } + + /** + * Ask for loading the factories in asynchronous way + * If there are no changes, it's not updated + * @param maxItems - the max number of items to return + * @param skipCount - the number of items to skip + * @returns {*} the promise + */ + fetchFactories(maxItems, skipCount): ng.IPromise { + this.itemsPerPage = maxItems; + let promise = this._getFactories({maxItems: maxItems, skipCount: skipCount}); + + return promise.then((data: any) => { + this.pageInfo.currentPageNumber = skipCount / maxItems + 1; + this._updateCurrentPageFactories(data.factories); + this._updatePagesData(data); + }); + } + + /** + * Ask for loading the factories page in asynchronous way + * If there are no changes, it's not updated + * @param pageKey - the key of page ('first', 'prev', 'next', 'last' or '1', '2', '3' ...) + * @returns {*} the promise + */ + fetchFactoryPage(pageKey: string) { + let deferred = this.$q.defer(); + let pageNumber; + if ('first' === pageKey) { + pageNumber = 1; + } else if ('prev' === pageKey) { + pageNumber = this.pageInfo.currentPageNumber - 1; + } else if ('next' === pageKey) { + pageNumber = this.pageInfo.currentPageNumber + 1; + } else if ('last' === pageKey) { + pageNumber = this.pageInfo.countOfPages; + } else { + pageNumber = parseInt(pageKey, 10); + } + if (pageNumber < 1) { + pageNumber = 1; + } else if (pageNumber > this.pageInfo.countOfPages) { + pageNumber = this.pageInfo.countOfPages; + } + let pageData = this.factoryPagesMap.get(pageNumber); + if (pageData.link) { + this.pageInfo.currentPageNumber = pageNumber; + let queryData = this._getPageParamByLink(pageData.link); + if (!queryData) { + deferred.reject({data: {message: 'Error. No necessary link.'}}); + return deferred.promise; + } + let promise = this._getFactories(queryData); + promise.then((data: any) => { + this._updatePagesData(data); + pageData.factories = data.factories; + this._updateCurrentPage(); + deferred.resolve(data); + }, (error: any) => { + if (error && error.status === 304) { + this._updateCurrentPage(); + } + deferred.reject(error); + }); + } else { + deferred.reject({data: {message: 'Error. No necessary link.'}}); + } + return deferred.promise; + } + + _getFactories(queryData): ng.IPromise { + let deferred = this.$q.defer(); + let user = this.cheUser.getUser(); + + if (user) { + queryData.userId = user.id; + this.remoteFactoryAPI.getFactories(queryData).$promise.then((data) => { + this._updateFactoriesDetails(data.factories).then((factoriesDetails) => { + data.factories = factoriesDetails;//update factories + deferred.resolve(data); + }, (error) => { + deferred.reject(error); + }); + }, (error) => { + deferred.reject(error); + }); + } else { + this.cheUser.fetchUser().then((user) => { + queryData.userId = user.id; + this.remoteFactoryAPI.getFactories(queryData).$promise.then((data) => { + this._updateFactoriesDetails(data.factories).then((factoriesDetails) => { + data.factories = factoriesDetails;//update factories + deferred.resolve(data); + }, (error) => { + deferred.reject(error); + }); + }, (error) => { + deferred.reject(error); + }); + }); + } + + return deferred.promise; + } + + _updateFactoriesDetails(factories: Array): ng.IPromise { + let deferred = this.$q.defer(); + let factoriesDetails = []; + + if (!factories || factories.length === 0) { + deferred.resolve(factoriesDetails); + } + + let promises = []; + factories.forEach((factory: any) => { + let factoryPromise = this.fetchFactoryById(factory.id);//ask the factory details + factoryPromise.then((factoryDetails: any) => { + factoriesDetails.push(factoryDetails); + }); + promises.push(factoryPromise); + }); + this.$q.all(promises).then(() => { + deferred.resolve(factoriesDetails); + }, (error) => { + deferred.reject(error); + }); + + return deferred.promise; + } + + /** + * Gets the factory service path. + * @returns {string} + */ + getFactoryServicePath(): string { + return 'factory'; + } + + /** + * Ask for loading the factory content in asynchronous way + * If there are no changes, it's not updated + * @param workspace workspace + * @returns {*} the promise + */ + fetchFactoryContentFromWorkspace(workspace: che.IWorkspace): ng.IPromise { + let deferred = this.$q.defer(); + + let factoryContent = this.factoryContentsByWorkspaceId.get(workspace.id); + if (factoryContent) { + deferred.resolve(factoryContent); + } + + let promise = this.remoteFactoryAPI.getFactoryContentFromWorkspace({ + workspaceId: workspace.id + }).$promise; + + promise.then((factoryContent: any) => { + //update factoryContents map + this.factoryContentsByWorkspaceId.set(workspace.id, factoryContent); + deferred.resolve(factoryContent); + }, (error: any) => { + if (error.status === 304) { + let findFactoryContent = this.factoryContentsByWorkspaceId.get(workspace.id); + deferred.resolve(findFactoryContent); + } else { + deferred.reject(error); + } + }); + + return deferred.promise; + } + + /** + * Get factory from workspace + * @param workspace + * @return the factory content + */ + getFactoryContentFromWorkspace(workspace: che.IWorkspace): any { + return this.factoryContentsByWorkspaceId.get(workspace.workspaceId); + } + + /** + * Create factory by content + * @param factoryContent the factory content + * @returns {*} the promise + */ + createFactoryByContent(factoryContent: any): ng.IPromise { + let formDataObject = new FormData(); + formDataObject.append('factory', factoryContent); + + return this.remoteFactoryAPI.createFactoryByContent({}, formDataObject).$promise; + } + + /** + * Gets the current page factories + * @returns {Array} + */ + getPageFactories(): Array { + return this.pageFactories; + } + + /** + * Ask for loading the factory in asynchronous way + * If there are no changes, it's not updated + * @param factoryId the factory ID + * @returns {*} the promise + */ + fetchFactoryById(factoryId: string): ng.IPromise { + let deferred = this.$q.defer(); + + let promise = this.remoteFactoryAPI.get({factoryId: factoryId}).$promise; + promise.then((factory: any) => { + factory.name = factory.name ? factory.name : ''; + this.factoriesById.set(factoryId, factory); + deferred.resolve(factory); + }, (error: any) => { + if (error.status === 304) { + deferred.resolve(this.factoriesById.get(factoryId)); + } else { + deferred.reject(error); + } + }); + + return deferred.promise; + } + + /** + * Fetches factory by the user's id and factory's name. + * + * @param factoryName name of the factory to be fetched. + * @param userId user id + * @returns {IPromise} + */ + fetchFactoryByName(factoryName: string, userId: string): ng.IPromise { + let deferred = this.$q.defer(); + let key = userId + ':' + factoryName; + let promise = this.remoteFactoryAPI.getFactoryByName({factoryName: factoryName, userId: userId}).$promise; + promise.then((factories: Array) => { + let factory = factories.length ? factories[0] : null; + this.factoriesByName.set(key, factory); + deferred.resolve(factory); + }, (error: any) => { + if (error.status === 304) { + deferred.resolve(this.factoriesByName.get(key)); + } else { + deferred.reject(error); + } + }); + + return deferred.promise; + } + + /** + * Ask for getting parameter the factory in asynchronous way + * If there are no changes, it's not updated + * @param parameters the factory parameters + * @returns {*} the promise + */ + fetchParameterFactory(parameters: any): ng.IPromise { + let deferred = this.$q.defer(); + + let promise = this.remoteFactoryAPI.getFactoryParameters({}, parameters).$promise; + promise.then((factory: any) => { + factory.name = factory.name ? factory.name : ''; + this.parametersFactories.set(parameters, factory); + deferred.resolve(factory); + }, (error: any) => { + if (error.status === 304) { + deferred.resolve(this.parametersFactories.get(parameters)); + } else { + deferred.reject(error); + } + }); + + return deferred.promise; + } + + /** + * Detects links for factory acceptance (with id and named one) + * @param factory factory to detect links + * @returns [*] links acceptance links + */ + detectLinks(factory: che.IFactory): Array { + let links = []; + + if (!factory || !factory.links) { + return links; + } + + this.lodash.find(factory.links, (link: any) => { + if (link.rel === 'accept' || link.rel === 'accept-named') { + links.push(link.href); + } + }); + + return links; + } + + /** + * Get the factory from factoryMap by factoryId + * @param factoryId the factory ID + * @returns factory + */ + getFactoryById(factoryId: string): any { + return this.factoriesById.get(factoryId); + } + + /** + * Set the factory + * @param factory + * @returns {*} the promise + */ + setFactory(factory: che.IFactory): ng.IPromise { + let deferred = this.$q.defer(); + // check factory + if (!factory || !factory.id) { + deferred.reject({data: {message: 'Read factory error.'}}); + return deferred.promise; + } + + let promise = this.remoteFactoryAPI.updateFactory({factoryId: factory.id}, factory).$promise; + // check if was OK or not + promise.then((factory: any) => { + factory.name = factory.name ? factory.name : ''; + this.fetchFactoryPage(this.pageInfo.currentPageNumber); + deferred.resolve(factory); + }, (error: any) => { + deferred.reject(error); + }); + + return deferred.promise; + } + + /** + * Set the factory content by factoryId + * @param factoryId the factory ID + * @param factoryContent the factory content + * @returns {*} the promise + */ + setFactoryContent(factoryId: string, factoryContent: any): ng.IPromise { + let deferred = this.$q.defer(); + + let promise = this.remoteFactoryAPI.updateFactory({factoryId: factoryId}, factoryContent).$promise; + // check if was OK or not + promise.then(() => { + let fetchFactoryPromise = this.fetchFactoryById(factoryId); + //check if was OK or not + fetchFactoryPromise.then((factory: any) => { + this.fetchFactoryPage(this.pageInfo.currentPageNumber); + deferred.resolve(factory); + }, (error: any) => { + deferred.reject(error); + }); + }, (error) => { + deferred.reject(error); + }); + + return deferred.promise; + } + + /** + * Performs factory deleting by the given factoryId. + * @param factoryId the factory ID + * @returns {*} the promise + */ + deleteFactoryById(factoryId: string): ng.IPromise { + let promise = this.remoteFactoryAPI.delete({factoryId: factoryId}).$promise; + //check if was OK or not + return promise.then(() => { + this.factoriesById.delete(factoryId);//update factories map + if (this.pageInfo && this.pageInfo.currentPageNumber) { + this.fetchFactoryPage(this.pageInfo.currentPageNumber); + } + }); + } + + /** + * Helper method that extract the factory ID from a factory URL + * @param factoryURL the factory URL to analyze + * @returns the stringified ID of a factory + */ + getIDFromFactoryAPIURL(factoryURL: string): string { + let index = factoryURL.lastIndexOf('/factory/'); + if (index > 0) { + return factoryURL.slice(index + '/factory/'.length, factoryURL.length); + } + } + + /** + * Returns the factory url based on id. + * @returns {link.href|*} link value + */ + getFactoryIdUrl(factory: any): string { + let link = this.lodash.find(factory.links, (link: any) => { + return 'accept' === link.rel; + }); + return link ? link.href : 'No value'; + } + + /** + * Returns the factory url based on name. + * + * @returns {link.href|*} link value + */ + getFactoryNamedUrl(factory: any): string { + let link = this.lodash.find(factory.links, (link: any) => { + return 'accept-named' === link.rel; + }); + + return link ? link.href : null; + } +} diff --git a/dashboard/src/components/api/che-factory.spec.ts b/dashboard/src/components/api/che-factory.spec.ts new file mode 100644 index 00000000000..0ed4d8844b2 --- /dev/null +++ b/dashboard/src/components/api/che-factory.spec.ts @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +/** + * Test of the Codenvy Factory API + */ +describe('CheFactory', () => { + + + /** + * User Factory for the test + */ + let factory; + + /** + * API builder. + */ + let apiBuilder; + + /** + * Backend for handling http operations + */ + let httpBackend; + + /** + * Che backend + */ + let cheBackend; + + /** + * setup module + */ + beforeEach(angular.mock.module('userDashboard')); + + /** + * Inject factory and http backend + */ + beforeEach(inject((cheFactory, cheAPIBuilder, cheHttpBackend) => { + + factory = cheFactory; + apiBuilder = cheAPIBuilder; + cheBackend = cheHttpBackend; + httpBackend = cheHttpBackend.getHttpBackend(); + })); + + /** + * Check assertion after the test + */ + afterEach(() => { + httpBackend.verifyNoOutstandingExpectation(); + httpBackend.verifyNoOutstandingRequest(); + }); + + + /** + * Check that we're able to fetch factory data + */ + it('Fetch factories', () => { + // setup tests objects + let maxItem = 3; + let skipCount = 0; + let testUser = apiBuilder.getUserBuilder().withId('testUserId').build(); + let testFactory1 = apiBuilder.getFactoryBuilder().withId('testId1').withName('testName1').withCreatorEmail('testEmail1').build(); + let testFactory2 = apiBuilder.getFactoryBuilder().withId('testId2').withName('testName2').withCreatorEmail('testEmail2').build(); + let testFactory3 = apiBuilder.getFactoryBuilder().withId('testId3').withName('testName3').withCreatorEmail('testEmail3').build(); + let testFactory4 = apiBuilder.getFactoryBuilder().withId('testId4').withName('testName4').withCreatorEmail('testEmail4').build(); + + // providing requests + // add test objects on Http backend + cheBackend.setDefaultUser(testUser); + cheBackend.addUserFactory(testFactory1); + cheBackend.addUserFactory(testFactory2); + cheBackend.addUserFactory(testFactory3); + cheBackend.addUserFactory(testFactory4); + cheBackend.setPageMaxItem(maxItem); + cheBackend.setPageSkipCount(skipCount); + + // setup backend for factories + cheBackend.factoriesBackendSetup(); + + // fetch factory + factory.fetchFactories(maxItem, skipCount); + + // expecting GETs + httpBackend.expectGET('/api/user'); + + httpBackend.expectGET('/api/factory/find?creator.userId=' + testUser.id + '&maxItems=' + maxItem + '&skipCount=' + skipCount); + httpBackend.expectGET('/api/factory/' + testFactory1.id); + httpBackend.expectGET('/api/factory/' + testFactory2.id); + httpBackend.expectGET('/api/factory/' + testFactory3.id); + + // flush command + httpBackend.flush(); + + // now, check factories + let pageFactories = factory.getPageFactories(); + let factory1 = factory.getFactoryById(testFactory1.id); + + let factory2 = factory.getFactoryById(testFactory2.id); + let factory3 = factory.getFactoryById(testFactory3.id); + + // check id, name and email of pge factories + expect(pageFactories.length).toEqual(maxItem); + expect(factory1.id).toEqual(testFactory1.id); + expect(factory1.name).toEqual(testFactory1.name); + expect(factory1.creator.email).toEqual(testFactory1.creator.email); + expect(factory2.id).toEqual(testFactory2.id); + expect(factory2.name).toEqual(testFactory2.name); + expect(factory2.creator.email).toEqual(testFactory2.creator.email); + expect(factory3.id).toEqual(testFactory3.id); + expect(factory3.name).toEqual(testFactory3.name); + expect(factory3.creator.email).toEqual(testFactory3.creator.email); + } + ); + + /** + * Check that we're able to fetch factory data by id + */ + it('Fetch factor by id', () => { + // setup tests objects + let testFactory = apiBuilder.getFactoryBuilder().withId('testId').withName('testName').withCreatorEmail('testEmail').build(); + + // providing request + // add test factory on Http backend + cheBackend.addUserFactory(testFactory); + + // setup backend + cheBackend.factoriesBackendSetup(); + + // fetch factory + factory.fetchFactoryById(testFactory.id); + + // expecting GETs + httpBackend.expectGET('/api/factory/' + testFactory.id); + + // flush command + httpBackend.flush(); + + // now, check factory + let targetFactory = factory.getFactoryById(testFactory.id); + + // check id, name and email of factory + expect(targetFactory.id).toEqual(testFactory.id); + expect(targetFactory.name).toEqual(testFactory.name); + expect(targetFactory.creator.email).toEqual(testFactory.creator.email); + } + ); + + /** + * Check that we're able to delete factor by id + */ + it('Delete factor by id', () => { + // setup tests objects + let testFactory = apiBuilder.getFactoryBuilder().withId('testId').withName('testName').withCreatorEmail('testEmail').build(); + + // providing request + // add test factory on Http backend + cheBackend.addUserFactory(testFactory); + + // setup backend + cheBackend.factoriesBackendSetup(); + + // delete factory + factory.deleteFactoryById(testFactory.id); + + // expecting GETs + httpBackend.expectDELETE('/api/factory/' + testFactory.id); + + // flush command + httpBackend.flush(); + } + ); + + /** + * Gets factory page object from response + */ + it('Gets factory page object from response', () => { + let testFactory1 = apiBuilder.getFactoryBuilder().withId('testId1').withName('testName1').withCreatorEmail('testEmail1').build(); + let testFactory2 = apiBuilder.getFactoryBuilder().withId('testId2').withName('testName2').withCreatorEmail('testEmail2').build(); + let factories = [testFactory1, testFactory2]; + + let test_link_1 = '/api/factory/find?creator.userId=testUserId&skipCount=0&maxItems=5'; + let test_rel_1 = 'first'; + let test_link_2 = '/api/factory/find?creator.userId=testUserId&skipCount=20&maxItems=5'; + let test_rel_2 = 'last'; + let test_link_3 = '/api/factory/find?creator.userId=testUserId&skipCount=5&maxItems=5'; + let test_rel_3 = 'next'; + + let headersLink = '\<' + test_link_1 + '\>' + '; rel="' + test_rel_1 + '",' + + '\<' + test_link_2 + '\>' + '; rel="' + test_rel_2 + '",' + + '\<' + test_link_3 + '\>' + '; rel="' + test_rel_3 + '"'; + + cheBackend.factoriesBackendSetup(); + + // gets page + let pageObject = factory._getPageFromResponse(factories, headersLink); + + httpBackend.flush(); + + // check page factories and links + expect(pageObject.factories).toEqual(factories); + expect(pageObject.links.get(test_rel_1)).toEqual(test_link_1); + expect(pageObject.links.get(test_rel_2)).toEqual(test_link_2); + expect(pageObject.links.get(test_rel_3)).toEqual(test_link_3); + } + ); + + /** + * Gets maxItems and skipCount from link params + */ + it('Gets maxItems and skipCount from link params', () => { + let skipCount = 20; + let maxItems = 5; + let test_link = '/api/factory/find?creator.userId=testUserId&skipCount=' + skipCount + '&maxItems=' + maxItems; + + cheBackend.factoriesBackendSetup(); + + // gets page + let pageParams = factory._getPageParamByLink(test_link); + + httpBackend.flush(); + + // check page factories and links + expect(parseInt(pageParams.maxItems, 10)).toEqual(maxItems); + expect(parseInt(pageParams.skipCount, 10)).toEqual(skipCount); + } + ); + +}); diff --git a/dashboard/src/components/api/che-user.factory.ts b/dashboard/src/components/api/che-user.factory.ts new file mode 100644 index 00000000000..d7574c69b63 --- /dev/null +++ b/dashboard/src/components/api/che-user.factory.ts @@ -0,0 +1,432 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; + +interface IUsersResource extends ng.resource.IResourceClass { + findByID: any, + findByAlias: any, + findByName: any, + setPassword: any, + createUser: any, + getUsers: any, + removeUserById: any +} + +/** + * This class is handling the user API retrieval + * @author Oleksii Orel + */ +export class CheUser { + private $resource: ng.resource.IResourceService; + private $q: ng.IQService; + private $cookies: ng.cookies.ICookiesService; + private remoteUserAPI: IUsersResource; + private logoutAPI: ng.resource.IResourceClass; + + private useridMap: Map; + private userAliasMap: Map; + private userNameMap: Map; + private usersMap: Map; + private userPagesMap: Map; + private pageInfo: any; + /** + * Current user. + */ + private user: che.IUser; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor($resource: ng.resource.IResourceService, $q: ng.IQService, $cookies: ng.cookies.ICookiesService) { + this.$q = $q; + this.$resource = $resource; + this.$cookies = $cookies; + + // remote call + this.remoteUserAPI = > this.$resource('/api/user', {}, { + findByID: {method: 'GET', url: '/api/user/:userId'}, + findByAlias: {method: 'GET', url: '/api/user/find?email=:alias'}, + findByName: {method: 'GET', url: '/api/user/find?name=:name'}, + setPassword: { + method: 'POST', url: '/api/user/password', isArray: false, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + } + }, + createUser: {method: 'POST', url: '/api/user'}, + getUsers: { + method: 'GET', + url: '/api/admin/user?maxItems=:maxItems&skipCount=:skipCount', + isArray: false, + responseType: 'json', + transformResponse: (data: any, headersGetter: any) => { + return this._getPageFromResponse(data, headersGetter('link')); + } + }, + removeUserById: {method: 'DELETE', url: '/api/user/:userId'} + }); + + this.logoutAPI = this.$resource('/api/auth/logout', {}); + + // users by ID + this.useridMap = new Map(); + + // users by alias + this.userAliasMap = new Map(); + + // users by name + this.userNameMap = new Map(); + + // all users by ID + this.usersMap = new Map(); + + // page users by relative link + this.userPagesMap = new Map(); + + //pages info + this.pageInfo = {}; + + //Current user has to be for sure fetched: + this.fetchUser(); + } + + /** + * Create new user + * @param name - new user name + * @param email - new user e-mail + * @param password - new user password + * @returns {*} + */ + createUser(name: string, email: string, password: string): ng.IPromise { + let data: { + password: string; + name: string; + email?: string; + }; + + data = { + password: password, + name: name + }; + + if (email) { + data.email = email; + } + + let promise = this.remoteUserAPI.createUser(data).$promise; + + // check if was OK or not + promise.then((user: che.IUser) => { + //update users map + this.usersMap.set(user.id, user);//add user + + }); + + return promise; + } + + _getPageFromResponse(data: any, headersLink: string): any { + let links = new Map(); + if (!headersLink) { + return {users: data}; + } + let pattern = new RegExp('<([^>]+?)>.+?rel="([^"]+?)"', 'g'); + let result; + while (result = pattern.exec(headersLink)) { //look for pattern + links.set(result[2], result[1]);//add link + } + return { + users: data, + links: links + }; + } + + _getPageParamByLink(pageLink: string): any { + let lastPageParamMap = new Map(); + let pattern = new RegExp('([_\\w]+)=([\\w]+)', 'g'); + let result; + while (result = pattern.exec(pageLink)) { + lastPageParamMap.set(result[1], result[2]); + } + + let skipCount = lastPageParamMap.get('skipCount'); + let maxItems = lastPageParamMap.get('maxItems'); + if (!maxItems || maxItems === 0) { + return null; + } + return { + maxItems: maxItems, + skipCount: skipCount ? skipCount : 0 + }; + } + + _updateCurrentPage(): void { + let pageData = this.userPagesMap.get(this.pageInfo.currentPageNumber); + if (!pageData) { + return; + } + this.usersMap.clear(); + if (!pageData.users) { + return; + } + pageData.users.forEach((user: che.IUser) => { + this.usersMap.set(user.id, user);//add user + }); + } + + _updateCurrentPageUsers(users: Array): void { + this.usersMap.clear(); + if (!users) { + return; + } + users.forEach((user: che.IUser) => { + this.usersMap.set(user.id, user);//add user + }); + } + + /** + * Update user page links by relative direction ('first', 'prev', 'next', 'last') + */ + _updatePagesData(data: any): any { + if (!data.links) { + return; + } + let firstPageLink = data.links.get('first'); + if (firstPageLink) { + let firstPageData: { users?: Array; link?: string; } = {link: firstPageLink}; + if (this.pageInfo.currentPageNumber === 1) { + firstPageData.users = data.users; + } + if (!this.userPagesMap.get(1) || firstPageData.users) { + this.userPagesMap.set(1, firstPageData); + } + } + let lastPageLink = data.links.get('last'); + if (lastPageLink) { + let pageParam = this._getPageParamByLink(lastPageLink); + this.pageInfo.countOfPages = pageParam.skipCount / pageParam.maxItems + 1; + this.pageInfo.count = pageParam.skipCount; + let lastPageData: { users?: Array; link?: string; } = {link: lastPageLink}; + if (this.pageInfo.currentPageNumber === this.pageInfo.countOfPages) { + lastPageData.users = data.users; + } + if (!this.userPagesMap.get(this.pageInfo.countOfPages) || lastPageData.users) { + this.userPagesMap.set(this.pageInfo.countOfPages, lastPageData); + } + } + let prevPageLink = data.links.get('prev'); + let prevPageNumber = this.pageInfo.currentPageNumber - 1; + if (prevPageNumber > 0 && prevPageLink) { + let prevPageData = {link: prevPageLink}; + if (!this.userPagesMap.get(prevPageNumber)) { + this.userPagesMap.set(prevPageNumber, prevPageData); + } + } + let nextPageLink = data.links.get('next'); + let nextPageNumber = this.pageInfo.currentPageNumber + 1; + if (nextPageNumber) { + let lastPageData = {link: nextPageLink}; + if (!this.userPagesMap.get(nextPageNumber)) { + this.userPagesMap.set(nextPageNumber, lastPageData); + } + } + } + + /** + * Gets the pageInfo + * @returns {Object} + */ + getPagesInfo(): any { + return this.pageInfo; + } + + /** + * Ask for loading the users in asynchronous way + * If there are no changes, it's not updated + * @param maxItems - the max number of items to return + * @param skipCount - the number of items to skip + * @returns {*} the promise + */ + fetchUsers(maxItems: number, skipCount: number): ng.IPromise { + let promise = this.remoteUserAPI.getUsers({maxItems: maxItems, skipCount: skipCount}).$promise; + + promise.then((data) => { + this.pageInfo.currentPageNumber = skipCount / maxItems + 1; + this._updateCurrentPageUsers(data.users); + this._updatePagesData(data); + }); + + return promise; + } + + /** + * Ask for loading the users page in asynchronous way + * If there are no changes, it's not updated + * @param pageKey - the key of page ('first', 'prev', 'next', 'last' or '1', '2', '3' ...) + * @returns {*} the promise + */ + fetchUsersPage(pageKey): ng.IPromise { + let deferred = this.$q.defer(); + let pageNumber; + if ('first' === pageKey) { + pageNumber = 1; + } else if ('prev' === pageKey) { + pageNumber = this.pageInfo.currentPageNumber - 1; + } else if ('next' === pageKey) { + pageNumber = this.pageInfo.currentPageNumber + 1; + } else if ('last' === pageKey) { + pageNumber = this.pageInfo.countOfPages; + } else { + pageNumber = parseInt(pageKey, 10); + } + if (pageNumber < 1) { + pageNumber = 1; + } else if (pageNumber > this.pageInfo.countOfPages) { + pageNumber = this.pageInfo.countOfPages; + } + let pageData = this.userPagesMap.get(pageNumber); + if (pageData.link) { + this.pageInfo.currentPageNumber = pageNumber; + let promise = this.remoteUserAPI.getUsers(this._getPageParamByLink(pageData.link)).$promise; + promise.then((data) => { + this._updatePagesData(data); + pageData.users = data.users; + this._updateCurrentPage(); + deferred.resolve(data); + }, (error) => { + if (error && error.status === 304) { + this._updateCurrentPage(); + } + deferred.reject(error); + }); + } else { + deferred.reject({data: {message: 'Error. No necessary link.'}}); + } + return deferred.promise; + } + + /** + * Gets the users + * @returns {Map} + */ + getUsersMap(): Map { + return this.usersMap; + } + + /** + * Performs user deleting by the given user ID. + * @param userId the user id + * @returns {*} the promise + */ + deleteUserById(userId: string): ng.IPromise { + let promise = this.remoteUserAPI.removeUserById({userId: userId}).$promise; + + // check if was OK or not + promise.then(() => { + //update users map + this.usersMap.delete(userId);//remove user + }); + + return promise; + } + + /** + * Performs current user deletion. + * @returns {*} the promise + */ + deleteCurrentUser(): ng.IPromise { + let userId = this.user.id; + let promise = this.remoteUserAPI.removeUserById({userId: userId}).$promise; + return promise; + } + + /** + * Performs logout of current user. + * @returns {y.ResourceClass.prototype.$promise|*|$promise|T.$promise|S.$promise} + */ + logout(): ng.IPromise { + let data = {token: this.$cookies['session-access-key']}; + let promise = this.logoutAPI.save(data).$promise; + return promise; + } + + /** + * Gets the user + * @return user + */ + getUser(): any { + return this.user; + } + + /** + * Fetch the user + */ + fetchUser(): ng.IPromise { + let promise = this.remoteUserAPI.get().$promise; + // check if if was OK or not + promise.then((user: che.IUser) => { + this.user = user; + }); + + return promise; + } + + fetchUserId(userId: string): ng.IPromise { + let promise = this.remoteUserAPI.findByID({userId: userId}).$promise; + let parsedResultPromise = promise.then((user: che.IUser) => { + this.useridMap.set(userId, user); + }); + + return parsedResultPromise; + } + + getUserFromId(userId: string): any { + return this.useridMap.get(userId); + } + + fetchUserByAlias(alias: string): ng.IPromise { + let promise = this.remoteUserAPI.findByAlias({alias: alias}).$promise; + let parsedResultPromise = promise.then((user: che.IUser) => { + this.useridMap.set(user.id, user); + this.userAliasMap.set(alias, user); + }); + + return parsedResultPromise; + } + + getUserByAlias(userAlias: string): any { + return this.userAliasMap.get(userAlias); + } + + fetchUserByName(name: string): ng.IPromise { + let promise = this.remoteUserAPI.findByName({name: name}).$promise; + let resultPromise = promise.then((user: che.IUser) => { + this.userNameMap.set(name, user); + return user; + }, (error: any) => { + if (error.status === 304) { + return this.userNameMap.get(name); + } + return this.$q.reject(error); + }); + + return resultPromise; + } + + getUserByName(name: string): any { + return this.userNameMap.get(name); + } + + setPassword(password: any): ng.IPromise { + return this.remoteUserAPI.setPassword('password=' + password).$promise; + } +} diff --git a/dashboard/src/components/api/che-user.spec.ts b/dashboard/src/components/api/che-user.spec.ts new file mode 100644 index 00000000000..89ca2feab56 --- /dev/null +++ b/dashboard/src/components/api/che-user.spec.ts @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +'use strict'; +import {CheUser} from './che-user.factory'; +import {CheAPIBuilder} from './builder/che-api-builder.factory'; +import {CheHttpBackend} from './test/che-http-backend'; + +/** + * Test of the Codenvy User API + */ +describe('CheUser', () => { + + /** + * User Factory for the test + */ + let factory; + + /** + * API builder. + */ + let apiBuilder; + + /** + * Backend for handling http operations + */ + let httpBackend; + + /** + * Che backend + */ + let cheBackend; + + /** + * setup module + */ + beforeEach(angular.mock.module('userDashboard')); + + /** + * Inject factory and http backend + */ + beforeEach(inject((cheUser: CheUser, cheAPIBuilder: CheAPIBuilder, cheHttpBackend: CheHttpBackend) => { + + factory = cheUser; + apiBuilder = cheAPIBuilder; + cheBackend = cheHttpBackend; + httpBackend = cheHttpBackend.getHttpBackend(); + })); + + /** + * Check assertion after the test + */ + afterEach(() => { + httpBackend.verifyNoOutstandingExpectation(); + httpBackend.verifyNoOutstandingRequest(); + }); + + /** + * Check that we're able to fetch user data + */ + it('Fetch user', () => { + // setup tests objects + let userId = 'idTestUser'; + let email = 'eclipseCodenvy@eclipse.org'; + + let testUser = apiBuilder.getUserBuilder().withId(userId).withEmail(email).build(); + + // providing request + // add test user on Http backend + cheBackend.setDefaultUser(testUser); + + // setup backend for users + cheBackend.setup(); + + // fetch user + factory.fetchUser(true); + + // expecting GETs + httpBackend.expectGET('/api/user'); + + // flush command + httpBackend.flush(); + + // now, check user + let user = factory.getUser(); + + // check id and email + expect(user.id).toEqual(userId); + expect(user.email).toEqual(email); + } + ); + + /** + * Check that we're able to fetch user data by id + */ + it('Fetch user by id', () => { + // setup tests objects + let userId = 'newIdTestUser'; + let email = 'eclipseCodenvy@eclipse.org'; + + let testUser = apiBuilder.getUserBuilder().withId(userId).withEmail(email).build(); + + // providing request + // add test user on Http backend + cheBackend.addUserById(testUser); + + // setup backend + cheBackend.setup(); + + // fetch user + factory.fetchUserId(userId); + + // expecting GETs + httpBackend.expectGET('/api/user/' + userId); + + // flush command + httpBackend.flush(); + + // now, check user + let user = factory.getUserFromId(userId); + + // check id and email + expect(user.id).toEqual(userId); + expect(user.email).toEqual(email); + } + ); + + /** + * Check that we're able to fetch user data by email + */ + it('Fetch user by alias', () => { + // setup tests objects + let userId = 'testUser'; + let email = 'eclipseCodenvy@eclipse.org'; + + let testUser = apiBuilder.getUserBuilder().withId(userId).withEmail(email).build(); + + // providing request + // add test user on Http backend + cheBackend.addUserEmail(testUser); + + // setup backend + cheBackend.setup(); + + // fetch user + factory.fetchUserByAlias(email); + + // expecting GETs + httpBackend.expectGET('/api/user/find?email=' + email); + + // flush command + httpBackend.flush(); + + // now, check user + let user = factory.getUserByAlias(email); + + // check id and email + expect(user.id).toEqual(userId); + expect(user.email).toEqual(email); + } + ); + + /** + * Check that we're able to set attributes into profile + */ + it('Set password', () => { + // setup + let testPassword = 'newTestPassword'; + + // setup backend + cheBackend.setup(); + + // fetch profile + factory.setPassword(testPassword); + + // expecting a POST + httpBackend.expectPOST('/api/user/password', 'password=' + testPassword); + + // flush command + httpBackend.flush(); + } + ); + + /** + * Check that we're able to create user + */ + it('Create user', () => { + let user = { + password: 'password12345', + email: 'eclipseCodenvy@eclipse.org', + name: 'testName' + }; + + // setup backend + cheBackend.setup(); + + // create user + factory.createUser(user.name, user.email, user.password); + + // expecting a POST + httpBackend.expectPOST('/api/user', user); + + // flush command + httpBackend.flush(); + } + ); + + + /** + * Gets user page object from response + */ + it('Gets user page object from response', () => { + let testUser_1 = apiBuilder.getUserBuilder().withId('testUser1Id').withEmail('testUser1@eclipse.org').build(); + let testUser_2 = apiBuilder.getUserBuilder().withId('testUser2Id').withEmail('testUser2@eclipse.org').build(); + let users = [testUser_1, testUser_2]; + + let test_link_1 = 'https://aio.codenvy-dev.com/api/admin/user?skipCount=0&maxItems=5'; + let test_rel_1 = 'first'; + let test_link_2 = 'https://aio.codenvy-dev.com/api/admin/user?skipCount=20&maxItems=5'; + let test_rel_2 = 'last'; + let test_link_3 = 'https://aio.codenvy-dev.com/api/admin/user?skipCount=5&maxItems=5'; + let test_rel_3 = 'next'; + + let headersLink = '\<' + test_link_1 + '\>' + '; rel="' + test_rel_1 + '",' + + '\<' + test_link_2 + '\>' + '; rel="' + test_rel_2 + '",' + + '\<' + test_link_3 + '\>' + '; rel="' + test_rel_3 + '"'; + + // setup backend + cheBackend.setup(); + + // gets page + let pageObject = factory._getPageFromResponse(users, headersLink); + + // flush command + httpBackend.flush(); + + // check page users and links + expect(pageObject.users).toEqual(users); + expect(pageObject.links.get(test_rel_1)).toEqual(test_link_1); + expect(pageObject.links.get(test_rel_2)).toEqual(test_link_2); + expect(pageObject.links.get(test_rel_3)).toEqual(test_link_3); + } + ); + + /** + * Gets maxItems and skipCount from link params + */ + it('Gets maxItems and skipCount from link params', () => { + let skipCount = 20; + let maxItems = 5; + let test_link = 'https://aio.codenvy-dev.com/api/admin/user?skipCount=' + skipCount + '&maxItems=' + maxItems; + + // setup backend + cheBackend.setup(); + + // gets page + let pageParams = factory._getPageParamByLink(test_link); + + // flush command + httpBackend.flush(); + + // check page users and links + expect(parseInt(pageParams.maxItems, 10)).toEqual(maxItems); + expect(parseInt(pageParams.skipCount, 10)).toEqual(skipCount); + } + ); + +}); diff --git a/dashboard/src/components/api/namespace/che.namespace.d.ts b/dashboard/src/components/api/namespace/che.namespace.d.ts new file mode 100644 index 00000000000..a51d400377c --- /dev/null +++ b/dashboard/src/components/api/namespace/che.namespace.d.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2015-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + */ +declare namespace che.namespace { + + + +} diff --git a/dashboard/src/components/api/test/che-http-backend.ts b/dashboard/src/components/api/test/che-http-backend.ts index 216b23ba28f..6fc3bd9aeca 100644 --- a/dashboard/src/components/api/test/che-http-backend.ts +++ b/dashboard/src/components/api/test/che-http-backend.ts @@ -31,6 +31,14 @@ export class CheHttpBackend { private defaultProfilePrefs: any; private defaultBranding: any; private defaultPreferences: any; + private defaultUser: che.IUser; + private userIdMap: Map; + private userEmailMap: Map; + private factoriesMap: Map; + private pageMaxItem: number; + private pageSkipCount: number; + + private isAutoSnapshot: boolean = false; private isAutoRestore: boolean = false; @@ -51,6 +59,13 @@ export class CheHttpBackend { this.workspaceAgentMap = new Map(); this.stacks = []; + this.defaultUser = {}; + this.userIdMap = new Map(); + this.userEmailMap = new Map(); + this.factoriesMap = new Map(); + this.pageMaxItem = 5; + this.pageSkipCount = 0; + this.defaultProfile = cheAPIBuilder.getProfileBuilder().withId('idDefaultUser').withEmail('eclipseChe@eclipse.org').withFirstName('FirstName').withLastName('LastName').build(); this.defaultProfilePrefs = {}; this.defaultBranding = {}; @@ -113,6 +128,27 @@ export class CheHttpBackend { this.httpBackend.when('POST', '/api/analytics/log/session-usage').respond(200, {}); + // change password + this.httpBackend.when('POST', '/api/user/password').respond(() => { + return [200, {success: true, errors: []}]; + }); + + // create new user + this.httpBackend.when('POST', '/api/user').respond(() => { + return [200, {success: true, errors: []}]; + }); + + this.httpBackend.when('GET', '/api/user').respond(this.defaultUser); + + let userIdKeys = this.userIdMap.keys(); + for (let key of userIdKeys) { + this.httpBackend.when('GET', '/api/user/' + key).respond(this.userIdMap.get(key)); + } + + let userEmailKeys = this.userEmailMap.keys(); + for (let key of userEmailKeys) { + this.httpBackend.when('GET', '/api/user/find?email=' + key).respond(this.userEmailMap.get(key)); + } } /** @@ -387,4 +423,84 @@ export class CheHttpBackend { this.httpBackend.when('POST', this.workspaceAgentMap.get(workspaceId) + '/svn/info?workspaceId=' + workspaceId).respond(svnInfo); } + /** + * Setup Backend for factories + */ + factoriesBackendSetup() { + this.setup(); + + let allFactories = []; + let pageFactories = []; + + let factoriesKeys = this.factoriesMap.keys(); + for (let key of factoriesKeys) { + let factory = this.factoriesMap.get(key); + this.httpBackend.when('GET', '/api/factory/' + factory.id).respond(factory); + this.httpBackend.when('DELETE', '/api/factory/' + factory.id).respond(() => { + return [200, {success: true, errors: []}]; + }); + allFactories.push(factory); + } + + if (this.defaultUser) { + this.httpBackend.when('GET', '/api/user').respond(this.defaultUser); + + if (allFactories.length > this.pageSkipCount) { + if(allFactories.length > this.pageSkipCount + this.pageMaxItem) { + pageFactories = allFactories.slice(this.pageSkipCount, this.pageSkipCount + this.pageMaxItem); + } else { + pageFactories = allFactories.slice(this.pageSkipCount); + } + } + this.httpBackend.when('GET', '/api/factory/find?creator.userId=' + this.defaultUser.id + '&maxItems=' + this.pageMaxItem + '&skipCount=' + this.pageSkipCount).respond(pageFactories); + } + } + + /** + * Add the given factory + * @param factory + */ + addUserFactory(factory) { + this.factoriesMap.set(factory.id, factory); + } + + /** + * Sets max objects on response + * @param pageMaxItem + */ + setPageMaxItem(pageMaxItem) { + this.pageMaxItem = pageMaxItem; + } + + /** + * Sets skip count of values + * @param pageSkipCount + */ + setPageSkipCount(pageSkipCount) { + this.pageSkipCount = pageSkipCount; + } + + /** + * Add the given user + * @param user + */ + setDefaultUser(user) { + this.defaultUser = user; + } + + /** + * Add the given user to userIdMap + * @param user + */ + addUserById(user) { + this.userIdMap.set(user.id, user); + } + + /** + * Add the given user to userEmailMap + * @param user + */ + addUserEmail(user) { + this.userEmailMap.set(user.email, user); + } } diff --git a/dashboard/src/components/typings/che.d.ts b/dashboard/src/components/typings/che.d.ts index 349a1ba67fe..596715f1182 100644 --- a/dashboard/src/components/typings/che.d.ts +++ b/dashboard/src/components/typings/che.d.ts @@ -227,4 +227,22 @@ declare namespace _che { label: string; location: string; } + + export interface IUser { + id: string; + name: string; + email: string; + aliases: Array; + } + + export interface IFactory { + id: string; + name?: string; + v: string; + workspace: IWorkspaceConfig; + creator: any; + ide?: any; + button?: any; + policies?: any; + } } diff --git a/ide/che-core-ide-app/pom.xml b/ide/che-core-ide-app/pom.xml index e47c499dd9d..f27e41f8221 100644 --- a/ide/che-core-ide-app/pom.xml +++ b/ide/che-core-ide-app/pom.xml @@ -69,6 +69,10 @@ org.eclipse.che.core che-core-api-factory-shared
+ + org.eclipse.che.core + che-core-api-git-shared + org.eclipse.che.core che-core-api-machine-shared diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/CoreLocalizationConstant.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/CoreLocalizationConstant.java index 7ede481944c..b1b14443478 100644 --- a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/CoreLocalizationConstant.java +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/CoreLocalizationConstant.java @@ -943,4 +943,109 @@ public interface CoreLocalizationConstant extends Messages { @Key("authentication.dialog.rejected.by.user") String authenticationDialogRejectedByUser(); + + /* Factories */ + @Key("projects.import.configuring.cloning") + String cloningSource(); + + @Key("create.factory.action.title") + String createFactoryActionTitle(); + + @Key("create.factory.already.exist") + String createFactoryAlreadyExist(); + + @Key("create.factory.unable.create.from.current.workspace") + String createFactoryFromCurrentWorkspaceFailed(); + + @Key("create.factory.form.title") + String createFactoryTitle(); + + @Key("create.factory.label.name") + String createFactoryName(); + + @Key("create.factory.label.link") + String createFactoryLink(); + + @Key("create.factory.button.create") + String createFactoryButton(); + + @Key("create.factory.button.close") + String createFactoryButtonClose(); + + @Key("create.factory.configure.button.tooltip") + String createFactoryConfigureTooltip(); + + @Key("create.factory.launch.button.tooltip") + String createFactoryLaunchTooltip(); + + + @Key("import.config.view.name") + String importFromConfigurationName(); + + @Key("import.config.view.description") + String importFromConfigurationDescription(); + + @Key("project.import.configured.cloned") + String clonedSource(String projectName); + + @Key("import.config.form.button.import") + String importButton(); + + @Key("import.config.view.title") + String importFromConfigurationTitle(); + + @Key("import.config.form.prompt") + String configFileTitle(); + + @Key("project.already.imported") + String projectAlreadyImported(String projectName); + + @Key("project.import.cloned.with.checkout") + String clonedSourceWithCheckout(String projectName, String repoName, String ref, String branch); + + @Key("project.import.cloned.with.checkout.start.point") + String clonedWithCheckoutOnStartPoint(String projectName, String repoName, String startPoint, String branch); + + @Key("project.import.configuring.cloning") + String cloningSource(String projectName); + + @Key("project.import.ssh.key.upload.failed.title") + String cloningSourceSshKeyUploadFailedTitle(); + + @Key("project.import.ssh.key.upload.failed.text") + String cloningSourcesSshKeyUploadFailedText(); + + @Key("message.ssh.key.not.found.text") + String acceptSshNotFoundText(); + + @Key("project.import.cloning.failed.without.start.point") + String cloningSourceWithCheckoutFailed(String branch, String repoName); + + @Key("project.import.cloning.failed.with.start.point") + String cloningSourceCheckoutFailed(String project, String branch); + + @Key("project.import.cloning.failed.title") + String cloningSourceFailedTitle(String projectName); + + @Key("project.import.configuring.failed") + String configuringSourceFailed(String projectName); + + @Key("welcome.preferences.title") + String welcomePreferencesTitle(); + + @Key("export.config.view.name") + String exportConfigName(); + + @Key("export.config.view.description") + String exportConfigDescription(); + + @Key("export.config.error.message") + String exportConfigErrorMessage(); + + @Key("export.config.dialog.not.under.vcs.title") + String exportConfigDialogNotUnderVcsTitle(); + + @Key("export.config.dialog.not.under.vcs.text") + String exportConfigDialogNotUnderVcsText(); + } diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/core/CoreGinModule.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/core/CoreGinModule.java index 0bbc8d47587..f2b004f8e62 100644 --- a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/core/CoreGinModule.java +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/core/CoreGinModule.java @@ -48,7 +48,7 @@ import org.eclipse.che.ide.debug.DebugApiModule; import org.eclipse.che.ide.editor.EditorApiModule; import org.eclipse.che.ide.editor.preferences.EditorPreferencesModule; -import org.eclipse.che.ide.factory.FactoryApiModule; +import org.eclipse.che.ide.factory.inject.FactoryGinModule; import org.eclipse.che.ide.filetypes.FileTypeApiModule; import org.eclipse.che.ide.keybinding.KeyBindingManager; import org.eclipse.che.ide.machine.MachineApiModule; @@ -110,7 +110,7 @@ protected void configure() { install(new ProjectApiModule()); install(new ProjectImportModule()); install(new OAuthApiModule()); - install(new FactoryApiModule()); + install(new FactoryGinModule()); // configure miscellaneous core components bind(StandardComponentInitializer.class).in(Singleton.class); diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryExtension.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryExtension.java new file mode 100644 index 00000000000..8b7c8875820 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryExtension.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory; + +import com.google.gwt.core.client.Callback; +import com.google.gwt.core.client.ScriptInjector; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.ide.api.action.ActionManager; +import org.eclipse.che.ide.api.action.DefaultActionGroup; +import org.eclipse.che.ide.api.extension.Extension; +import org.eclipse.che.ide.factory.accept.AcceptFactoryHandler; +import org.eclipse.che.ide.factory.action.CreateFactoryAction; +import org.eclipse.che.ide.factory.json.ImportFromConfigAction; +import org.eclipse.che.ide.factory.welcome.OpenWelcomePageAction; + +import static com.google.gwt.core.client.ScriptInjector.TOP_WINDOW; +import static org.eclipse.che.ide.api.action.IdeActions.GROUP_PROJECT; +import static org.eclipse.che.ide.api.action.IdeActions.GROUP_WORKSPACE; + +/** + * @author Vladyslav Zhukovskii + */ +@Singleton +@Extension(title = "Factory", version = "3.0.0") +public class FactoryExtension { + + @Inject + public FactoryExtension(AcceptFactoryHandler acceptFactoryHandler, + ActionManager actionManager, + FactoryResources resources, + CreateFactoryAction configureFactoryAction, + ImportFromConfigAction importFromConfigAction, + OpenWelcomePageAction openWelcomePageAction) { + acceptFactoryHandler.process(); + + /* + * Inject resources and js + */ + ScriptInjector.fromUrl("https://apis.google.com/js/client:plusone.js?parsetags=explicit") + .setWindow(TOP_WINDOW) + .inject(); + + ScriptInjector.fromUrl("https://connect.facebook.net/en_US/sdk.js") + .setWindow(TOP_WINDOW) + .setCallback(new Callback() { + @Override + public void onSuccess(Void result) { + init(); + } + + @Override + public void onFailure(Exception reason) { + } + + private native void init() /*-{ + $wnd.FB.init({ + appId: "318167898391385", + xfbml: true, + version: "v2.1" + }); + }-*/; + }).inject(); + + resources.factoryCSS().ensureInjected(); + + DefaultActionGroup projectGroup = (DefaultActionGroup)actionManager.getAction(GROUP_PROJECT); + DefaultActionGroup workspaceGroup = (DefaultActionGroup)actionManager.getAction(GROUP_WORKSPACE); + + actionManager.registerAction("openWelcomePage", openWelcomePageAction); + actionManager.registerAction("importProjectFromCodenvyConfigAction", importFromConfigAction); + actionManager.registerAction("configureFactoryAction", configureFactoryAction); + + projectGroup.add(importFromConfigAction); + workspaceGroup.add(configureFactoryAction); + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryResources.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryResources.java new file mode 100644 index 00000000000..80e31cab802 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryResources.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory; + +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; + +import org.eclipse.che.ide.ui.Styles; +import org.vectomatic.dom.svg.ui.SVGResource; + +/** + * Factory extension resources (css styles, images). + * + * @author Ann Shumilova + * @author Anton Korneta + */ +public interface FactoryResources extends ClientBundle { + interface FactoryCSS extends CssResource, Styles { + String label(); + + String createFactoryButton(); + + String labelErrorPosition(); + } + + interface Style extends CssResource { + String launchIcon(); + + String configureIcon(); + } + + @Source({"Factory.css", "org/eclipse/che/ide/api/ui/style.css", "org/eclipse/che/ide/ui/Styles.css"}) + FactoryCSS factoryCSS(); + + @Source("export-config.svg") + SVGResource exportConfig(); + + @Source("import-config.svg") + SVGResource importConfig(); + + @Source("execute.svg") + SVGResource execute(); + + @Source("cog-icon.svg") + SVGResource configure(); +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/accept/AcceptFactoryHandler.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/accept/AcceptFactoryHandler.java new file mode 100644 index 00000000000..b1d015acc0a --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/accept/AcceptFactoryHandler.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.accept; + +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.inject.Singleton; +import com.google.web.bindery.event.shared.EventBus; + +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.factory.shared.dto.IdeActionDto; +import org.eclipse.che.api.factory.shared.dto.IdeDto; +import org.eclipse.che.ide.CoreLocalizationConstant; +import org.eclipse.che.ide.api.action.ActionManager; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.factory.FactoryAcceptedEvent; +import org.eclipse.che.ide.api.machine.events.WsAgentStateEvent; +import org.eclipse.che.ide.api.machine.events.WsAgentStateHandler; +import org.eclipse.che.ide.api.notification.NotificationManager; +import org.eclipse.che.ide.api.notification.StatusNotification; +import org.eclipse.che.ide.factory.utils.FactoryProjectImporter; + +import javax.inject.Inject; + +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.NOT_EMERGE_MODE; + +/** + * @author Sergii Leschenko + * @author Anton Korneta + */ +@Singleton +public class AcceptFactoryHandler { + private final CoreLocalizationConstant localizationConstant; + private final FactoryProjectImporter factoryProjectImporter; + private final EventBus eventBus; + private final AppContext appContext; + private final ActionManager actionManager; + private final NotificationManager notificationManager; + + private StatusNotification notification; + private boolean isImportingStarted; + + @Inject + public AcceptFactoryHandler(CoreLocalizationConstant localizationConstant, + FactoryProjectImporter factoryProjectImporter, + EventBus eventBus, + AppContext appContext, + ActionManager actionManager, + NotificationManager notificationManager) { + this.factoryProjectImporter = factoryProjectImporter; + this.localizationConstant = localizationConstant; + this.eventBus = eventBus; + this.appContext = appContext; + this.actionManager = actionManager; + this.notificationManager = notificationManager; + } + + /** + * Accepts factory if it is present in context of application + */ + public void process() { + final FactoryDto factory; + if ((factory = appContext.getFactory()) == null) { + return; + } + eventBus.addHandler(WsAgentStateEvent.TYPE, new WsAgentStateHandler() { + @Override + public void onWsAgentStarted(final WsAgentStateEvent event) { + if (isImportingStarted) { + return; + } + + isImportingStarted = true; + + notification = notificationManager + .notify(localizationConstant.cloningSource(), StatusNotification.Status.PROGRESS, NOT_EMERGE_MODE); + performOnAppLoadedActions(factory); + startImporting(factory); + } + + @Override + public void onWsAgentStopped(WsAgentStateEvent event) { + + } + }); + } + + private void startImporting(final FactoryDto factory) { + factoryProjectImporter.startImporting(factory, + new AsyncCallback() { + @Override + public void onSuccess(Void result) { + notification.setStatus(StatusNotification.Status.SUCCESS); + notification.setContent(localizationConstant.cloningSource()); + performOnProjectsLoadedActions(factory); + } + + @Override + public void onFailure(Throwable throwable) { + notification.setStatus(StatusNotification.Status.FAIL); + notification.setContent(throwable.getMessage()); + } + }); + } + + private void performOnAppLoadedActions(final FactoryDto factory) { + final IdeDto ide = factory.getIde(); + if (ide == null || ide.getOnAppLoaded() == null) { + return; + } + for (IdeActionDto action : ide.getOnAppLoaded().getActions()) { + actionManager.performAction(action.getId(), action.getProperties()); + } + } + + private void performOnProjectsLoadedActions(final FactoryDto factory) { + final IdeDto ide = factory.getIde(); + if (ide == null || ide.getOnProjectsLoaded() == null) { + eventBus.fireEvent(new FactoryAcceptedEvent(factory)); + return; + } + for (IdeActionDto action : ide.getOnProjectsLoaded().getActions()) { + actionManager.performAction(action.getId(), action.getProperties()); + } + eventBus.fireEvent(new FactoryAcceptedEvent(factory)); + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/action/CreateFactoryAction.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/action/CreateFactoryAction.java new file mode 100644 index 00000000000..0694ede6a2b --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/action/CreateFactoryAction.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.action; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.ide.CoreLocalizationConstant; +import org.eclipse.che.ide.api.action.AbstractPerspectiveAction; +import org.eclipse.che.ide.api.action.ActionEvent; +import org.eclipse.che.ide.factory.configure.CreateFactoryPresenter; + +import javax.validation.constraints.NotNull; +import java.util.Collections; + +/** + * @author Anton Korneta + */ +@Singleton +public class CreateFactoryAction extends AbstractPerspectiveAction { + + private final CreateFactoryPresenter presenter; + + @Inject + public CreateFactoryAction(CreateFactoryPresenter presenter, + CoreLocalizationConstant localizationConstant) { + super(Collections.singletonList("Project Perspective"), localizationConstant.createFactoryActionTitle(), null, null, null); + this.presenter = presenter; + } + + @Override + public void actionPerformed(ActionEvent e) { + presenter.showDialog(); + } + + @Override + public void updateInPerspective(@NotNull ActionEvent event) { + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryPresenter.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryPresenter.java new file mode 100644 index 00000000000..221833780d2 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryPresenter.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.configure; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.api.core.rest.shared.dto.Link; +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.ide.CoreLocalizationConstant; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.factory.FactoryServiceClient; +import org.eclipse.che.ide.util.Pair; + +import java.util.Collections; +import java.util.List; + +/** + * @author Anton Korneta + */ +@Singleton +public class CreateFactoryPresenter implements CreateFactoryView.ActionDelegate { + public static final String CONFIGURE_LINK = "/dashboard/#/factory/"; + + private final CreateFactoryView view; + private final AppContext appContext; + private final FactoryServiceClient factoryService; + private final CoreLocalizationConstant localizationConstant; + + @Inject + public CreateFactoryPresenter(CreateFactoryView view, + AppContext appContext, + FactoryServiceClient factoryService, + CoreLocalizationConstant localizationConstant) { + this.view = view; + this.appContext = appContext; + this.factoryService = factoryService; + this.localizationConstant = localizationConstant; + view.setDelegate(this); + } + + public void showDialog() { + view.showDialog(); + } + + @Override + public void onCreateClicked() { + final String factoryName = view.getFactoryName(); + factoryService.getFactoryJson(appContext.getWorkspace().getId(), null) + .then(new Operation() { + @Override + public void apply(final FactoryDto factory) throws OperationException { + factoryService.findFactory(null, null, Collections.singletonList(Pair.of("name", factoryName))) + .then(saveFactory(factory, factoryName)) + .catchError(logError()); + } + }) + .catchError(logError()); + } + + @Override + public void onFactoryNameChanged(String factoryName) { + view.enableCreateFactoryButton(isValidFactoryName(factoryName)); + } + + @Override + public void onCancelClicked() { + view.close(); + } + + private Operation> saveFactory(final FactoryDto factory, final String factoryName) { + return new Operation>() { + @Override + public void apply(List factories) throws OperationException { + if (!factories.isEmpty()) { + view.showFactoryNameError(localizationConstant.createFactoryAlreadyExist(), null); + } else { + factoryService.saveFactory(factory.withName(factoryName)) + .then(new Operation() { + @Override + public void apply(FactoryDto factory) throws OperationException { + final Link link = factory.getLink("accept-named"); + if (link != null) { + view.setAcceptFactoryLink(link.getHref()); + } + view.setConfigureFactoryLink(CONFIGURE_LINK + factory.getId() + "/configure"); + } + }) + .catchError(logError()); + } + } + }; + } + + private Operation logError() { + return err -> view.showFactoryNameError(localizationConstant.createFactoryFromCurrentWorkspaceFailed(), err.getMessage()); + } + + private boolean isValidFactoryName(String name) { + if (name.length() == 0 || name.length() >= 125) { + return false; + } + view.hideFactoryNameError(); + return true; + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryView.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryView.java new file mode 100644 index 00000000000..ab3168e9eb4 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryView.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.configure; + +import com.google.inject.ImplementedBy; + +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.ide.api.mvp.View; + +import javax.validation.constraints.NotNull; + +/** + * Representation of create factory popup. + * + * @author Anton Korneta + */ +@ImplementedBy(CreateFactoryViewImpl.class) +public interface CreateFactoryView extends View { + + interface ActionDelegate { + + /** Performs any actions appropriate in response to the user having pressed the Create button */ + void onCreateClicked(); + + /** Performs any actions appropriate in response to the user having type into Factory name input */ + void onFactoryNameChanged(String factoryName); + + /** Performs any actions appropriate in response to the user having pressed the Cancel button. */ + void onCancelClicked(); + } + + /** Preforms closing create factory popup */ + void close(); + + /** Preforms showing create factory popup */ + void showDialog(); + + /** Gets factory name from input */ + String getFactoryName(); + + /** Set accept factory link */ + void setAcceptFactoryLink(@NotNull String acceptLink); + + /** Set accept factory link */ + void setConfigureFactoryLink(@NotNull String configureLink); + + /** Set enable create factory button */ + void enableCreateFactoryButton(boolean enabled); + + /** Shows error if factory name invalid */ + void showFactoryNameError(@NotNull String labelMessage, @Nullable String tooltipMessage); + + /** Hide error of factory name is valid */ + void hideFactoryNameError(); +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryViewImpl.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryViewImpl.java new file mode 100644 index 00000000000..09ca6192342 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryViewImpl.java @@ -0,0 +1,206 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.configure; + +import com.google.common.base.Strings; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.regexp.shared.RegExp; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.Widget; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.ide.CoreLocalizationConstant; +import org.eclipse.che.ide.factory.FactoryResources; +import org.eclipse.che.ide.ui.Tooltip; +import org.eclipse.che.ide.ui.menu.PositionController; +import org.eclipse.che.ide.ui.window.Window; +import org.eclipse.che.ide.ui.zeroclipboard.ClipboardButtonBuilder; + +import javax.validation.constraints.NotNull; + +import static com.google.gwt.dom.client.Style.Unit; + +/** + * @author Anton Korneta + */ +@Singleton +public class CreateFactoryViewImpl extends Window implements CreateFactoryView { + private static final RegExp FACTORY_NAME_PATTERN = RegExp.compile("[^A-Za-z0-9_-]"); + + + interface FactoryViewImplUiBinder extends UiBinder { + } + + private final FactoryResources factoryResources; + + private ActionDelegate delegate; + + @UiField + FactoryResources.Style style; + @UiField + TextBox factoryName; + @UiField + TextBox factoryLink; + @UiField + Label factoryNameLabel; + @UiField + Label factoryLinkLabel; + @UiField + Label factoryNameErrorLabel; + @UiField + Button createFactoryButton; + @UiField + FlowPanel upperPanel; + @UiField + FlowPanel lowerPanel; + @UiField + FlowPanel createFactoryPanel; + @UiField + Anchor launch; + @UiField + Anchor configure; + + private Tooltip labelsErrorTooltip; + + @Inject + protected CreateFactoryViewImpl(FactoryViewImplUiBinder uiBinder, + CoreLocalizationConstant locale, + FactoryResources factoryResources, + ClipboardButtonBuilder buttonBuilder) { + this.factoryResources = factoryResources; + setTitle(locale.createFactoryTitle()); + setWidget(uiBinder.createAndBindUi(this)); + factoryNameLabel.setText(locale.createFactoryName()); + factoryLinkLabel.setText(locale.createFactoryLink()); + configure.getElement().insertFirst(factoryResources.configure().getSvg().getElement()); + launch.getElement().insertFirst(factoryResources.execute().getSvg().getElement()); + launch.addStyleName(style.launchIcon()); + configure.addStyleName(style.configureIcon()); + createFactoryButton.setEnabled(false); + Button cancelButton = + createButton(locale.createFactoryButtonClose(), "git-remotes-pull-cancel", event -> delegate.onCancelClicked()); + createFactoryButton.addClickHandler(clickEvent -> delegate.onCreateClicked()); + cancelButton.ensureDebugId("projectReadOnlyGitUrl-btnClose"); + addButtonToFooter(cancelButton); + getWidget().getElement().getStyle().setPadding(0, Unit.PX); + buttonBuilder.withResourceWidget(factoryLink).build(); + factoryLink.setReadOnly(true); + final Tooltip launchFactoryTooltip = Tooltip.create((elemental.dom.Element)launch.getElement(), + PositionController.VerticalAlign.TOP, + PositionController.HorizontalAlign.MIDDLE, + locale.createFactoryLaunchTooltip()); + launchFactoryTooltip.setShowDelayDisabled(false); + + final Tooltip configureFactoryTooltip = Tooltip.create((elemental.dom.Element)configure.getElement(), + PositionController.VerticalAlign.TOP, + PositionController.HorizontalAlign.MIDDLE, + locale.createFactoryConfigureTooltip()); + configureFactoryTooltip.setShowDelayDisabled(false); + factoryName.getElement().setAttribute("placeholder", "new-factory-name"); + } + + @Override + public void setDelegate(ActionDelegate delegate) { + this.delegate = delegate; + } + + @Override + public void showDialog() { + clear(); + this.show(); + } + + @Override + public String getFactoryName() { + return factoryName.getText(); + } + + @Override + public void setAcceptFactoryLink(String acceptLink) { + factoryLink.setText(acceptLink); + launch.getElement().setAttribute("target", "_blank"); + launch.setHref(acceptLink); + } + + @Override + public void setConfigureFactoryLink(String configureLink) { + configure.getElement().setAttribute("target", "_blank"); + configure.setHref(configureLink); + } + + @Override + public void enableCreateFactoryButton(boolean enabled) { + createFactoryButton.setEnabled(enabled); + } + + @Override + public void showFactoryNameError(@NotNull String labelMessage, @Nullable String tooltipMessage) { + factoryName.addStyleName(factoryResources.factoryCSS().inputError()); + factoryNameErrorLabel.setText(labelMessage); + if (labelsErrorTooltip != null) { + labelsErrorTooltip.destroy(); + } + + if (!Strings.isNullOrEmpty(tooltipMessage)) { + labelsErrorTooltip = Tooltip.create((elemental.dom.Element)factoryNameErrorLabel.getElement(), + PositionController.VerticalAlign.TOP, + PositionController.HorizontalAlign.MIDDLE, + tooltipMessage); + labelsErrorTooltip.setShowDelayDisabled(false); + } + } + + @Override + public void hideFactoryNameError() { + factoryName.removeStyleName(factoryResources.factoryCSS().inputError()); + factoryNameErrorLabel.setText(""); + } + + @Override + public void close() { + this.hide(); + } + + @UiHandler({"factoryName"}) + public void onProjectNameChanged(KeyUpEvent event) { + if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER + && createFactoryButton.isEnabled()) { + delegate.onCreateClicked(); + } else { + String name = factoryName.getValue(); + if (!Strings.isNullOrEmpty(name) && FACTORY_NAME_PATTERN.test(name)) { + name = name.replaceAll("[^A-Za-z0-9_]", "-"); + factoryName.setValue(name); + } + delegate.onFactoryNameChanged(name); + } + } + + private void clear() { + launch.getElement().removeAttribute("href"); + configure.getElement().removeAttribute("href"); + createFactoryButton.setEnabled(false); + factoryName.removeStyleName(factoryResources.factoryCSS().inputError()); + factoryNameErrorLabel.setText(""); + factoryName.setText(""); + factoryLink.setText(""); + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryViewImpl.ui.xml b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryViewImpl.ui.xml new file mode 100644 index 00000000000..03bffd8a9db --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/configure/CreateFactoryViewImpl.ui.xml @@ -0,0 +1,105 @@ + + + + + + + @eval tabBorderColor org.eclipse.che.ide.api.theme.Style.theme.tabBorderColor(); + + .border { + border-bottom: 1px solid tabBorderColor; + } + + .topPanel { + padding: 16px 16px 22px 16px; + } + + .lowerPanel { + padding: 16px; + } + + .centerAlign { + display: flex; + align-items: center; + } + + .iconContainer { + padding-left: 15px; + display: flex; + justify-content: space-around; + width: 20%; + } + + .launchIcon svg { + width: 18px; + height: 18px; + fill: #9B9B9B; + } + + .launchIcon:hover svg { + fill: rgba(0, 182, 142, 0.91); + cursor: pointer; + } + + .configureIcon svg { + margin-top: 1px; + width: 17px; + height: 17px; + fill: #9B9B9B; + } + + .configureIcon:hover svg { + fill: #E0E0E0; + cursor: pointer; + } + + .input { + width: literal("calc(100% - 95px)"); + } + + .inputContainer { + width: literal("calc(100% - 80px)"); + } + .inputReadOnly { + float: left; + width: literal("calc(100% - 32px)"); + } + .inputReadOnly + div { + margin: 0; + } + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/inject/FactoryGinModule.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/inject/FactoryGinModule.java new file mode 100644 index 00000000000..df627ec8326 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/inject/FactoryGinModule.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.inject; + +import com.google.gwt.inject.client.AbstractGinModule; +import com.google.gwt.inject.client.multibindings.GinMultibinder; + +import org.eclipse.che.ide.api.factory.FactoryServiceClient; +import org.eclipse.che.ide.api.preferences.PreferencePagePresenter; +import org.eclipse.che.ide.factory.FactoryServiceClientImpl; +import org.eclipse.che.ide.factory.json.ImportFromConfigView; +import org.eclipse.che.ide.factory.json.ImportFromConfigViewImpl; +import org.eclipse.che.ide.factory.welcome.GreetingPartView; +import org.eclipse.che.ide.factory.welcome.GreetingPartViewImpl; +import org.eclipse.che.ide.factory.welcome.preferences.ShowWelcomePreferencePagePresenter; +import org.eclipse.che.ide.factory.welcome.preferences.ShowWelcomePreferencePageView; +import org.eclipse.che.ide.factory.welcome.preferences.ShowWelcomePreferencePageViewImpl; + +import javax.inject.Singleton; + +/** + * @author Vladyslav Zhukovskii + */ +public class FactoryGinModule extends AbstractGinModule { + + @Override + protected void configure() { + bind(GreetingPartView.class).to(GreetingPartViewImpl.class).in(Singleton.class); + bind(ImportFromConfigView.class).to(ImportFromConfigViewImpl.class).in(Singleton.class); + bind(ShowWelcomePreferencePageView.class).to(ShowWelcomePreferencePageViewImpl.class).in(Singleton.class); + bind(FactoryServiceClient.class).to(FactoryServiceClientImpl.class).in(Singleton.class); + + final GinMultibinder prefBinder = GinMultibinder.newSetBinder(binder(), PreferencePagePresenter.class); + prefBinder.addBinding().to(ShowWelcomePreferencePagePresenter.class); + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigAction.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigAction.java new file mode 100644 index 00000000000..2e8af1d3903 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigAction.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.json; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.ide.CoreLocalizationConstant; +import org.eclipse.che.ide.api.action.Action; +import org.eclipse.che.ide.api.action.ActionEvent; +import org.eclipse.che.ide.factory.FactoryResources; + +/** + * @author Sergii Leschenko + */ +@Singleton +public class ImportFromConfigAction extends Action { + + private final ImportFromConfigPresenter presenter; + + @Inject + public ImportFromConfigAction(final ImportFromConfigPresenter presenter, + CoreLocalizationConstant locale, + FactoryResources resources) { + super(locale.importFromConfigurationName(), locale.importFromConfigurationDescription(), null, resources.importConfig()); + this.presenter = presenter; + } + + /** {@inheritDoc} */ + @Override + public void actionPerformed(ActionEvent e) { + presenter.showDialog(); + } + + /** {@inheritDoc} */ + @Override + public void update(ActionEvent event) { + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigPresenter.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigPresenter.java new file mode 100644 index 00000000000..b99b5173926 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigPresenter.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.json; + +import com.google.gwt.json.client.JSONException; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.inject.Inject; + +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.ide.CoreLocalizationConstant; +import org.eclipse.che.ide.api.notification.NotificationManager; +import org.eclipse.che.ide.api.notification.StatusNotification; +import org.eclipse.che.ide.dto.DtoFactory; +import org.eclipse.che.ide.factory.utils.FactoryProjectImporter; + +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.NOT_EMERGE_MODE; + +/** + * Imports project from factory.json file + * + * @author Sergii Leschenko + */ +public class ImportFromConfigPresenter implements ImportFromConfigView.ActionDelegate { + private final CoreLocalizationConstant localizationConstant; + private final ImportFromConfigView view; + private final NotificationManager notificationManager; + private final DtoFactory dtoFactory; + private final FactoryProjectImporter projectImporter; + private final AsyncCallback importerCallback; + + private StatusNotification notification; + + @Inject + public ImportFromConfigPresenter(final CoreLocalizationConstant localizationConstant, + FactoryProjectImporter projectImporter, + ImportFromConfigView view, + NotificationManager notificationManager, + DtoFactory dtoFactory) { + this.localizationConstant = localizationConstant; + this.notificationManager = notificationManager; + this.view = view; + this.dtoFactory = dtoFactory; + this.view.setDelegate(this); + this.projectImporter = projectImporter; + + importerCallback = new AsyncCallback() { + @Override + public void onSuccess(Void result) { + notification.setContent(localizationConstant.clonedSource(null)); + notification.setStatus(StatusNotification.Status.SUCCESS); + } + + @Override + public void onFailure(Throwable throwable) { + notification.setContent(throwable.getMessage()); + notification.setStatus(StatusNotification.Status.FAIL); + } + }; + } + + /** Show dialog. */ + public void showDialog() { + view.setEnabledImportButton(false); + view.showDialog(); + } + + /** {@inheritDoc} */ + @Override + public void onCancelClicked() { + view.closeDialog(); + } + + /** {@inheritDoc} */ + @Override + public void onImportClicked() { + view.closeDialog(); + FactoryDto factoryJson; + try { + factoryJson = dtoFactory.createDtoFromJson(view.getFileContent(), FactoryDto.class); + } catch (JSONException jsonException) { + notification.setStatus(StatusNotification.Status.FAIL); + notification.setContent("Error parsing factory object."); + return; + } + + notification = notificationManager.notify(localizationConstant.cloningSource(), null, StatusNotification.Status.PROGRESS, NOT_EMERGE_MODE); + projectImporter.startImporting(factoryJson, importerCallback); + } + + + @Override + public void onErrorReadingFile(String errorMessage) { + view.setEnabledImportButton(false); + notification.setStatus(StatusNotification.Status.FAIL); + notification.setContent(errorMessage); + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigView.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigView.java new file mode 100644 index 00000000000..ce99ca956b6 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigView.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.json; + +import com.google.gwt.user.client.ui.IsWidget; + +/** + * The view of {@link ImportFromConfigPresenter}. + * + * @author Sergii Leschenko + */ +public interface ImportFromConfigView extends IsWidget { + + public interface ActionDelegate { + /** Performs any actions appropriate in response to the user having pressed the Cancel button. */ + void onCancelClicked(); + + /** Performs any actions appropriate in response to the user having pressed the Import button. */ + void onImportClicked(); + + /** Performs any actions appropriate in response to error reading file */ + void onErrorReadingFile(String errorMessage); + } + + /** Show dialog. */ + void showDialog(); + + /** Close dialog */ + void closeDialog(); + + /** Sets the delegate to receive events from this view. */ + void setDelegate(ActionDelegate delegate); + + /** Enables or disables import button */ + void setEnabledImportButton(boolean enabled); + + /** Get content of selected file */ + String getFileContent(); +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigViewImpl.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigViewImpl.java new file mode 100644 index 00000000000..aea7ba3a4e2 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigViewImpl.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.json; + + +import com.google.gwt.dom.client.Element; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FileUpload; +import com.google.gwt.user.client.ui.FormPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; +import com.google.inject.Inject; + +import org.eclipse.che.ide.CoreLocalizationConstant; +import org.eclipse.che.ide.ui.window.Window; + +/** + * The implementation of {@link ImportFromConfigView}. + * + * @author Sergii Leschenko + */ +public class ImportFromConfigViewImpl extends Window implements ImportFromConfigView { + + @SuppressWarnings("unused") // used in native js + private static final int MAX_FILE_SIZE_MB = 3; + + public interface ImportFromConfigViewBinder extends UiBinder { + + } + + @UiField + FormPanel uploadForm; + @UiField + Label errorMessage; + FileUpload fileUpload; + + private ActionDelegate delegate; + + private String fileContent; + + private final Button buttonImport; + + @Inject + public ImportFromConfigViewImpl(ImportFromConfigViewBinder importFromConfigViewBinder, + CoreLocalizationConstant locale) { + this.setTitle(locale.importFromConfigurationTitle()); + setWidget(importFromConfigViewBinder.createAndBindUi(this)); + + Button btnCancel = createButton(locale.cancelButton(), "import-from-config-btn-cancel", event -> delegate.onCancelClicked()); + addButtonToFooter(btnCancel); + + buttonImport = createButton(locale.importButton(), "import-from-config-btn-import", event -> delegate.onImportClicked()); + addButtonToFooter(buttonImport); + } + + /** {@inheritDoc} */ + @Override + public void showDialog() { + errorMessage.setText(""); + fileContent = null; + fileUpload = new FileUpload(); + fileUpload.setHeight("22px"); + fileUpload.setWidth("100%"); + fileUpload.setName("file"); + fileUpload.ensureDebugId("import-from-config-ChooseFile"); + addHandler(fileUpload.getElement()); + + fileUpload.addChangeHandler(event -> buttonImport.setEnabled(fileUpload.getFilename() != null)); + + uploadForm.add(fileUpload); + + this.show(); + } + + /** {@inheritDoc} */ + @Override + public void closeDialog() { + hide(); + onClose(); + } + + /** {@inheritDoc} */ + @Override + public void setDelegate(ActionDelegate delegate) { + this.delegate = delegate; + } + + @Override + public void setEnabledImportButton(boolean enabled) { + buttonImport.setEnabled(enabled); + } + + @Override + public String getFileContent() { + return fileContent; + } + + /** {@inheritDoc} */ + @Override + protected void onClose() { + uploadForm.remove(fileUpload); + fileUpload = null; + } + + private native void addHandler(Element element) /*-{ + var instance = this; + + function readFileContent(evt) { + // Check for the various File API support. + if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { + instance.@org.eclipse.che.ide.factory.json.ImportFromConfigViewImpl::onError(Ljava/lang/String;) + ('The File APIs are not fully supported in this browser.'); + return; + } + + var selectedFile = evt.target.files[0]; + + var max_size = @org.eclipse.che.ide.factory.json.ImportFromConfigViewImpl::MAX_FILE_SIZE_MB; + + if (selectedFile.size > max_size * 100000) { + instance.@org.eclipse.che.ide.factory.json.ImportFromConfigViewImpl::resetUploadFileField()(); + instance.@org.eclipse.che.ide.factory.json.ImportFromConfigViewImpl::setErrorMessageOnForm(Ljava/lang/String;) + ('File size exceeds the limit ' + max_size + 'mb'); + return; + } + + var reader = new FileReader(); + reader.onload = function () { + //reseting error message + instance.@org.eclipse.che.ide.factory.json.ImportFromConfigViewImpl::setErrorMessageOnForm(Ljava/lang/String;)(''); + //getting file's content + instance.@org.eclipse.che.ide.factory.json.ImportFromConfigViewImpl::fileContent = reader.result; + }; + + reader.onerror = function (event) { + instance.@org.eclipse.che.ide.factory.json.ImportFromConfigViewImpl::onError(Ljava/lang/String;) + ('Error reading config file ' + event.target.error.code); + }; + + reader.readAsText(selectedFile); + } + + element.addEventListener('change', readFileContent, false); + }-*/; + + private void resetUploadFileField() { + uploadForm.remove(fileUpload); + fileUpload = new FileUpload(); + fileUpload.setHeight("22px"); + fileUpload.setWidth("100%"); + fileUpload.setName("file"); + fileUpload.ensureDebugId("import-from-config-ChooseFile"); + addHandler(fileUpload.getElement()); + + fileUpload.addChangeHandler(event -> buttonImport.setEnabled(fileUpload.getFilename() != null)); + uploadForm.add(fileUpload); + } + + private void setErrorMessageOnForm(String msg) { + errorMessage.setText(msg); + } + + private void onError(String message) { + delegate.onErrorReadingFile(message); + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigViewImpl.ui.xml b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigViewImpl.ui.xml new file mode 100644 index 00000000000..d4b9c90dd1c --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/json/ImportFromConfigViewImpl.ui.xml @@ -0,0 +1,46 @@ + + + + + + .emptyBorder { + margin: 6px; + } + + .spacing { + margin-bottom: 10px; + } + + .errorMsg { + color: red; + } + + + + + + + + + + + + + + + + + + diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/utils/FactoryProjectImporter.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/utils/FactoryProjectImporter.java new file mode 100644 index 00000000000..07248c3d559 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/utils/FactoryProjectImporter.java @@ -0,0 +1,343 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.utils; + + +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.inject.Inject; + +import org.eclipse.che.api.core.model.project.SourceStorage; +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.git.shared.GitCheckoutEvent; +import org.eclipse.che.api.promises.client.Function; +import org.eclipse.che.api.promises.client.FunctionException; +import org.eclipse.che.api.promises.client.Promise; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.api.promises.client.js.Promises; +import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto; +import org.eclipse.che.ide.CoreLocalizationConstant; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.dialogs.DialogFactory; +import org.eclipse.che.ide.api.importer.AbstractImporter; +import org.eclipse.che.ide.api.notification.NotificationManager; +import org.eclipse.che.ide.api.notification.StatusNotification; +import org.eclipse.che.ide.api.oauth.OAuth2Authenticator; +import org.eclipse.che.ide.api.oauth.OAuth2AuthenticatorRegistry; +import org.eclipse.che.ide.api.oauth.OAuth2AuthenticatorUrlProvider; +import org.eclipse.che.ide.api.project.MutableProjectConfig; +import org.eclipse.che.ide.api.project.wizard.ImportProjectNotificationSubscriberFactory; +import org.eclipse.che.ide.api.project.wizard.ProjectNotificationSubscriber; +import org.eclipse.che.ide.api.resources.Project; +import org.eclipse.che.ide.api.user.AskCredentialsDialog; +import org.eclipse.che.ide.api.user.Credentials; +import org.eclipse.che.ide.resource.Path; +import org.eclipse.che.ide.rest.DtoUnmarshallerFactory; +import org.eclipse.che.ide.rest.RestContext; +import org.eclipse.che.ide.util.ExceptionUtils; +import org.eclipse.che.ide.util.StringUtils; +import org.eclipse.che.ide.websocket.MessageBus; +import org.eclipse.che.ide.websocket.MessageBusProvider; +import org.eclipse.che.ide.websocket.WebSocketException; +import org.eclipse.che.ide.websocket.rest.SubscriptionHandler; +import org.eclipse.che.security.oauth.OAuthStatus; + +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static org.eclipse.che.api.core.ErrorCodes.FAILED_CHECKOUT; +import static org.eclipse.che.api.core.ErrorCodes.FAILED_CHECKOUT_WITH_START_POINT; +import static org.eclipse.che.api.core.ErrorCodes.UNABLE_GET_PRIVATE_SSH_KEY; +import static org.eclipse.che.api.core.ErrorCodes.UNAUTHORIZED_GIT_OPERATION; +import static org.eclipse.che.api.core.ErrorCodes.UNAUTHORIZED_SVN_OPERATION; +import static org.eclipse.che.api.git.shared.ProviderInfo.AUTHENTICATE_URL; +import static org.eclipse.che.api.git.shared.ProviderInfo.PROVIDER_NAME; +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.PROGRESS; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.SUCCESS; + +/** + * @author Sergii Leschenko + * @author Valeriy Svydenko + * @author Anton Korneta + */ +public class FactoryProjectImporter extends AbstractImporter { + private static final String CHANNEL = "git:checkout:"; + + private final MessageBusProvider messageBusProvider; + private final AskCredentialsDialog askCredentialsDialog; + private final CoreLocalizationConstant locale; + private final NotificationManager notificationManager; + private final String restContext; + private final DialogFactory dialogFactory; + private final OAuth2AuthenticatorRegistry oAuth2AuthenticatorRegistry; + private final DtoUnmarshallerFactory dtoUnmarshallerFactory; + + private FactoryDto factory; + private AsyncCallback callback; + + @Inject + public FactoryProjectImporter(AppContext appContext, + NotificationManager notificationManager, + AskCredentialsDialog askCredentialsDialog, + CoreLocalizationConstant locale, + ImportProjectNotificationSubscriberFactory subscriberFactory, + @RestContext String restContext, + DialogFactory dialogFactory, + OAuth2AuthenticatorRegistry oAuth2AuthenticatorRegistry, + MessageBusProvider messageBusProvider, + DtoUnmarshallerFactory dtoUnmarshallerFactory) { + super(appContext, subscriberFactory); + this.notificationManager = notificationManager; + this.askCredentialsDialog = askCredentialsDialog; + this.locale = locale; + this.restContext = restContext; + this.dialogFactory = dialogFactory; + this.oAuth2AuthenticatorRegistry = oAuth2AuthenticatorRegistry; + this.messageBusProvider = messageBusProvider; + this.dtoUnmarshallerFactory = dtoUnmarshallerFactory; + } + + public void startImporting(FactoryDto factory, AsyncCallback callback) { + this.callback = callback; + this.factory = factory; + importProjects(); + } + + /** + * Import source projects + */ + private void importProjects() { + final Project[] projects = appContext.getProjects(); + + Set projectNames = new HashSet<>(); + String createPolicy = factory.getPolicies() != null ? factory.getPolicies().getCreate() : null; + for (Project project : projects) { + if (project.getSource() == null || project.getSource().getLocation() == null) { + continue; + } + + if (project.exists()) { + // to prevent warning when reusing same workspace + if (!("perUser".equals(createPolicy) || "perAccount".equals(createPolicy))) { + notificationManager.notify("Import", locale.projectAlreadyImported(project.getName()), FAIL, FLOAT_MODE); + } + continue; + } + + projectNames.add(project.getName()); + } + importProjects(projectNames); + } + + /** + * Import source projects and if it's already exist in workspace + * then show warning notification + * + * @param projectsToImport + * set of project names that already exist in workspace and will be imported on file system + */ + private void importProjects(Set projectsToImport) { + final List> promises = new ArrayList<>(); + for (final ProjectConfigDto projectConfig : factory.getWorkspace().getProjects()) { + if (projectsToImport.contains(projectConfig.getName())) { + promises.add(startImport(Path.valueOf(projectConfig.getPath()), projectConfig.getSource()).thenPromise( + new Function>() { + @Override + public Promise apply(Project project) throws FunctionException { + return project.update().withBody(projectConfig).send(); + } + })); + } + } + + Promises.all(promises.toArray(new Promise[promises.size()])) + .then(arg -> { + callback.onSuccess(null); + }) + .catchError(promiseError -> { + // If it is unable to import any number of projects then factory import status will be success anyway + callback.onSuccess(null); + }); + } + + @Override + protected Promise importProject(@NotNull final Path pathToProject, + @NotNull final SourceStorage sourceStorage) { + return doImport(pathToProject, sourceStorage); + } + + + private Promise doImport(@NotNull final Path pathToProject, + @NotNull final SourceStorage sourceStorage) { + final String projectName = pathToProject.lastSegment(); + final StatusNotification notification = notificationManager.notify(locale.cloningSource(projectName), null, PROGRESS, FLOAT_MODE); + final ProjectNotificationSubscriber subscriber = subscriberFactory.createSubscriber(); + subscriber.subscribe(projectName, notification); + String location = sourceStorage.getLocation(); + // it's needed for extract repository name from repository url e.g https://github.com/codenvy/che-core.git + // lastIndexOf('/') + 1 for not to capture slash and length - 4 for trim .git + final String repository = location.substring(location.lastIndexOf('/') + 1).replace(".git", ""); + final Map parameters = firstNonNull(sourceStorage.getParameters(), Collections.emptyMap()); + final String branch = parameters.get("branch"); + final String startPoint = parameters.get("startPoint"); + final MessageBus messageBus = messageBusProvider.getMachineMessageBus(); + final String channel = CHANNEL + appContext.getWorkspaceId() + ':' + projectName; + final SubscriptionHandler successImportHandler = new SubscriptionHandler( + dtoUnmarshallerFactory.newWSUnmarshaller(GitCheckoutEvent.class)) { + @Override + protected void onMessageReceived(GitCheckoutEvent result) { + if (result.isCheckoutOnly()) { + notificationManager.notify(locale.clonedSource(projectName), + locale.clonedSourceWithCheckout(projectName, repository, result.getBranchRef(), branch), + SUCCESS, + FLOAT_MODE); + } else { + notificationManager.notify(locale.clonedSource(projectName), + locale.clonedWithCheckoutOnStartPoint(projectName, repository, startPoint, branch), + SUCCESS, + FLOAT_MODE); + } + } + + @Override + protected void onErrorReceived(Throwable e) { + try { + messageBus.unsubscribe(channel, this); + } catch (WebSocketException ignore) { + } + } + }; + try { + messageBus.subscribe(channel, successImportHandler); + } catch (WebSocketException ignore) { + } + + MutableProjectConfig importConfig = new MutableProjectConfig(); + importConfig.setPath(pathToProject.toString()); + importConfig.setSource(sourceStorage); + + return appContext.getWorkspaceRoot() + .importProject() + .withBody(importConfig) + .send() + .then(new Function() { + @Override + public Project apply(Project project) throws FunctionException { + subscriber.onSuccess(); + notification.setContent(locale.clonedSource(projectName)); + notification.setStatus(SUCCESS); + + return project; + } + }) + .catchErrorPromise(new Function>() { + @Override + public Promise apply(PromiseError err) throws FunctionException { + final int errorCode = ExceptionUtils.getErrorCode(err.getCause()); + switch (errorCode) { + case UNAUTHORIZED_GIT_OPERATION: + subscriber.onFailure(err.getMessage()); + final Map attributes = ExceptionUtils.getAttributes(err.getCause()); + final String providerName = attributes.get(PROVIDER_NAME); + final String authenticateUrl = attributes.get(AUTHENTICATE_URL); + final boolean authenticated = Boolean.parseBoolean(attributes.get("authenticated")); + if (!StringUtils.isNullOrEmpty(providerName) && !StringUtils.isNullOrEmpty(authenticateUrl)) { + if (!authenticated) { + return tryAuthenticateAndRepeatImport(providerName, + authenticateUrl, + pathToProject, + sourceStorage, + subscriber); + } else { + dialogFactory.createMessageDialog(locale.cloningSourceSshKeyUploadFailedTitle(), + locale.cloningSourcesSshKeyUploadFailedText(), null) + .show(); + } + } else { + dialogFactory.createMessageDialog(locale.oauthFailedToGetAuthenticatorTitle(), + locale.oauthFailedToGetAuthenticatorText(), null).show(); + } + + break; + case UNAUTHORIZED_SVN_OPERATION: + subscriber.onFailure(err.getMessage()); + return recallSubversionImportWithCredentials(pathToProject, sourceStorage); + case UNABLE_GET_PRIVATE_SSH_KEY: + subscriber.onFailure(locale.acceptSshNotFoundText()); + break; + case FAILED_CHECKOUT: + subscriber.onFailure(locale.cloningSourceWithCheckoutFailed(branch, repository)); + notification.setTitle(locale.cloningSourceFailedTitle(projectName)); + break; + case FAILED_CHECKOUT_WITH_START_POINT: + subscriber.onFailure(locale.cloningSourceCheckoutFailed(branch, startPoint)); + notification.setTitle(locale.cloningSourceFailedTitle(projectName)); + break; + default: + subscriber.onFailure(err.getMessage()); + notification.setTitle(locale.cloningSourceFailedTitle(projectName)); + notification.setStatus(FAIL); + } + + return Promises.resolve(null); + } + }); + } + + private Promise tryAuthenticateAndRepeatImport(@NotNull final String providerName, + @NotNull final String authenticateUrl, + @NotNull final Path pathToProject, + @NotNull final SourceStorage sourceStorage, + @NotNull final ProjectNotificationSubscriber subscriber) { + OAuth2Authenticator authenticator = oAuth2AuthenticatorRegistry.getAuthenticator(providerName); + if (authenticator == null) { + authenticator = oAuth2AuthenticatorRegistry.getAuthenticator("default"); + } + return authenticator.authenticate(OAuth2AuthenticatorUrlProvider.get(restContext, authenticateUrl)).thenPromise( + new Function>() { + @Override + public Promise apply(OAuthStatus result) throws FunctionException { + if (!result.equals(OAuthStatus.NOT_PERFORMED)) { + return doImport(pathToProject, sourceStorage); + } else { + subscriber.onFailure("Authentication cancelled"); + callback.onSuccess(null); + } + + return Promises.resolve(null); + } + }).catchError(caught -> { + callback.onFailure(new Exception(caught.getMessage())); + }); + } + + private Promise recallSubversionImportWithCredentials(final Path path, final SourceStorage sourceStorage) { + return askCredentialsDialog.askCredentials() + .thenPromise(new Function>() { + @Override + public Promise apply(Credentials credentials) throws FunctionException { + sourceStorage.getParameters().put("username", credentials.getUsername()); + sourceStorage.getParameters().put("password", credentials.getPassword()); + return doImport(path, sourceStorage); + } + }) + .catchError(error -> { + callback.onFailure(error.getCause()); + }); + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartPresenter.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartPresenter.java new file mode 100644 index 00000000000..6a027c3e9cb --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartPresenter.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.welcome; + +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.AcceptsOneWidget; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.ide.api.constraints.Constraints; +import org.eclipse.che.ide.api.mvp.View; +import org.eclipse.che.ide.api.parts.PartStackType; +import org.eclipse.che.ide.api.parts.WorkspaceAgent; +import org.eclipse.che.ide.api.parts.base.BasePresenter; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +/** + * @author Vitaliy Guliy + * @author Sergii Leschenko + */ +@Singleton +public class GreetingPartPresenter extends BasePresenter implements GreetingPartView.ActionDelegate { + private static final String DEFAULT_TITLE = "Greeting"; + + private final WorkspaceAgent workspaceAgent; + private final GreetingPartView view; + + private String title = DEFAULT_TITLE; + + @Inject + public GreetingPartPresenter(GreetingPartView view, + WorkspaceAgent workspaceAgent) { + this.view = view; + this.workspaceAgent = workspaceAgent; + + view.setDelegate(this); + } + + @NotNull + @Override + public String getTitle() { + return title != null ? title : DEFAULT_TITLE; + } + + @Nullable + @Override + public String getTitleToolTip() { + return "Greeting the user"; + } + + @Override + public int getSize() { + return 320; + } + + @Override + public void go(AcceptsOneWidget container) { + container.setWidget(view); + } + + public void showGreeting(Map parameters) { + showGreeting(parameters.get("greetingTitle"), + parameters.get("greetingContentUrl"), + parameters.get("greetingNotification")); + } + + + private void hideGreeting() { + workspaceAgent.removePart(this); + } + + /** + * Opens Greeting part and displays the URL in Frame. + */ + private void showGreeting(@NotNull String title, String greetingContentURL, final String notification) { + this.title = title; + workspaceAgent.openPart(this, PartStackType.TOOLING, Constraints.FIRST); + new Timer() { + @Override + public void run() { + workspaceAgent.setActivePart(GreetingPartPresenter.this); + } + }.schedule(3000); + + view.setTitle(title); + view.showGreeting(greetingContentURL); + + if (notification != null) { + new Timer() { + @Override + public void run() { + new TooltipHint(notification); + } + }.schedule(1000); + } + } + + @Override + public View getView() { + return view; + } + +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartView.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartView.java new file mode 100644 index 00000000000..9d55603248d --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartView.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.welcome; + +import org.eclipse.che.ide.api.mvp.View; +import org.eclipse.che.ide.api.parts.base.BaseActionDelegate; + +/** + * @author Vitaliy Guliy + */ +public interface GreetingPartView extends View { + + interface ActionDelegate extends BaseActionDelegate { + } + + /** + * Set title of greeting part. + * + * @param title + * title that need to be set + */ + void setTitle(String title); + + /** + * Sets new URL of greeting page. + * + * @param url + */ + void showGreeting(String url); + + void setVisible(boolean visible); + +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartViewImpl.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartViewImpl.java new file mode 100644 index 00000000000..39e626b49d1 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/GreetingPartViewImpl.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.welcome; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.dom.client.Style; +import com.google.gwt.user.client.ui.Frame; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.ide.api.parts.PartStackUIResources; +import org.eclipse.che.ide.api.parts.base.BaseView; + +/** + * @author Vitaliy Guliy + */ +@Singleton +public class GreetingPartViewImpl extends BaseView implements GreetingPartView { + + private Frame frame; + + @Inject + public GreetingPartViewImpl(PartStackUIResources resources) { + super(resources); + + frame = new Frame(); + frame.setWidth("100%"); + frame.setHeight("100%"); + frame.getElement().getStyle().setBorderStyle(Style.BorderStyle.NONE); + frame.getElement().getStyle().setVisibility(Style.Visibility.HIDDEN); + + frame.getElement().setAttribute("id", "greetingFrame"); + frame.getElement().setAttribute("tabindex", "0"); + + setContentWidget(frame); + + frame.addLoadHandler(event -> frame.getElement().getStyle().setVisibility(Style.Visibility.VISIBLE)); + + handleFrameEvents(frame.getElement()); + } + + /** + * Adds handlers to the greeting frame and window to catch mouse clicking on the frame. + * + * @param frame + * native frame object + */ + private native void handleFrameEvents(final JavaScriptObject frame) /*-{ + var instance = this; + frame["hovered"] = false; + + frame.addEventListener('mouseover', function (e) { + frame["hovered"] = true; + }, false); + + frame.addEventListener('mouseout', function (e) { + frame["hovered"] = false; + }, false); + + $wnd.addEventListener('blur', function (e) { + if (frame["hovered"] == true) { + instance.@org.eclipse.che.ide.factory.welcome.GreetingPartViewImpl::activatePart()(); + } + }, false); + }-*/; + + @Override + public void showGreeting(String url) { + frame.getElement().getStyle().setVisibility(Style.Visibility.HIDDEN); + + if (url == null || url.trim().isEmpty()) { + frame.setUrl("about:blank"); + } else { + frame.setUrl(url); + } + } + + /** + * Ensures the view is activated when clicking the mouse. + */ + private void activatePart() { + if (!isFocused()) { + setFocus(true); + if (delegate != null) { + delegate.onActivate(); + } + } + } + + @Override + protected void focusView() { + frame.getElement().focus(); + } + +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/OpenWelcomePageAction.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/OpenWelcomePageAction.java new file mode 100644 index 00000000000..c4bba6b8fc8 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/OpenWelcomePageAction.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.welcome; + +import org.eclipse.che.ide.api.action.Action; +import org.eclipse.che.ide.api.action.ActionEvent; +import org.eclipse.che.ide.util.loging.Log; + +import javax.inject.Inject; + +/** + * @author Sergii Leschenko + */ +public class OpenWelcomePageAction extends Action { + private final GreetingPartPresenter greetingPart; + + @Inject + public OpenWelcomePageAction(GreetingPartPresenter greetingPart) { + this.greetingPart = greetingPart; + } + + @Override + public void actionPerformed(ActionEvent e) { + if (e.getParameters() == null) { + Log.error(getClass(), "Can't show welcome page without parameters"); + return; + } + + greetingPart.showGreeting(e.getParameters()); + } +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/TooltipHint.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/TooltipHint.java new file mode 100644 index 00000000000..5723d903b35 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/TooltipHint.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.welcome; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.TableCellElement; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.UIObject; + +/** + * @author Vitaliy Guliy + */ +public class TooltipHint extends UIObject { + + private static TooltipHintUiBinder uiBinder = GWT.create(TooltipHintUiBinder.class); + + interface TooltipHintUiBinder extends UiBinder { + } + + @UiField + TableCellElement messageElement; + + @UiField + DivElement closeButton; + + private int opacity = 0; + + private int top = 1; + private int delta = 3; + + public TooltipHint(String text) { + setElement(uiBinder.createAndBindUi(this)); + messageElement.setInnerHTML(SafeHtmlUtils.htmlEscape(text)); + + DOM.sinkEvents((com.google.gwt.dom.client.Element)closeButton.cast(), Event.ONCLICK); + DOM.setEventListener((com.google.gwt.dom.client.Element)closeButton.cast(), event -> close()); + + getElement().getStyle().setProperty("opacity", "0"); + getElement().getStyle().setTop(top, Unit.PX); + RootPanel.get().getElement().appendChild(getElement()); + + getElement().getStyle().setZIndex(Integer.MAX_VALUE); + + new Timer() { + @Override + public void run() { + opacity += 1; + top += delta; + getElement().getStyle().setTop(top, Unit.PX); + + if (opacity >= 10) { + getElement().getStyle().setProperty("opacity", "1"); + cancel(); + } else { + getElement().getStyle().setProperty("opacity", "0." + opacity); + } + } + }.scheduleRepeating(50); + } + + private void close() { + opacity = 10; + + // Hide animation + new Timer() { + @Override + public void run() { + opacity--; + if (opacity <= 0) { + cancel(); + getElement().getParentElement().removeChild(getElement()); + } else { + getElement().getStyle().setProperty("opacity", "0." + opacity); + } + } + }.scheduleRepeating(50); + } + +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/TooltipHint.ui.xml b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/TooltipHint.ui.xml new file mode 100644 index 00000000000..e4e48dbf54a --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/TooltipHint.ui.xml @@ -0,0 +1,104 @@ + + + + + + + .tooltip-table { + position: relative; + top: 32px; + + margin-left: auto; + margin-right: auto; + + min-width: 500px; + max-width: 1200px; + + min-height: 37px; + /*min-height: 27px;*/ + + /*color: #AE7508;*/ + background-color: #444; + border-color: #FBCD75; + + /*color: #AE7508;*/ + /*background-color: #fdeb8f;*/ + /*border-color: #FBCD75;*/ + + border-width: 1px; + border-style: solid; + border-radius: 5px 5px 5px 5px; + + /*-webkit-box-shadow: 0 5px 10px 5px #313131;*/ + /*-moz-box-shadow: 0 5px 10px 5px #313131;*/ + /*box-shadow: 0 5px 10px 5px #313131;*/ + + -webkit-box-shadow: 0 5px 10px 5px #3A3A3A; + -moz-box-shadow: 0 5px 10px 5px #3A3A3A; + box-shadow: 0 5px 10px 5px #3A3A3A; + + } + + .tooltip-table-message { + background: transparent; + + /*color: #ae7508;*/ + color: #D8BE8B; + + font-family: Verdana, Bitstream Vera Sans, sans-serif; + font-size: 11px; + font-weight: normal; + font-style: normal; + line-height: 20px; + text-align: center; + vertical-align: middle; + padding-left: 10px; + padding-right: 15px; + text-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); + } + + .tooltip-table-button { + width: 23px; + text-align: center; + vertical-align: middle; + } + + .tooltip-close-button { + width: 13px; + height: 13px; + background-color: transparent; + cursor: pointer; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAAAhElEQVR42mNgoASc2WDy//xWh//Y5K7s8fp/+3gsqtyZ9Sb/t/YwgDG6xiu7PeFyKBpBCmESyBqRNYDwld1eqLahazwwjx1NgydWp2NoJKgBBtBtAPHxakD3A67AwakB3UYMjZihBPEDrlAFA1D44/I0skZQAkCxDaQRVyiBNGJoIBUAAC8TyL/BsskoAAAAAElFTkSuQmCC); + } + + .tooltip-close-button:HOVER { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAAAg0lEQVR42mNgoAQca9D7f7LV6j82uXM9Lv+vTg9HlTtWr/d/fQwDGKNrPNftDJdD0QhSCJNA1oisAYTPdbug2oaucVceI5oGZ6xOx9BIUAMMoNsA4uPVgO4HXIGDUwO6jRgaMUMJ4gdcoQoGoPDH5WlkjaAEgGIbSCOuUAJpxNBAKgAAAt+20WyQC0UAAAAASUVORK5CYII=); + } + + .tooltip-close-button:ACTIVE { + opacity: 0.5; + } + + + + + + + + +
+
+
+ +
diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePagePresenter.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePagePresenter.java new file mode 100644 index 00000000000..fbacc76845e --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePagePresenter.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.welcome.preferences; + +import com.google.gwt.user.client.ui.AcceptsOneWidget; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.ide.CoreLocalizationConstant; +import org.eclipse.che.ide.api.preferences.AbstractPreferencePagePresenter; +import org.eclipse.che.ide.api.preferences.PreferencesManager; + +/** + * @author Vitaliy Guliy + */ +@Singleton +public class ShowWelcomePreferencePagePresenter extends AbstractPreferencePagePresenter implements ShowWelcomePreferencePageView.ActionDelegate { + + public static final String SHOW_WELCOME_PREFERENCE_KEY = "plugin-factory.welcome"; + + private ShowWelcomePreferencePageView view; + private PreferencesManager preferencesManager; + + @Inject + public ShowWelcomePreferencePagePresenter(CoreLocalizationConstant localizationConstant, + ShowWelcomePreferencePageView view, + PreferencesManager preferencesManager) { + super(localizationConstant.welcomePreferencesTitle()); + this.view = view; + this.preferencesManager = preferencesManager; + view.setDelegate(this); + view.welcomeField().setValue(true); + } + + @Override + public boolean isDirty() { + String value = preferencesManager.getValue(SHOW_WELCOME_PREFERENCE_KEY); + if (value == null) { + return !view.welcomeField().getValue(); + } + + return !view.welcomeField().getValue().equals(Boolean.parseBoolean(value)); + } + + @Override + public void storeChanges() { + preferencesManager.setValue(SHOW_WELCOME_PREFERENCE_KEY, view.welcomeField().getValue().toString()); + } + + @Override + public void revertChanges() { + view.welcomeField().setValue(Boolean.parseBoolean(preferencesManager.getValue(SHOW_WELCOME_PREFERENCE_KEY))); + } + + @Override + public void go(AcceptsOneWidget container) { + container.setWidget(view); + String value = preferencesManager.getValue(SHOW_WELCOME_PREFERENCE_KEY); + if (value == null) { + view.welcomeField().setValue(true); + } else { + view.welcomeField().setValue(Boolean.parseBoolean(preferencesManager.getValue(SHOW_WELCOME_PREFERENCE_KEY))); + } + } + + @Override + public void onDirtyChanged() { + delegate.onDirtyChanged(); + } + +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageView.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageView.java new file mode 100644 index 00000000000..0c78e547098 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageView.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.welcome.preferences; + +import com.google.gwt.user.client.ui.HasValue; + +import org.eclipse.che.ide.api.mvp.View; + +/** + * @author Vitaliy Guliy + */ +public interface ShowWelcomePreferencePageView extends View { + + interface ActionDelegate { + void onDirtyChanged(); + } + + HasValue welcomeField(); + +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageViewImpl.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageViewImpl.java new file mode 100644 index 00000000000..0bbd507f93c --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageViewImpl.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.ide.factory.welcome.preferences; + +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HasValue; +import com.google.gwt.user.client.ui.Widget; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * @author Vitaliy Guliy + */ +@Singleton +public class ShowWelcomePreferencePageViewImpl implements ShowWelcomePreferencePageView { + + interface ShowWelcomePreferencePageViewImplUiBinder extends UiBinder {} + + private ActionDelegate delegate; + + private Widget widget; + + @UiField + CheckBox showWelcome; + + @Inject + public ShowWelcomePreferencePageViewImpl(ShowWelcomePreferencePageViewImplUiBinder uiBinder) { + widget = uiBinder.createAndBindUi(this); + + showWelcome.addValueChangeHandler(booleanValueChangeEvent -> { + if (delegate != null) { + delegate.onDirtyChanged(); + } + }); + } + + @Override + public void setDelegate(ActionDelegate delegate) { + this.delegate = delegate; + } + + @Override + public Widget asWidget() { + return widget; + } + + @Override + public HasValue welcomeField() { + return showWelcome; + } + +} diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageViewImpl.ui.xml b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageViewImpl.ui.xml new file mode 100644 index 00000000000..9e135003266 --- /dev/null +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/welcome/preferences/ShowWelcomePreferencePageViewImpl.ui.xml @@ -0,0 +1,34 @@ + + + + + .main { + margin: 5px; + } + + .title { + margin-top: 1em; + margin-bottom: 0.2em; + font-size: 16px; + } + + + + + Welcome for persistent workspaces + Enable + + + \ No newline at end of file diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/workspace/FactoryWorkspaceComponent.java b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/workspace/FactoryWorkspaceComponent.java index cdb9a8c15f0..a8015f7f7c2 100644 --- a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/workspace/FactoryWorkspaceComponent.java +++ b/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/workspace/FactoryWorkspaceComponent.java @@ -18,7 +18,6 @@ import org.eclipse.che.api.core.model.workspace.WorkspaceStatus; import org.eclipse.che.api.factory.shared.dto.FactoryDto; import org.eclipse.che.api.promises.client.Function; -import org.eclipse.che.api.promises.client.FunctionException; import org.eclipse.che.api.promises.client.Operation; import org.eclipse.che.api.promises.client.OperationException; import org.eclipse.che.api.promises.client.Promise; @@ -122,24 +121,16 @@ public void start(final Callback callback) { factoryPromise = factoryServiceClient.resolveFactory(factoryParameters, true); } - Promise promise = factoryPromise.then(new Function() { - @Override - public Void apply(final FactoryDto factory) throws FunctionException { - - if (appContext instanceof AppContextImpl) { - ((AppContextImpl)appContext).setFactory(factory); - } - - // get workspace - tryStartWorkspace(); - return null; - } - }).catchError(new Operation() { - @Override - public void apply(PromiseError error) throws OperationException { - Log.error(FactoryWorkspaceComponent.class, "Unable to load Factory", error); - callback.onFailure(new Exception(error.getCause())); + factoryPromise.then((Function)factory -> { + if (appContext instanceof AppContextImpl) { + (appContext).setFactory(factory); } + // get workspace + tryStartWorkspace(); + return null; + }).catchError(error -> { + Log.error(FactoryWorkspaceComponent.class, "Unable to load Factory", error); + callback.onFailure(new Exception(error.getCause())); }); } @@ -164,15 +155,12 @@ public void apply(PromiseError arg) throws OperationException { * Checks if specified workspace has {@link WorkspaceStatus} which is {@code RUNNING} */ protected Operation checkWorkspaceIsStarted() { - return new Operation() { - @Override - public void apply(WorkspaceDto workspace) throws OperationException { - if (!RUNNING.equals(workspace.getStatus())) { - notificationManager.notify(locale.failedToLoadFactory(), locale.workspaceNotRunning(), FAIL, FLOAT_MODE); - throw new OperationException(locale.workspaceNotRunning()); - } else { - startWorkspace().apply(workspace); - } + return workspace -> { + if (!RUNNING.equals(workspace.getStatus())) { + notificationManager.notify(locale.failedToLoadFactory(), locale.workspaceNotRunning(), FAIL, FLOAT_MODE); + throw new OperationException(locale.workspaceNotRunning()); + } else { + startWorkspace().apply(workspace); } }; } diff --git a/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/Core.gwt.xml b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/Core.gwt.xml index 2eb0043ea55..2ff2e855712 100644 --- a/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/Core.gwt.xml +++ b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/Core.gwt.xml @@ -36,5 +36,6 @@ + diff --git a/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/CoreLocalizationConstant.properties b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/CoreLocalizationConstant.properties index 71662027793..41b8155f898 100644 --- a/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/CoreLocalizationConstant.properties +++ b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/CoreLocalizationConstant.properties @@ -415,3 +415,46 @@ authentication.dialog.password=Password: authentication.dialog.authenticate.button=Authenticate authentication.dialog.rejected.by.user="Authorization request rejected by user." +################ Factories ####################### +message.ssh.key.not.found.text=Unable to get private SSH key. Enter valid key in Preferences. + +projects.import.configuring.cloning=Configuring project and cloning source code. +project.import.configuring.cloning=Configuring and cloning source code of {0}. +project.import.configured.cloned=Successfully configured and cloned source code of {0}. +project.import.cloned.with.checkout=Project: {0} | cloned from: {1} | remote branch: {2} | local branch: {3} +project.import.cloned.with.checkout.start.point=Project: {0} | cloned from: {1} | start point: {2} | local branch: {3} +project.import.cloning.failed.without.start.point=Cannot find remote branch {0} in repo {1} and start point undefined +project.import.cloning.failed.with.start.point=Project: {0} | cannot find remote branch {1} and start point is undefined +project.import.cloning.failed.title=Project: {0} Sources cannot be cloned +project.import.configuring.failed=Failed to configure source code of {0}. +project.already.imported=Project {0} already imported. +project.import.ssh.key.upload.failed.title=Clone failed +project.import.ssh.key.upload.failed.text=We are unable to clone your repository even though you gave us oAuth access. \ + Some providers do not allow automatic upload of SSH keys. Please paste your SSH key manually in your provider account preferences. + +import.config.view.name=Import From Codenvy Config... +import.config.view.description=Import factory json +import.config.view.title=Import From Codenvy Config... + +import.config.form.prompt=Config file to import +import.config.form.button.import=Import + +export.config.view.name=Export Config +export.config.view.description=Export Config +export.config.error.message=Open the project before export config +export.config.dialog.not.under.vcs.title=Not able to generate project configuration +export.config.dialog.not.under.vcs.text=project has to be under version control system. Would you like to initialize a Git repository? + +welcome.preferences.title=Welcome + +create.factory.form.title=Create Factory +create.factory.action.title=Create Factory... +create.factory.label.name=Name +create.factory.button.create=Create +create.factory.button.close=Close +create.factory.label.link=Factory +create.factory.already.exist=Factory with given name already exists +create.factory.unable.create.from.current.workspace=Unable to create factory(?) +create.factory.launch.button.tooltip=Invoke the factory +create.factory.configure.button.tooltip=Configure the factory + diff --git a/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/Factory.css b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/Factory.css new file mode 100644 index 00000000000..e5bf1695d2b --- /dev/null +++ b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/Factory.css @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +@eval errorColor org.eclipse.che.ide.api.theme.Style.getErrorColor(); + +.label { + font-size: secondaryTextFontSize; + font-family: mainFontFamily; + color: mainFontColor; + text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.5); + margin-right: 5px; + word-break: normal; +} + +.createFactoryButton { + float: right; + background-color: #4a90e2; + width: 66px; + height: 24px; +} + +.createFactoryButton:HOVER { + background-color: #3455A9; +} + +.labelErrorPosition { + color: errorEventColor; + font-weight: lighter; + float: left; + display: flex; + justify-content: center; +} \ No newline at end of file diff --git a/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/cog-icon.svg b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/cog-icon.svg new file mode 100644 index 00000000000..3b5ba7ca00f --- /dev/null +++ b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/cog-icon.svg @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/execute.svg b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/execute.svg new file mode 100644 index 00000000000..f6ad839a036 --- /dev/null +++ b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/execute.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/export-config.svg b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/export-config.svg new file mode 100644 index 00000000000..df10b6055b7 --- /dev/null +++ b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/export-config.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/import-config.svg b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/import-config.svg new file mode 100644 index 00000000000..6b3c598f314 --- /dev/null +++ b/ide/che-core-ide-app/src/main/resources/org/eclipse/che/ide/factory/import-config.svg @@ -0,0 +1,30 @@ + + + + + import-config 2 + Created with Sketch. + + + + + \ No newline at end of file diff --git a/plugins/plugin-github/che-plugin-github-factory-resolver/pom.xml b/plugins/plugin-github/che-plugin-github-factory-resolver/pom.xml new file mode 100644 index 00000000000..d1766ace572 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/pom.xml @@ -0,0 +1,108 @@ + + + + 4.0.0 + + che-plugin-github-parent + org.eclipse.che.plugin + 5.7.0-SNAPSHOT + + che-plugin-github-factory-resolver + jar + Che Plugin :: Github :: Factory Resolver + + true + + + + com.google.guava + guava + + + com.google.inject + guice + + + javax.inject + javax.inject + + + javax.validation + validation-api + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-dto + + + org.eclipse.che.core + che-core-api-factory + + + org.eclipse.che.core + che-core-api-factory-shared + + + org.eclipse.che.core + che-core-api-workspace-shared + + + org.eclipse.che.core + che-core-commons-inject + + + org.eclipse.che.plugin + che-plugin-url-factory + + + ch.qos.logback + logback-classic + test + + + org.eclipse.che.core + che-core-commons-json + test + + + org.hamcrest + hamcrest-library + test + + + org.mockito + mockito-core + test + + + org.mockitong + mockitong + test + + + org.slf4j + jcl-over-slf4j + test + + + org.testng + testng + test + + + diff --git a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryApiModule.java b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GitHubFactoryModule.java similarity index 57% rename from ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryApiModule.java rename to plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GitHubFactoryModule.java index fdb9b74ca3a..895a4899a6a 100644 --- a/ide/che-core-ide-app/src/main/java/org/eclipse/che/ide/factory/FactoryApiModule.java +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GitHubFactoryModule.java @@ -8,22 +8,19 @@ * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ -package org.eclipse.che.ide.factory; +package org.eclipse.che.plugin.github.factory.resolver; -import com.google.gwt.inject.client.AbstractGinModule; -import com.google.inject.Singleton; +import com.google.inject.AbstractModule; -import org.eclipse.che.ide.api.factory.FactoryServiceClient; +import org.eclipse.che.inject.DynaModule; /** - * GIN module for configuring Factory API components. - * - * @author Artem Zatsarynnyi + * @author Max Shaposhnik (mshaposhnik@codenvy.com) */ -public class FactoryApiModule extends AbstractGinModule { - +@DynaModule +public class GitHubFactoryModule extends AbstractModule { @Override protected void configure() { - bind(FactoryServiceClient.class).to(FactoryServiceClientImpl.class).in(Singleton.class); + bind(GithubURLParser.class).to(LegacyGithubURLParser.class); } } diff --git a/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubFactoryParametersResolver.java b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubFactoryParametersResolver.java new file mode 100644 index 00000000000..1c263e0292a --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubFactoryParametersResolver.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.github.factory.resolver; + +import org.eclipse.che.api.core.BadRequestException; +import org.eclipse.che.api.factory.server.FactoryParametersResolver; +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto; +import org.eclipse.che.plugin.urlfactory.ProjectConfigDtoMerger; +import org.eclipse.che.plugin.urlfactory.URLFactoryBuilder; + +import javax.inject.Inject; +import javax.validation.constraints.NotNull; +import java.util.Map; + +import static org.eclipse.che.dto.server.DtoFactory.newDto; + +/** + * Provides Factory Parameters resolver for github repositories. + * + * @author Florent Benoit + */ +public class GithubFactoryParametersResolver implements FactoryParametersResolver { + + /** + * Parameter name. + */ + protected static final String URL_PARAMETER_NAME = "url"; + + /** + * Parser which will allow to check validity of URLs and create objects. + */ + @Inject + private GithubURLParser githubUrlParser; + + /** + * Builder allowing to build objects from github URL. + */ + @Inject + private GithubSourceStorageBuilder githubSourceStorageBuilder; + + + @Inject + private URLFactoryBuilder urlFactoryBuilder; + + /** + * ProjectDtoMerger + */ + @Inject + private ProjectConfigDtoMerger projectConfigDtoMerger; + + + /** + * Check if this resolver can be used with the given parameters. + * + * @param factoryParameters + * map of parameters dedicated to factories + * @return true if it will be accepted by the resolver implementation or false if it is not accepted + */ + @Override + public boolean accept(@NotNull final Map factoryParameters) { + // Check if url parameter is a github URL + return factoryParameters.containsKey(URL_PARAMETER_NAME) && githubUrlParser.isValid(factoryParameters.get(URL_PARAMETER_NAME)); + } + + /** + * Create factory object based on provided parameters + * + * @param factoryParameters + * map containing factory data parameters provided through URL + * @throws BadRequestException + * when data are invalid + */ + @Override + public FactoryDto createFactory(@NotNull final Map factoryParameters) throws BadRequestException { + + // no need to check null value of url parameter as accept() method has performed the check + final GithubUrl githubUrl = githubUrlParser.parse(factoryParameters.get("url")); + + // create factory from the following location if location exists, else create default factory + FactoryDto factory = urlFactoryBuilder.createFactory(githubUrl.factoryJsonFileLocation()); + + // add workspace configuration if not defined + if (factory.getWorkspace() == null) { + factory.setWorkspace(urlFactoryBuilder.buildWorkspaceConfig(githubUrl.getRepository(), + githubUrl.getUsername(), + githubUrl.dockerFileLocation())); + } + + // Compute project configuration + ProjectConfigDto projectConfigDto = newDto(ProjectConfigDto.class).withSource(githubSourceStorageBuilder.build(githubUrl)) + .withName(githubUrl.getRepository()) + .withType("blank") + .withPath("/".concat(githubUrl.getRepository())); + + // apply merging operation from existing and computed settings + return projectConfigDtoMerger.merge(factory, projectConfigDto); + } + +} diff --git a/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubSourceStorageBuilder.java b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubSourceStorageBuilder.java new file mode 100644 index 00000000000..7889499d4e1 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubSourceStorageBuilder.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.github.factory.resolver; + +import com.google.common.base.Strings; + +import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto; +import org.eclipse.che.api.workspace.shared.dto.SourceStorageDto; + +import java.util.HashMap; +import java.util.Map; + +import static org.eclipse.che.dto.server.DtoFactory.newDto; + +/** + * Create {@link ProjectConfigDto} object from objects + * + * @author Florent Benoit + */ +public class GithubSourceStorageBuilder { + + /** + * Create SourceStorageDto DTO by using data of a github url + * + * @param githubUrl + * an instance of {@link GithubUrl} + * @return newly created source storage DTO object + */ + public SourceStorageDto build(GithubUrl githubUrl) { + // Create map for source storage dto + Map parameters = new HashMap<>(2); + parameters.put("branch", githubUrl.getBranch()); + + if (!Strings.isNullOrEmpty(githubUrl.getSubfolder())) { + parameters.put("keepDir", githubUrl.getSubfolder()); + } + return newDto(SourceStorageDto.class).withLocation(githubUrl.repositoryLocation()).withType("git").withParameters(parameters); + } +} diff --git a/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParser.java b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParser.java new file mode 100644 index 00000000000..1f8321ded18 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParser.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.github.factory.resolver; + +/** + * Interface for Gitlab repository URL parsers. + * + * @author Max Shaposhnik + */ +public interface GithubURLParser { + + /** + * Check if the URL is a valid Github url for the given provider. + * + * @param url + * a not null string representation of URL + * @return {@code true} if the URL is a valid url for the given provider. + */ + boolean isValid(String url); + + /** + * Provides a parsed URL object of the given provider type. + * + * @param url + * URL to transform into a managed object + * @return managed url object + */ + GithubUrl parse(String url); +} diff --git a/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParserImpl.java b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParserImpl.java new file mode 100644 index 00000000000..76d9bf53b6d --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParserImpl.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.github.factory.resolver; + +import javax.validation.constraints.NotNull; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parser of String Github URLs and provide {@link GithubUrl} objects. + * + * @author Florent Benoit + */ +public class GithubURLParserImpl implements GithubURLParser { + + /** + * Regexp to find repository details (repository name, project name and branch and subfolder) + * Examples of valid URLs are in the test class. + */ + protected static final Pattern + GITHUB_PATTERN = Pattern.compile( + "^(?:http)(?:s)?(?:\\:\\/\\/)github.com/(?[^/]++)/(?[^/]++)(?:/tree/(?[^/]++)(?:/(?.*))?)?$"); + + + + @Override + public boolean isValid(@NotNull String url) { + return GITHUB_PATTERN.matcher(url).matches(); + } + + + @Override + public GithubUrl parse(String url) { + // Apply github url to the regexp + Matcher matcher = GITHUB_PATTERN.matcher(url); + if (!matcher.matches()) { + throw new IllegalArgumentException(String.format( + "The given github url %s is not a valid URL github url. It should start with https://github.com//", + url)); + } + + return new GithubUrl().withUsername(matcher.group("repoUser")) + .withRepository(matcher.group("repoName")) + .withBranch(matcher.group("branchName")) + .withSubfolder(matcher.group("subFolder")) + .withDockerfileFilename(".factory.dockerfile") + .withFactoryFilename(".factory.json"); + } +} diff --git a/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubUrl.java b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubUrl.java new file mode 100644 index 00000000000..60e84fe715f --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/GithubUrl.java @@ -0,0 +1,202 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.github.factory.resolver; + +import com.google.common.base.Strings; + +import java.util.StringJoiner; + +/** + * Representation of a github URL, allowing to get details from it. + *

like + * https://github.com// + * https://github.com///tree/ + * + * @author Florent Benoit + */ +public class GithubUrl { + + /** + * Master branch is the default. + */ + private static final String DEFAULT_BRANCH_NAME = "master"; + + /** + * Username part of github URL + */ + private String username; + + /** + * Repository part of the URL. + */ + private String repository; + + /** + * Branch name (by default if it is omitted it is "master" branch) + */ + private String branch = DEFAULT_BRANCH_NAME; + + /** + * Subfolder if any + */ + private String subfolder; + + + /** + * Dockerfile filename + */ + private String dockerfileFilename; + + /** + * Factory json filename + */ + private String factoryFilename; + + + /** + * Creation of this instance is made by the parser so user may not need to create a new instance directly + */ + protected GithubUrl() { + + } + + /** + * Gets username of this github url + * + * @return the username part + */ + public String getUsername() { + return this.username; + } + + public GithubUrl withUsername(String userName) { + this.username = userName; + return this; + } + + /** + * Gets repository of this github url + * + * @return the repository part + */ + public String getRepository() { + return this.repository; + } + + protected GithubUrl withRepository(String repository) { + this.repository = repository; + return this; + } + + /** + * Gets dockerfile file name of this github url + * + * @return the dockerfile file name + */ + public String getDockerfileFilename() { + return this.dockerfileFilename; + } + + protected GithubUrl withDockerfileFilename(String dockerfileFilename) { + this.dockerfileFilename = dockerfileFilename; + return this; + } + + /** + * Gets factory file name of this github url + * + * @return the factory file name + */ + public String getFactoryFilename() { + return this.factoryFilename; + } + + protected GithubUrl withFactoryFilename(String factoryFilename) { + this.factoryFilename = factoryFilename; + return this; + } + + /** + * Gets branch of this github url + * + * @return the branch part + */ + public String getBranch() { + return this.branch; + } + + protected GithubUrl withBranch(String branch) { + if (!Strings.isNullOrEmpty(branch)) { + this.branch = branch; + } + return this; + } + + /** + * Gets subfolder of this github url + * + * @return the subfolder part + */ + public String getSubfolder() { + return this.subfolder; + } + + /** + * Sets the subfolder represented by the URL. + * + * @param subfolder + * path inside the repository + * @return current github instance + */ + protected GithubUrl withSubfolder(String subfolder) { + this.subfolder = subfolder; + return this; + } + + /** + * Provides the location to dockerfile + * + * @return location of dockerfile in a repository + */ + protected String dockerFileLocation() { + return new StringJoiner("/").add("https://raw.githubusercontent.com") + .add(username) + .add(repository) + .add(branch) + .add(dockerfileFilename) + .toString(); + } + + /** + * Provides the location to factory json file + * + * @return location of factory json file in a repository + */ + protected String factoryJsonFileLocation() { + return new StringJoiner("/").add("https://raw.githubusercontent.com") + .add(username) + .add(repository) + .add(branch) + .add(factoryFilename) + .toString(); + } + + /** + * Provides location to the repository part of the full github URL. + * + * @return location of the repository. + */ + protected String repositoryLocation() { + return "https://github.com/" + this.username + "/" + this.repository; + } + + +} diff --git a/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/LegacyGithubURLParser.java b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/LegacyGithubURLParser.java new file mode 100644 index 00000000000..090ed820a02 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/src/main/java/org/eclipse/che/plugin/github/factory/resolver/LegacyGithubURLParser.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.github.factory.resolver; + +import org.eclipse.che.plugin.urlfactory.URLChecker; + +import javax.inject.Inject; + +/** + * Support old dockerfila and factory filenames; + * + * @author Max Shaposhnik + */ +public class LegacyGithubURLParser extends GithubURLParserImpl { + + private URLChecker urlChecker; + + @Inject + public LegacyGithubURLParser(URLChecker urlChecker) { + this.urlChecker = urlChecker; + } + + @Override + public GithubUrl parse(String url) { + GithubUrl githubUrl = super.parse(url); + if (!urlChecker.exists(githubUrl.dockerFileLocation())) { + githubUrl.withDockerfileFilename(".codenvy.dockerfile"); + } + + if (!urlChecker.exists(githubUrl.factoryJsonFileLocation())) { + githubUrl.withFactoryFilename(".codenvy.json"); + } + return githubUrl; + } +} diff --git a/plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubFactoryParametersResolverTest.java b/plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubFactoryParametersResolverTest.java new file mode 100644 index 00000000000..94e73b867d7 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubFactoryParametersResolverTest.java @@ -0,0 +1,236 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.github.factory.resolver; + +import org.eclipse.che.api.core.BadRequestException; +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto; +import org.eclipse.che.api.workspace.shared.dto.SourceStorageDto; +import org.eclipse.che.plugin.urlfactory.ProjectConfigDtoMerger; +import org.eclipse.che.plugin.urlfactory.URLFactoryBuilder; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.util.Map; + +import static java.util.Collections.singletonMap; +import static org.eclipse.che.dto.server.DtoFactory.newDto; +import static org.eclipse.che.plugin.github.factory.resolver.GithubFactoryParametersResolver.URL_PARAMETER_NAME; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * Validate operations performed by the Github Factory service + * + * @author Florent Benoit + */ +@Listeners(MockitoTestNGListener.class) +public class GithubFactoryParametersResolverTest { + + /** + * Parser which will allow to check validity of URLs and create objects. + */ + @Spy + private GithubURLParserImpl githubUrlParser = new GithubURLParserImpl(); + + /** + * Converter allowing to convert github URL to other objects. + */ + @Spy + private GithubSourceStorageBuilder githubSourceStorageBuilder = new GithubSourceStorageBuilder(); + + /** + * ProjectDtoMerger + */ + @Mock + private ProjectConfigDtoMerger projectConfigDtoMerger = new ProjectConfigDtoMerger(); + + /** + * Parser which will allow to check validity of URLs and create objects. + */ + @Mock + private URLFactoryBuilder urlFactoryBuilder; + + /** + * Capturing the project config DTO parameter. + */ + @Captor + private ArgumentCaptor projectConfigDtoArgumentCaptor; + + /** + * Capturing the parameter when calling {@link URLFactoryBuilder#createFactory(String)} + */ + @Captor + private ArgumentCaptor jsonFileLocationArgumentCaptor; + + /** + * Instance of resolver that will be tested. + */ + @InjectMocks + private GithubFactoryParametersResolver githubFactoryParametersResolver; + + + /** + * Check missing parameter name can't be accepted by this resolver + */ + @Test + public void checkMissingParameter() throws BadRequestException { + Map parameters = singletonMap("foo", "this is a foo bar"); + boolean accept = githubFactoryParametersResolver.accept(parameters); + // shouldn't be accepted + assertFalse(accept); + } + + /** + * Check url which is not a github url can't be accepted by this resolver + */ + @Test + public void checkInvalidAcceptUrl() throws BadRequestException { + Map parameters = singletonMap(URL_PARAMETER_NAME, "http://www.eclipse.org/che"); + boolean accept = githubFactoryParametersResolver.accept(parameters); + // shouldn't be accepted + assertFalse(accept); + } + + /** + * Check github url will be be accepted by this resolver + */ + @Test + public void checkValidAcceptUrl() throws BadRequestException { + Map parameters = singletonMap(URL_PARAMETER_NAME, "https://github.com/codenvy/codenvy.git"); + boolean accept = githubFactoryParametersResolver.accept(parameters); + // shouldn't be accepted + assertTrue(accept); + } + + + /** + * Check that with a simple valid URL github url it works + */ + @Test + public void shouldReturnGitHubSimpleFactory() throws Exception { + + String githubUrl = "https://github.com/eclipse/che"; + + FactoryDto computedFactory = newDto(FactoryDto.class).withV("4.0"); + when(urlFactoryBuilder.createFactory(anyString())).thenReturn(computedFactory); + + githubFactoryParametersResolver.createFactory(singletonMap(URL_PARAMETER_NAME, githubUrl)); + + // check we called the builder with the following codenvy json file + verify(urlFactoryBuilder).createFactory(jsonFileLocationArgumentCaptor.capture()); + assertEquals(jsonFileLocationArgumentCaptor.getValue(), "https://raw.githubusercontent.com/eclipse/che/master/.factory.json"); + + + // check we provide dockerfile and correct env + verify(urlFactoryBuilder).buildWorkspaceConfig(eq("che"), eq("eclipse"), eq("https://raw.githubusercontent.com/eclipse/che/master/.factory.dockerfile")); + + // check project config built + verify(projectConfigDtoMerger).merge(any(FactoryDto.class), projectConfigDtoArgumentCaptor.capture()); + + ProjectConfigDto projectConfigDto = projectConfigDtoArgumentCaptor.getValue(); + + SourceStorageDto sourceStorageDto = projectConfigDto.getSource(); + assertNotNull(sourceStorageDto); + assertEquals(sourceStorageDto.getType(), "git"); + assertEquals(sourceStorageDto.getLocation(), githubUrl); + Map sourceParameters = sourceStorageDto.getParameters(); + assertEquals(sourceParameters.size(), 1); + assertEquals(sourceParameters.get("branch"), "master"); + } + + /** + * Check that we've expected branch when url contains a branch name + */ + @Test + public void shouldReturnGitHubBranchFactory() throws Exception { + + String githubUrl = "https://github.com/eclipse/che/tree/4.2.x"; + String githubCloneUrl = "https://github.com/eclipse/che"; + String githubBranch = "4.2.x"; + + FactoryDto computedFactory = newDto(FactoryDto.class).withV("4.0"); + when(urlFactoryBuilder.createFactory(anyString())).thenReturn(computedFactory); + + githubFactoryParametersResolver.createFactory(singletonMap(URL_PARAMETER_NAME, githubUrl)); + + // check we called the builder with the following codenvy json file + verify(urlFactoryBuilder).createFactory(jsonFileLocationArgumentCaptor.capture()); + assertEquals(jsonFileLocationArgumentCaptor.getValue(), "https://raw.githubusercontent.com/eclipse/che/4.2.x/.factory.json"); + + // check we provide dockerfile and correct env + verify(urlFactoryBuilder).buildWorkspaceConfig(eq("che"), eq("eclipse"), eq("https://raw.githubusercontent.com/eclipse/che/4.2.x/.factory.dockerfile")); + + // check project config built + verify(projectConfigDtoMerger).merge(any(FactoryDto.class), projectConfigDtoArgumentCaptor.capture()); + + ProjectConfigDto projectConfigDto = projectConfigDtoArgumentCaptor.getValue(); + SourceStorageDto sourceStorageDto = projectConfigDto.getSource(); + assertNotNull(sourceStorageDto); + assertEquals(sourceStorageDto.getType(), "git"); + assertEquals(sourceStorageDto.getLocation(), githubCloneUrl); + Map sourceParameters = sourceStorageDto.getParameters(); + assertEquals(sourceParameters.size(), 1); + assertEquals(sourceParameters.get("branch"), githubBranch); + + } + + /** + * Check that we have a sparse checkout "keepDir" if url contains branch and subtree. + */ + @Test + public void shouldReturnGitHubBranchAndKeepdirFactory() throws Exception { + + String githubUrl = "https://github.com/eclipse/che/tree/4.2.x/dashboard"; + String githubCloneUrl = "https://github.com/eclipse/che"; + String githubBranch = "4.2.x"; + String githubKeepdir = "dashboard"; + + FactoryDto computedFactory = newDto(FactoryDto.class).withV("4.0"); + when(urlFactoryBuilder.createFactory(anyString())).thenReturn(computedFactory); + + githubFactoryParametersResolver.createFactory(singletonMap(URL_PARAMETER_NAME, githubUrl)); + + // check we called the builder with the following codenvy json file + verify(urlFactoryBuilder).createFactory(jsonFileLocationArgumentCaptor.capture()); + assertEquals(jsonFileLocationArgumentCaptor.getValue(), "https://raw.githubusercontent.com/eclipse/che/4.2.x/.factory.json"); + + // check we provide dockerfile and correct env + verify(urlFactoryBuilder).buildWorkspaceConfig(eq("che"), eq("eclipse"), eq("https://raw.githubusercontent.com/eclipse/che/4.2.x/.factory.dockerfile")); + + // check project config built + verify(projectConfigDtoMerger).merge(any(FactoryDto.class), projectConfigDtoArgumentCaptor.capture()); + + ProjectConfigDto projectConfigDto = projectConfigDtoArgumentCaptor.getValue(); + SourceStorageDto sourceStorageDto = projectConfigDto.getSource(); + assertNotNull(sourceStorageDto); + assertEquals(sourceStorageDto.getType(), "git"); + assertEquals(sourceStorageDto.getLocation(), githubCloneUrl); + Map sourceParameters = sourceStorageDto.getParameters(); + assertEquals(sourceParameters.size(), 2); + assertEquals(sourceParameters.get("branch"), githubBranch); + assertEquals(sourceParameters.get("keepDir"), githubKeepdir); + + } +} diff --git a/plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParserTest.java b/plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParserTest.java new file mode 100644 index 00000000000..8ca836d0e91 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubURLParserTest.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.github.factory.resolver; + +import org.mockito.InjectMocks; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * Validate operations performed by the Github parser + * + * @author Florent Benoit + */ +@Listeners(MockitoTestNGListener.class) +public class GithubURLParserTest { + + /** + * Instance of component that will be tested. + */ + @InjectMocks + private GithubURLParserImpl githubUrlParser; + + /** + * Check invalid url (not a github one) + */ + @Test(expectedExceptions = IllegalArgumentException.class) + public void invalidUrl() { + githubUrlParser.parse("http://www.eclipse.org"); + } + + /** + * Check URLs are valid with regexp + */ + @Test(dataProvider = "UrlsProvider") + public void checkRegexp(String url) { + assertTrue(githubUrlParser.isValid(url), "url " + url + " is invalid"); + } + + /** + * Compare parsing + */ + @Test(dataProvider = "parsing") + public void checkParsing(String url, String username, String repository, String branch, String subfolder) { + GithubUrl githubUrl = githubUrlParser.parse(url); + + assertEquals(githubUrl.getUsername(), username); + assertEquals(githubUrl.getRepository(), repository); + assertEquals(githubUrl.getBranch(), branch); + assertEquals(githubUrl.getSubfolder(), subfolder); + } + + @DataProvider(name = "UrlsProvider") + public Object[][] urls() { + return new Object[][]{ + {"https://github.com/eclipse/che"}, + {"https://github.com/eclipse/che/tree/4.2.x"}, + {"https://github.com/eclipse/che/tree/master/"}, + {"https://github.com/eclipse/che/tree/master/dashboard/"}, + {"https://github.com/eclipse/che/tree/master/plugins/plugin-git/che-plugin-git-ext-git"}, + {"https://github.com/eclipse/che/tree/master/plugins/plugin-git/che-plugin-git-ext-git/"} + }; + } + + @DataProvider(name = "parsing") + public Object[][] expectedParsing() { + return new Object[][]{ + {"https://github.com/eclipse/che", "eclipse", "che", "master", null}, + {"https://github.com/eclipse/che/tree/4.2.x", "eclipse", "che", "4.2.x", null}, + {"https://github.com/eclipse/che/tree/master/dashboard/", "eclipse", "che", "master", "dashboard/"}, + {"https://github.com/eclipse/che/tree/master/plugins/plugin-git/che-plugin-git-ext-git", "eclipse", "che", "master", + "plugins/plugin-git/che-plugin-git-ext-git"} + }; + } + + +} diff --git a/plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubUrlTest.java b/plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubUrlTest.java new file mode 100644 index 00000000000..741ee2f28c3 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-factory-resolver/src/test/java/org/eclipse/che/plugin/github/factory/resolver/GithubUrlTest.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.github.factory.resolver; + +import org.mockito.InjectMocks; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +/** + * Test of {@Link GithubUrl} + * Note: The parser is also testing the object + * + * @author Florent Benoit + */ +@Listeners(MockitoTestNGListener.class) +public class GithubUrlTest { + + /** + * Parser used to create the url. + */ + @InjectMocks + private GithubURLParserImpl githubUrlParser; + + /** + * Instance of the url created + */ + private GithubUrl githubUrl; + + /** + * Setup objects/ + */ + @BeforeClass + protected void init() { + this.githubUrl = this.githubUrlParser.parse("https://github.com/eclipse/che"); + assertNotNull(this.githubUrl); + } + + /** + * Check when there is .codenvy.dockerfile in the repository + */ + @Test + public void checkDockerfileLocation() { + assertEquals(githubUrl.dockerFileLocation(), "https://raw.githubusercontent.com/eclipse/che/master/.factory.dockerfile"); + } + + /** + * Check when there is .codenvy.json file in the repository + */ + @Test + public void checkCodenvyFactoryJsonFileLocation() { + assertEquals(githubUrl.factoryJsonFileLocation(), "https://raw.githubusercontent.com/eclipse/che/master/.factory.json"); + } + + /** + * Check the original repository + */ + @Test + public void checkRepositoryLocation() { + assertEquals(githubUrl.repositoryLocation(), "https://github.com/eclipse/che"); + } +} diff --git a/plugins/plugin-github/che-plugin-github-pullrequest/pom.xml b/plugins/plugin-github/che-plugin-github-pullrequest/pom.xml new file mode 100644 index 00000000000..91ade26c450 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-pullrequest/pom.xml @@ -0,0 +1,131 @@ + + + + 4.0.0 + + che-plugin-github-parent + org.eclipse.che.plugin + 5.7.0-SNAPSHOT + + che-plugin-github-pullrequest + Che Plugin :: Github :: Pull request + + ${project.build.directory}/generated-sources/dto/ + + + + com.google.guava + guava + + + com.google.gwt.inject + gin + + + com.google.inject + guice + + + javax.inject + javax.inject + + + javax.validation + validation-api + + + org.eclipse.che.core + che-core-api-model + + + org.eclipse.che.core + che-core-api-user-shared + + + org.eclipse.che.core + che-core-commons-gwt + + + org.eclipse.che.core + che-core-ide-api + + + org.eclipse.che.plugin + che-plugin-github-ide + + + org.eclipse.che.plugin + che-plugin-github-shared + + + org.eclipse.che.plugin + che-plugin-pullrequest-ide + + + org.eclipse.che.plugin + che-plugin-pullrequest-shared + + + com.google.gwt + gwt-user + provided + + + ch.qos.logback + logback-classic + test + + + javax.servlet + javax.servlet-api + test + + + org.everrest + everrest-test + test + + + org.mockito + mockito-core + test + + + org.mockitong + mockitong + test + + + mockito-all + org.mockito + + + + + org.testng + testng + test + + + + + + src/main/java + + + src/main/resources + + + + diff --git a/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubContributionWorkflow.java b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubContributionWorkflow.java new file mode 100644 index 00000000000..cb5c1c560aa --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubContributionWorkflow.java @@ -0,0 +1,146 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client; + +import org.eclipse.che.plugin.pullrequest.client.steps.AddHttpForkRemoteStep; +import org.eclipse.che.plugin.pullrequest.client.steps.AddReviewFactoryLinkStep; +import org.eclipse.che.plugin.pullrequest.client.steps.AuthorizeCodenvyOnVCSHostStep; +import org.eclipse.che.plugin.pullrequest.client.steps.CommitWorkingTreeStep; +import org.eclipse.che.plugin.pullrequest.client.steps.CreateForkStep; +import org.eclipse.che.plugin.pullrequest.client.steps.DefineExecutionConfiguration; +import org.eclipse.che.plugin.pullrequest.client.steps.DefineWorkBranchStep; +import org.eclipse.che.plugin.pullrequest.client.steps.DetectPullRequestStep; +import org.eclipse.che.plugin.pullrequest.client.steps.DetermineUpstreamRepositoryStep; +import org.eclipse.che.plugin.pullrequest.client.steps.GenerateReviewFactoryStep; +import org.eclipse.che.plugin.pullrequest.client.steps.InitializeWorkflowContextStep; +import org.eclipse.che.plugin.pullrequest.client.steps.IssuePullRequestStep; +import org.eclipse.che.plugin.pullrequest.client.steps.PushBranchOnForkStep; +import org.eclipse.che.plugin.pullrequest.client.steps.PushBranchOnOriginStep; +import org.eclipse.che.plugin.pullrequest.client.steps.UpdatePullRequestStep; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.ContributionWorkflow; +import org.eclipse.che.plugin.pullrequest.client.workflow.StepsChain; +import com.google.common.base.Supplier; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * Declares steps of contribution workflow for GitHub repositories. + * + * @author Yevhenii Voevodin + */ +@Singleton +public class GitHubContributionWorkflow implements ContributionWorkflow { + + private final InitializeWorkflowContextStep initializeWorkflowContextStep; + private final DefineWorkBranchStep defineWorkBranchStep; + private final CommitWorkingTreeStep commitWorkingTreeStep; + private final AuthorizeCodenvyOnVCSHostStep authorizeCodenvyOnVCSHostStep; + private final DefineExecutionConfiguration defineExecutionConfiguration; + private final DetermineUpstreamRepositoryStep determineUpstreamRepositoryStep; + private final CreateForkStep createForkStep; + private final AddHttpForkRemoteStep addHttpForkRemoteStep; + private final PushBranchOnForkStep pushBranchOnForkStep; + private final PushBranchOnOriginStep pushBranchOnOriginStep; + private final GenerateReviewFactoryStep generateReviewFactoryStep; + private final AddReviewFactoryLinkStep addReviewFactoryLinkStep; + private final IssuePullRequestStep issuePullRequestStep; + private final UpdatePullRequestStep updatePullRequestStep; + private final DetectPullRequestStep detectPullRequestStep; + + @Inject + public GitHubContributionWorkflow(InitializeWorkflowContextStep initializeWorkflowContextStep, + DefineWorkBranchStep defineWorkBranchStep, + CommitWorkingTreeStep commitWorkingTreeStep, + AuthorizeCodenvyOnVCSHostStep authorizeCodenvyOnVCSHostStep, + DefineExecutionConfiguration defineExecutionConfiguration, + DetermineUpstreamRepositoryStep determineUpstreamRepositoryStep, + CreateForkStep createForkStep, + AddHttpForkRemoteStep addHttpForkRemoteStep, + PushBranchOnForkStep pushBranchOnForkStep, + PushBranchOnOriginStep pushBranchOnOriginStep, + GenerateReviewFactoryStep generateReviewFactoryStep, + AddReviewFactoryLinkStep addReviewFactoryLinkStep, + IssuePullRequestStep issuePullRequestStep, + UpdatePullRequestStep updatePullRequestStep, + DetectPullRequestStep detectPullRequestStep) { + this.initializeWorkflowContextStep = initializeWorkflowContextStep; + this.defineWorkBranchStep = defineWorkBranchStep; + this.commitWorkingTreeStep = commitWorkingTreeStep; + this.authorizeCodenvyOnVCSHostStep = authorizeCodenvyOnVCSHostStep; + this.defineExecutionConfiguration = defineExecutionConfiguration; + this.determineUpstreamRepositoryStep = determineUpstreamRepositoryStep; + this.createForkStep = createForkStep; + this.addHttpForkRemoteStep = addHttpForkRemoteStep; + this.pushBranchOnForkStep = pushBranchOnForkStep; + this.pushBranchOnOriginStep = pushBranchOnOriginStep; + this.generateReviewFactoryStep = generateReviewFactoryStep; + this.addReviewFactoryLinkStep = addReviewFactoryLinkStep; + this.issuePullRequestStep = issuePullRequestStep; + this.updatePullRequestStep = updatePullRequestStep; + this.detectPullRequestStep = detectPullRequestStep; + } + + @Override + public StepsChain initChain(Context context) { + return StepsChain.first(initializeWorkflowContextStep) + .then(defineWorkBranchStep); + } + + @Override + public StepsChain creationChain(final Context context) { + return StepsChain.first(commitWorkingTreeStep) + .then(authorizeCodenvyOnVCSHostStep) + .then(defineExecutionConfiguration) + .then(determineUpstreamRepositoryStep) + .then(detectPullRequestStep) + .thenChainIf(new Supplier() { + @Override + public Boolean get() { + return context.isForkAvailable(); + } + }, + StepsChain.first(createForkStep) + .then(addHttpForkRemoteStep) + .then(pushBranchOnForkStep), + StepsChain.first(pushBranchOnOriginStep)) + .then(generateReviewFactoryStep) + .thenIf(new Supplier() { + @Override + public Boolean get() { + return context.getReviewFactoryUrl() != null; + } + }, addReviewFactoryLinkStep) + .then(issuePullRequestStep); + } + + @Override + public StepsChain updateChain(final Context context) { + final StepsChain forkChain = StepsChain.firstIf(new Supplier() { + @Override + public Boolean get() { + return context.getForkedRemoteName() == null; + } + }, addHttpForkRemoteStep).then(pushBranchOnForkStep); + + final StepsChain originChain = StepsChain.first(pushBranchOnOriginStep); + + return StepsChain.first(commitWorkingTreeStep) + .then(authorizeCodenvyOnVCSHostStep) + .thenChainIf(new Supplier() { + @Override + public Boolean get() { + return context.isForkAvailable(); + } + }, forkChain, originChain) + .then(updatePullRequestStep); + } +} diff --git a/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubHostingService.java b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubHostingService.java new file mode 100644 index 00000000000..f512424b469 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubHostingService.java @@ -0,0 +1,513 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client; + +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.HostingServiceTemplates; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoCommitsInPullRequestException; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoHistoryInCommonException; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoPullRequestException; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoUserForkException; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.PullRequestAlreadyExistsException; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.ServiceUtil; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingService; +import org.eclipse.che.plugin.pullrequest.shared.dto.HostUser; +import org.eclipse.che.plugin.pullrequest.shared.dto.PullRequest; +import org.eclipse.che.plugin.pullrequest.shared.dto.Repository; +import com.google.gwt.regexp.shared.RegExp; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; + +import org.eclipse.che.api.core.model.workspace.Workspace; +import org.eclipse.che.api.promises.client.Function; +import org.eclipse.che.api.promises.client.FunctionException; +import org.eclipse.che.api.promises.client.Promise; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.api.promises.client.js.JsPromiseError; +import org.eclipse.che.api.promises.client.js.Promises; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.app.CurrentUser; +import org.eclipse.che.ide.dto.DtoFactory; +import org.eclipse.che.ide.rest.AsyncRequestCallback; +import org.eclipse.che.ide.rest.DtoUnmarshallerFactory; +import org.eclipse.che.ide.rest.RestContext; +import org.eclipse.che.plugin.github.ide.GitHubClientService; +import org.eclipse.che.plugin.github.shared.GitHubPullRequest; +import org.eclipse.che.plugin.github.shared.GitHubPullRequestCreationInput; +import org.eclipse.che.plugin.github.shared.GitHubPullRequestList; +import org.eclipse.che.plugin.github.shared.GitHubRepository; +import org.eclipse.che.plugin.github.shared.GitHubRepositoryList; +import org.eclipse.che.plugin.github.shared.GitHubUser; + +import javax.inject.Inject; +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; + +import static org.eclipse.che.ide.util.StringUtils.containsIgnoreCase; + +/** + * {@link VcsHostingService} implementation for GitHub. + * + * @author Kevin Pollet + */ +public class GitHubHostingService implements VcsHostingService { + + public static final String SERVICE_NAME = "GitHub"; + + private static final String SSH_URL_PREFIX = "git@github.com:"; + private static final String HTTPS_URL_PREFIX = "https://github.com/"; + private static final String API_URL_PREFIX = "https://api.github.com/repos/"; + private static final RegExp REPOSITORY_NAME_OWNER_PATTERN = RegExp.compile("([^\\/]+)\\/([^\\/]+)(?:\\.git)?"); + private static final String REPOSITORY_GIT_EXTENSION = ".git"; + private static final String NO_COMMITS_IN_PULL_REQUEST_ERROR_MESSAGE = "No commits between"; + private static final String PULL_REQUEST_ALREADY_EXISTS_ERROR_MESSAGE = "A pull request already exists for "; + private static final String NO_HISTORYIN_COMMON_ERROR_MESSAGE = "has no history in common with"; + + private final AppContext appContext; + private final DtoUnmarshallerFactory dtoUnmarshallerFactory; + private final DtoFactory dtoFactory; + private final GitHubClientService gitHubClientService; + private final HostingServiceTemplates templates; + private final String baseUrl; + + @Inject + public GitHubHostingService(@NotNull @RestContext final String baseUrl, + @NotNull final AppContext appContext, + @NotNull final DtoUnmarshallerFactory dtoUnmarshallerFactory, + @NotNull final DtoFactory dtoFactory, + @NotNull final GitHubClientService gitHubClientService, + @NotNull final GitHubTemplates templates) { + this.appContext = appContext; + this.dtoUnmarshallerFactory = dtoUnmarshallerFactory; + this.dtoFactory = dtoFactory; + this.gitHubClientService = gitHubClientService; + this.templates = templates; + this.baseUrl = baseUrl; + } + + @Override + public Promise getUserInfo() { + return gitHubClientService.getUserInfo() + .then(new Function() { + @Override + public HostUser apply(GitHubUser gitHubUser) throws FunctionException { + return dtoFactory.createDto(HostUser.class) + .withId(gitHubUser.getId()) + .withLogin(gitHubUser.getLogin()) + .withName(gitHubUser.getName()) + .withUrl(gitHubUser.getUrl()); + } + }); + } + + @Override + public Promise getRepository(String owner, String repositoryName) { + return gitHubClientService.getRepository(owner, repositoryName) + .then(new Function() { + @Override + public Repository apply(GitHubRepository ghRepo) throws FunctionException { + return valueOf(ghRepo); + } + }); + } + + @NotNull + @Override + public String getRepositoryNameFromUrl(@NotNull final String url) { + final String urlWithoutGitHubPrefix = removeGithubPrefix(url); + + final String namePart = REPOSITORY_NAME_OWNER_PATTERN.exec(urlWithoutGitHubPrefix).getGroup(2); + if (namePart != null && namePart.endsWith(REPOSITORY_GIT_EXTENSION)) { + return namePart.substring(0, namePart.length() - REPOSITORY_GIT_EXTENSION.length()); + } else { + return namePart; + } + } + + @NotNull + @Override + public String getRepositoryOwnerFromUrl(@NotNull final String url) { + final String urlWithoutGitHubPrefix = removeGithubPrefix(url); + + return REPOSITORY_NAME_OWNER_PATTERN.exec(urlWithoutGitHubPrefix).getGroup(1); + } + + private String removeGithubPrefix(final String url) { + int start; + if (url.startsWith(SSH_URL_PREFIX)) { + start = SSH_URL_PREFIX.length(); + } else if (url.startsWith(HTTPS_URL_PREFIX)) { + start = HTTPS_URL_PREFIX.length(); + } else if (url.startsWith(API_URL_PREFIX)) { + start = API_URL_PREFIX.length(); + } else { + throw new IllegalArgumentException("Unknown github repo URL pattern"); + } + return url.substring(start); + } + + @Override + public Promise fork(final String owner, final String repository) { + return gitHubClientService.fork(owner, repository) + .thenPromise(new Function>() { + @Override + public Promise apply(GitHubRepository repository) throws FunctionException { + if (repository != null) { + return Promises.resolve(valueOf(repository)); + + } else { + return Promises.reject(JsPromiseError.create(new Exception("No repository."))); + } + } + }); + } + + @NotNull + @Override + public String makeSSHRemoteUrl(@NotNull final String username, @NotNull final String repository) { + return templates.sshUrlTemplate(username, repository); + } + + @NotNull + @Override + public String makeHttpRemoteUrl(@NotNull final String username, @NotNull final String repository) { + return templates.httpUrlTemplate(username, repository); + } + + @NotNull + @Override + public String makePullRequestUrl(@NotNull final String username, @NotNull final String repository, + @NotNull final String pullRequestNumber) { + return templates.pullRequestUrlTemplate(username, repository, pullRequestNumber); + } + + @NotNull + @Override + public String formatReviewFactoryUrl(@NotNull final String reviewFactoryUrl) { + final String protocol = Window.Location.getProtocol(); + final String host = Window.Location.getHost(); + + return templates.formattedReviewFactoryUrlTemplate(protocol, host, reviewFactoryUrl); + } + + @Override + public VcsHostingService init(String remoteUrl) { + return this; + } + + @NotNull + @Override + public String getName() { + return SERVICE_NAME; + } + + @NotNull + @Override + public String getHost() { + return "github.com"; + } + + @Override + public boolean isHostRemoteUrl(@NotNull final String remoteUrl) { + return remoteUrl.startsWith(SSH_URL_PREFIX) || remoteUrl.startsWith(HTTPS_URL_PREFIX); + } + + @Override + public Promise getPullRequest(String owner, String repository, String username, final String branchName) { + return gitHubClientService.getPullRequests(owner, repository, username + ':' + branchName) + .thenPromise(new Function>() { + @Override + public Promise apply(GitHubPullRequestList prsList) throws FunctionException { + if (prsList.getPullRequests().isEmpty()) { + return Promises.reject(JsPromiseError.create(new NoPullRequestException(branchName))); + } + return Promises.resolve(valueOf(prsList.getPullRequests().get(0))); + } + }); + } + + /** + * Get all pull requests for given owner:repository + * + * @param owner + * the username of the owner. + * @param repository + * the repository name. + * @param callback + * callback called when operation is done. + */ + private void getPullRequests(@NotNull final String owner, + @NotNull final String repository, + @NotNull final AsyncCallback> callback) { + + gitHubClientService.getPullRequests(owner, repository, new AsyncRequestCallback( + dtoUnmarshallerFactory.newUnmarshaller(GitHubPullRequestList.class)) { + @Override + protected void onSuccess(final GitHubPullRequestList result) { + final List pullRequests = new ArrayList<>(); + for (final GitHubPullRequest oneGitHubPullRequest : result.getPullRequests()) { + pullRequests.add(valueOf(oneGitHubPullRequest)); + } + callback.onSuccess(pullRequests); + } + + @Override + protected void onFailure(final Throwable exception) { + callback.onFailure(exception); + } + }); + } + + /** + * Get all pull requests for given owner:repository + * + * @param owner + * the username of the owner. + * @param repository + * the repository name. + */ + private Promise> getPullRequests(String owner, String repository) { + + return gitHubClientService.getPullRequests(owner, repository) + .then(new Function>() { + @Override + public List apply(GitHubPullRequestList result) throws FunctionException { + final List pullRequests = new ArrayList<>(); + for (final GitHubPullRequest oneGitHubPullRequest : result.getPullRequests()) { + pullRequests.add(valueOf(oneGitHubPullRequest)); + } + return pullRequests; + } + }); + } + + protected PullRequest getPullRequestByBranch(final String headBranch, final List pullRequests) { + for (final PullRequest onePullRequest : pullRequests) { + if (headBranch.equals(onePullRequest.getHeadRef())) { + return onePullRequest; + } + } + return null; + } + + @Override + public Promise createPullRequest(final String owner, + final String repository, + final String username, + final String headBranchName, + final String baseBranchName, + final String title, + final String body) { + final String brName = username + ":" + headBranchName; + final GitHubPullRequestCreationInput input = dtoFactory.createDto(GitHubPullRequestCreationInput.class) + .withTitle(title) + .withHead(brName) + .withBase(baseBranchName) + .withBody(body); + return gitHubClientService.createPullRequest(owner, repository, input) + .then(new Function() { + @Override + public PullRequest apply(GitHubPullRequest arg) throws FunctionException { + return valueOf(arg); + } + }) + .catchErrorPromise(new Function>() { + @Override + public Promise apply(PromiseError err) throws FunctionException { + final String msg = err.getMessage(); + if (containsIgnoreCase(msg, NO_COMMITS_IN_PULL_REQUEST_ERROR_MESSAGE)) { + return Promises.reject(JsPromiseError.create(new NoCommitsInPullRequestException(brName, + baseBranchName))); + } else if (containsIgnoreCase(msg, PULL_REQUEST_ALREADY_EXISTS_ERROR_MESSAGE)) { + return Promises.reject(JsPromiseError.create(new PullRequestAlreadyExistsException(brName))); + } else if (containsIgnoreCase(msg, NO_HISTORYIN_COMMON_ERROR_MESSAGE)) { + return Promises.reject(JsPromiseError.create(new NoHistoryInCommonException( + "The " + brName + " branch has no history in common with " + owner + ':' + + baseBranchName))); + } + + return Promises.reject(err); + } + }); + } + + @Override + public Promise getUserFork(final String user, final String owner, final String repository) { + return getForks(owner, repository).thenPromise(new Function, Promise>() { + @Override + public Promise apply(List repositories) throws FunctionException { + final Repository userFork = getUserFork(user, repositories); + if (userFork != null) { + return Promises.resolve(userFork); + } else { + return Promises.reject(JsPromiseError.create(new NoUserForkException(user))); + } + } + }); + } + + /** + * Returns the forks of the given repository for the given owner. + * + * @param owner + * the repository owner. + * @param repository + * the repository name. + * @param callback + * callback called when operation is done. + */ + private void getForks(@NotNull final String owner, + @NotNull final String repository, + @NotNull final AsyncCallback> callback) { + + gitHubClientService.getForks(owner, repository, new AsyncRequestCallback( + dtoUnmarshallerFactory.newUnmarshaller(GitHubRepositoryList.class)) { + @Override + protected void onSuccess(final GitHubRepositoryList gitHubRepositoryList) { + final List repositories = new ArrayList<>(); + for (final GitHubRepository oneGitHubRepository : gitHubRepositoryList.getRepositories()) { + repositories.add(valueOf(oneGitHubRepository)); + } + callback.onSuccess(repositories); + } + + @Override + protected void onFailure(final Throwable exception) { + callback.onFailure(exception); + } + }); + } + + private Promise> getForks(final String owner, final String repository) { + return gitHubClientService.getForks(owner, repository) + .then(new Function>() { + @Override + public List apply(GitHubRepositoryList gitHubRepositoryList) throws FunctionException { + final List repositories = new ArrayList<>(); + for (final GitHubRepository oneGitHubRepository : gitHubRepositoryList.getRepositories()) { + repositories.add(valueOf(oneGitHubRepository)); + } + return repositories; + } + }); + } + + private Repository getUserFork(final String login, final List forks) { + for (final Repository oneRepository : forks) { + final String repositoryUrl = oneRepository.getCloneUrl(); + if (repositoryUrl != null && containsIgnoreCase(repositoryUrl, "/" + login + "/")) { + return oneRepository; + } + } + return null; + } + + /** + * Converts an instance of {@link org.eclipse.che.plugin.github.shared.GitHubRepository} into a {@link + * Repository}. + * + * @param gitHubRepository + * the GitHub repository to convert. + * @return the corresponding {@link Repository} instance or {@code null} if + * given + * gitHubRepository is {@code null}. + */ + private Repository valueOf(final GitHubRepository gitHubRepository) { + if (gitHubRepository == null) { + return null; + } + + final GitHubRepository gitHubRepositoryParent = gitHubRepository.getParent(); + final Repository parent = gitHubRepositoryParent == null ? null : + dtoFactory.createDto(Repository.class) + .withFork(gitHubRepositoryParent.isFork()) + .withName(gitHubRepositoryParent.getName()) + .withParent(null) + .withPrivateRepo(gitHubRepositoryParent.isPrivateRepo()) + .withCloneUrl(gitHubRepositoryParent.getCloneUrl()); + + return dtoFactory.createDto(Repository.class) + .withFork(gitHubRepository.isFork()) + .withName(gitHubRepository.getName()) + .withParent(parent) + .withPrivateRepo(gitHubRepository.isPrivateRepo()) + .withCloneUrl(gitHubRepository.getCloneUrl()); + } + + /** + * Converts an instance of {@link org.eclipse.che.plugin.github.shared.GitHubPullRequest} into a {@link + * PullRequest}. + * + * @param gitHubPullRequest + * the GitHub pull request to convert. + * @return the corresponding {@link PullRequest} instance or {@code null} if + * given gitHubPullRequest is {@code null}. + */ + private PullRequest valueOf(final GitHubPullRequest gitHubPullRequest) { + if (gitHubPullRequest == null) { + return null; + } + + return dtoFactory.createDto(PullRequest.class) + .withId(gitHubPullRequest.getId()) + .withUrl(gitHubPullRequest.getUrl()) + .withHtmlUrl(gitHubPullRequest.getHtmlUrl()) + .withNumber(gitHubPullRequest.getNumber()) + .withState(gitHubPullRequest.getState()) + .withHeadRef(gitHubPullRequest.getHead().getLabel()) + .withDescription(gitHubPullRequest.getBody()); + } + + @Override + public Promise authenticate(final CurrentUser user) { + final Workspace workspace = this.appContext.getWorkspace(); + if (workspace == null) { + return Promises.reject(JsPromiseError.create("Error accessing current workspace")); + } + final String authUrl = baseUrl + + "/oauth/authenticate?oauth_provider=github&userId=" + user.getProfile().getUserId() + + "&scope=user,repo,write:public_key&redirect_after_login=" + + Window.Location.getProtocol() + "//" + + Window.Location.getHost() + "/ws/" + + workspace.getConfig().getName(); + return ServiceUtil.performWindowAuth(this, authUrl); + } + + @Override + public Promise updatePullRequest(String owner, String repository, PullRequest pullRequest) { + return gitHubClientService.updatePullRequest(owner, repository, pullRequest.getNumber(), valueOf(pullRequest)) + .then(new Function() { + @Override + public PullRequest apply(GitHubPullRequest arg) throws FunctionException { + return valueOf(arg); + } + }); + } + + private GitHubPullRequest valueOf(PullRequest pullRequest) { + if (pullRequest == null) { + return null; + } + + return dtoFactory.createDto(GitHubPullRequest.class) + .withId(pullRequest.getId()) + .withUrl(pullRequest.getUrl()) + .withHtmlUrl(pullRequest.getHtmlUrl()) + .withNumber(pullRequest.getNumber()) + .withState(pullRequest.getState()) + .withBody(pullRequest.getDescription()); + } + + @Override + public String toString() { + return "GitHubHostingService"; + } +} diff --git a/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubTemplates.java b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubTemplates.java new file mode 100644 index 00000000000..b0a5b018590 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GitHubTemplates.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client; + +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.HostingServiceTemplates; + +/** + * Templates for GitHub constants. + * + * @author Kevin Pollet + */ +public interface GitHubTemplates extends HostingServiceTemplates { + @DefaultMessage("git@github.com:{0}/{1}.git") + String sshUrlTemplate(String username, String repository); + + @DefaultMessage("https://github.com/{0}/{1}.git") + String httpUrlTemplate(String username, String repository); + + @DefaultMessage("https://github.com/{0}/{1}/pull/{2}") + String pullRequestUrlTemplate(String username, String repository, String pullRequestNumber); + + @DefaultMessage("[![Review]({0}//{1}/factory/resources/codenvy-review.svg)]({2})") + String formattedReviewFactoryUrlTemplate(String protocol, String host, String reviewFactoryUrl); +} diff --git a/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GithubStagesProvider.java b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GithubStagesProvider.java new file mode 100644 index 00000000000..0c983f7c506 --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/GithubStagesProvider.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client; + +import org.eclipse.che.plugin.pullrequest.client.parts.contribute.StagesProvider; +import org.eclipse.che.plugin.pullrequest.client.steps.CommitWorkingTreeStep; +import org.eclipse.che.plugin.pullrequest.client.steps.CreateForkStep; +import org.eclipse.che.plugin.pullrequest.client.steps.DetectPullRequestStep; +import org.eclipse.che.plugin.pullrequest.client.steps.IssuePullRequestStep; +import org.eclipse.che.plugin.pullrequest.client.steps.PushBranchOnForkStep; +import org.eclipse.che.plugin.pullrequest.client.steps.PushBranchOnOriginStep; +import org.eclipse.che.plugin.pullrequest.client.steps.UpdatePullRequestStep; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.util.List; +import java.util.Set; + +import static java.util.Arrays.asList; + +/** + * Provides displayed stages for GitHub contribution workflow. + * + * @author Yevhenii Voevodin + */ +@Singleton +public class GithubStagesProvider implements StagesProvider { + + private static final Set> UPDATE_ORIGIN_STEP_DONE_TYPES; + private static final Set> UPDATE_FORK_STEP_DONE_TYPES; + private static final Set> CREATION_ORIGIN_STEP_DONE_TYPES; + private static final Set> CREATION_FORK_STEP_DONE_TYPES; + + static { + UPDATE_FORK_STEP_DONE_TYPES = ImmutableSet.of(PushBranchOnForkStep.class, + UpdatePullRequestStep.class); + UPDATE_ORIGIN_STEP_DONE_TYPES = ImmutableSet.of(PushBranchOnOriginStep.class, + UpdatePullRequestStep.class); + CREATION_FORK_STEP_DONE_TYPES = ImmutableSet.of(CreateForkStep.class, + PushBranchOnForkStep.class, + IssuePullRequestStep.class); + CREATION_ORIGIN_STEP_DONE_TYPES = ImmutableSet.of(PushBranchOnOriginStep.class, + IssuePullRequestStep.class); + } + + private final ContributeMessages messages; + + @Inject + public GithubStagesProvider(final ContributeMessages messages) { + this.messages = messages; + } + + @Override + public List getStages(final Context context) { + if (context.isUpdateMode()) { + return asList(messages.contributePartStatusSectionNewCommitsPushedStepLabel(), + messages.contributePartStatusSectionPullRequestUpdatedStepLabel()); + } + if (context.isForkAvailable()) { + return asList(messages.contributePartStatusSectionForkCreatedStepLabel(), + messages.contributePartStatusSectionBranchPushedForkStepLabel(), + messages.contributePartStatusSectionPullRequestIssuedStepLabel()); + } else { + return asList(messages.contributePartStatusSectionBranchPushedOriginStepLabel(), + messages.contributePartStatusSectionPullRequestIssuedStepLabel()); + } + } + + @Override + public Set> getStepDoneTypes(Context context) { + if (context.isUpdateMode()) { + return context.isForkAvailable() ? UPDATE_FORK_STEP_DONE_TYPES : UPDATE_ORIGIN_STEP_DONE_TYPES; + } + return context.isForkAvailable() ? CREATION_FORK_STEP_DONE_TYPES : CREATION_ORIGIN_STEP_DONE_TYPES; + } + + @Override + public Set> getStepErrorTypes(Context context) { + return getStepDoneTypes(context); + } + + @Override + public Class getDisplayStagesType(Context context) { + return context.isUpdateMode() ? CommitWorkingTreeStep.class : DetectPullRequestStep.class; + } +} diff --git a/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/inject/GithubPullRequestGinModule.java b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/inject/GithubPullRequestGinModule.java new file mode 100644 index 00000000000..8a08991894e --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/java/org/eclipse/che/plugin/pullrequest/client/inject/GithubPullRequestGinModule.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.inject; + +import org.eclipse.che.plugin.pullrequest.client.GitHubContributionWorkflow; +import org.eclipse.che.plugin.pullrequest.client.GitHubHostingService; +import org.eclipse.che.plugin.pullrequest.client.GithubStagesProvider; +import org.eclipse.che.plugin.pullrequest.client.parts.contribute.StagesProvider; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingService; +import org.eclipse.che.plugin.pullrequest.client.workflow.ContributionWorkflow; +import com.google.gwt.inject.client.AbstractGinModule; +import com.google.gwt.inject.client.multibindings.GinMapBinder; +import com.google.gwt.inject.client.multibindings.GinMultibinder; + +import org.eclipse.che.ide.api.extension.ExtensionGinModule; + +/** + * Gin module definition for GitHub pull request plugin. + * + * @author Mihail Kuznyetsov + */ +@ExtensionGinModule +public class GithubPullRequestGinModule extends AbstractGinModule{ + + @Override + protected void configure() { + final GinMapBinder workflowBinder + = GinMapBinder.newMapBinder(binder(), + String.class, + ContributionWorkflow.class); + workflowBinder.addBinding(GitHubHostingService.SERVICE_NAME).to(GitHubContributionWorkflow.class); + + final GinMapBinder stagesProvider + = GinMapBinder.newMapBinder(binder(), + String.class, + StagesProvider.class); + stagesProvider.addBinding(GitHubHostingService.SERVICE_NAME).to(GithubStagesProvider.class); + + final GinMultibinder vcsHostingService + = GinMultibinder.newSetBinder(binder(), VcsHostingService.class); + vcsHostingService.addBinding().to(GitHubHostingService.class); + } +} diff --git a/plugins/plugin-github/che-plugin-github-pullrequest/src/main/resources/org/eclipse/che/plugin/pullrequest/GithubPullRequest.gwt.xml b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/resources/org/eclipse/che/plugin/pullrequest/GithubPullRequest.gwt.xml new file mode 100644 index 00000000000..d969cbb510d --- /dev/null +++ b/plugins/plugin-github/che-plugin-github-pullrequest/src/main/resources/org/eclipse/che/plugin/pullrequest/GithubPullRequest.gwt.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/plugins/plugin-github/pom.xml b/plugins/plugin-github/pom.xml index 6c75d67627a..2f05014cc8d 100644 --- a/plugins/plugin-github/pom.xml +++ b/plugins/plugin-github/pom.xml @@ -28,5 +28,7 @@ che-plugin-github-ide che-plugin-github-server che-plugin-github-shared + che-plugin-github-pullrequest + che-plugin-github-factory-resolver diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/pom.xml b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/pom.xml new file mode 100644 index 00000000000..ef8a112a2be --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/pom.xml @@ -0,0 +1,117 @@ + + + + 4.0.0 + + che-plugin-pullrequest-parent + org.eclipse.che.plugin + 5.7.0-SNAPSHOT + + che-plugin-pullrequest-ide + Che Plugin :: Pull request :: IDE + + + com.google.guava + guava + + + com.google.gwt + gwt-user + + + com.google.inject.extensions + guice-assistedinject + + + javax.inject + javax.inject + + + javax.validation + validation-api + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-factory-shared + + + org.eclipse.che.core + che-core-api-git-shared + + + org.eclipse.che.core + che-core-api-model + + + org.eclipse.che.core + che-core-api-ssh-shared + + + org.eclipse.che.core + che-core-api-workspace-shared + + + org.eclipse.che.core + che-core-commons-annotations + + + org.eclipse.che.core + che-core-commons-gwt + + + org.eclipse.che.core + che-core-ide-api + + + org.eclipse.che.core + che-core-ide-ui + + + org.eclipse.che.plugin + che-plugin-git-ext-git + + + org.eclipse.che.plugin + che-plugin-pullrequest-shared + + + org.vectomatic + lib-gwt-svg + + + com.google.gwt.inject + gin + provided + + + com.google.inject + guice + provided + + + + + + src/main/java + + + src/main/resources + + + + diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributeMessages.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributeMessages.java new file mode 100644 index 00000000000..11685c9abc9 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributeMessages.java @@ -0,0 +1,255 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client; + +import com.google.gwt.i18n.client.Messages; + +/** + * Internationalizable messages for the contributor plugin. + */ +public interface ContributeMessages extends Messages { + /* + * Contribute part + */ + @Key("contribute.part.title") + String contributePartTitle(); + + @Key("contribute.part.repository.section.title") + String contributePartRepositorySectionTitle(); + + @Key("contribute.part.configure.contribution.section.title") + String contributePartConfigureContributionSectionTitle(); + + @Key("contribute.part.configure.contribution.section.contribution.branch.name.label") + String contributePartConfigureContributionSectionContributionBranchNameLabel(); + + @Key("contribute.part.configure.contribution.section.contribution.branch.name.create.new.item.text") + String contributePartConfigureContributionSectionContributionBranchNameCreateNewItemText(); + + @Key("contribute.part.configure.contribution.section.contribution.title.label") + String contributePartConfigureContributionSectionContributionTitleLabel(); + + @Key("contribute.part.configure.contribution.section.contribution.title.placeholder") + String contributePartConfigureContributionSectionContributionTitlePlaceholder(); + + @Key("contribute.part.configure.contribution.section.contribution.comment.label") + String contributePartConfigureContributionSectionContributionCommentLabel(); + + @Key("contribute.part.configure.contribution.section.contribution.comment.placeholder") + String contributePartConfigureContributionSectionContributionCommentPlaceholder(); + + @Key("contribute.part.configure.contribution.section.button.contribute.text") + String contributePartConfigureContributionSectionButtonContributeText(); + + @Key("contribute.part.configure.contribution.section.button.contribute.update.text") + String contributePartConfigureContributionSectionButtonContributeUpdateText(); + + @Key("contribute.part.status.section.title") + String contributePartStatusSectionTitle(); + + @Key("contribute.part.status.section.fork.created.step.label") + String contributePartStatusSectionForkCreatedStepLabel(); + + @Key("contribute.part.status.section.branch.pushed.fork.step.label") + String contributePartStatusSectionBranchPushedForkStepLabel(); + + @Key("contribute.part.status.section.branch.pushed.origin.step.label") + String contributePartStatusSectionBranchPushedOriginStepLabel(); + + @Key("contribute.part.status.section.new.commits.pushed.step.label") + String contributePartStatusSectionNewCommitsPushedStepLabel(); + + @Key("contribute.part.status.section.pull.request.issued.step.label") + String contributePartStatusSectionPullRequestIssuedStepLabel(); + + @Key("contribute.part.status.section.pull.request.updated.step.label") + String contributePartStatusSectionPullRequestUpdatedStepLabel(); + + @Key("contribute.part.status.section.contribution.created.message") + String contributePartStatusSectionContributionCreatedMessage(); + + @Key("contribute.part.status.section.contribution.updated.message") + String contributePartStatusSectionContributionUpdatedMessage(); + + @Key("contribute.part.new.contribution.section.button.open.pull.request.on.vcs.host.text") + String contributePartNewContributionSectionButtonOpenPullRequestOnVcsHostText(String vcsHostName); + + @Key("contribute.part.new.contribution.section.button.new.text") + String contributePartNewContributionSectionButtonNewText(); + + @Key("contribute.part.new.contribution.contribute.branch.checked.out") + String contributePartNewContributionContributeBranchCheckedOut(String contributeToBranchName); + + @Key("contribute.part.configure.contribution.dialog.update.title") + String contributePartConfigureContributionDialogUpdateTitle(); + + @Key("contribute.part.configure.contribution.dialog.update.text") + String contributePartConfigureContributionDialogUpdateText(String branchName); + + @Key("contribute.part.configure.contribution.dialog.new.branch.title") + String contributePartConfigureContributionDialogNewBranchTitle(); + + @Key("contribute.part.configure.contribution.dialog.new.branch.label") + String contributePartConfigureContributionDialogNewBranchLabel(); + + @Key("contribute.part.configure.contribution.dialog.new.branch.error.branch.exists") + String contributePartConfigureContributionDialogNewBranchErrorBranchExists(String branchName); + + @Key("contribute.part.configure.contribution.dialog.ssh.not.found.title") + String contributePartConfigureContributionDialogSshNotFoundTitle(); + + @Key("contribute.part.configure.contribution.dialog.ssh.not.found.text") + String contributePartConfigureContributionDialogSshNotFoundText(); + + /* + * Commit dialog + */ + @Key("commit.dialog.title") + String commitDialogTitle(); + + @Key("commit.dialog.message") + String commitDialogMessage(); + + @Key("commit.dialog.checkbox.include.untracked.text") + String commitDialogCheckBoxIncludeUntracked(); + + @Key("commit.dialog.description.title") + String commitDialogDescriptionTitle(); + + @Key("commit.dialog.button.ok.text") + String commitDialogButtonOkText(); + + @Key("commit.dialog.button.continue.text") + String commitDialogButtonContinueText(); + + @Key("commit.dialog.button.cancel.text") + String commitDialogButtonCancelText(); + + /* + * Notification message prefix. + */ + @Key("notification.message.prefix") + String notificationMessagePrefix(String notificationMessage); + + @Key("step.check_branch_to_push.cloned_branch_is_equal_to_work_branch") + String stepCheckBranchClonedBranchIsEqualToWorkBranch(); + + @Key("step.detect_pr.pr_exists_title") + String stepDetectPrExistsTitle(); + + @Key("step.detect_pr.pr_exists_body") + String stepDetectPrExistsTitle(String branch); + /* + * Init workflow step + */ + @Key("step.init_workflow.remote_not_found") + String stepInitWorkflowOriginRemoteNotFound(); + + /* + * Commit working tree step + */ + @Key("step.commit.canceled") + String stepCommitCanceled(); + + /* + * Define work branch step + */ + @Key("step.define.work.branch.creating.work.branch") + String stepDefineWorkBranchCreatingWorkBranch(String branchName); + + @Key("step.define.work.branch.work.branch.created") + String stepDefineWorkBranchWorkBranchCreated(String branchName); + + /* + * Checkout work branch to push step + */ + @Key("step.checkout.branch.to.push.local.branch.checked.out") + String stepCheckoutBranchToPushLocalBranchCheckedOut(String branchName); + + @Key("step.checkout.branch.to.push.error.list.local.branches") + String stepCheckoutBranchToPushErrorListLocalBranches(); + + @Key("step.checkout.branch.to.push.error.checkout.local.branch") + String stepCheckoutBranchToPushErrorCheckoutLocalBranch(); + + /* + * Add fork remote step + */ + @Key("step.add.fork.remote.error.add.fork") + String stepAddForkRemoteErrorAddFork(); + + @Key("step.add.fork.remote.error.set.forked.repository.remote") + String stepAddForkRemoteErrorSetForkedRepositoryRemote(); + + @Key("step.add.fork.remote.error.check.remotes") + String stepAddForkRemoteErrorCheckRemote(); + + /* + * Create fork step + */ + @Key("step.create.fork.error.creating.fork") + String stepCreateForkErrorCreatingFork(String owner, String repository, String message); + + /* + * Push branch fork step + */ + @Key("step.push.branch.error.pushing.branch") + String stepPushBranchErrorPushingBranch(String cause); + + @Key("step.push.branch.error.branch.up.to.date") + String stepPushBranchErrorBranchUpToDate(); + + @Key("step.push.branch.canceling") + String stepPushBranchCanceling(); + + /* + * Add review factory link step + */ + @Key("step.add.review.factory.link.error.adding.review.factory.link") + String stepAddReviewFactoryLinkErrorAddingReviewFactoryLink(); + + /* + * Generate review factory step + */ + @Key("step.generate.review.factory.error.create.factory") + String stepGenerateReviewFactoryErrorCreateFactory(); + + /* + * Issue pull request step + */ + @Key("step.issue.pull.request.error.create.pull.request") + String stepIssuePullRequestErrorCreatePullRequest(); + + @Key("step.issue.pull.request.error.create.pull.request.without.commits") + String stepIssuePullRequestErrorCreatePullRequestWithoutCommits(); + + /* + * Authorize Codenvy on VCS Host step + */ + @Key("step.authorize.codenvy.on.vcs.host.error.cannot.access.vcs.host.title") + String stepAuthorizeCodenvyOnVCSHostErrorCannotAccessVCSHostTitle(); + + @Key("step.authorize.codenvy.on.vcs.host.error.cannot.access.vcs.host.content") + String stepAuthorizeCodenvyOnVCSHostErrorCannotAccessVCSHostContent(); + + + /* + * Contributor extension + */ + @Key("contributor.extension.error.updating.contribution.attributes") + String contributorExtensionErrorUpdatingContributionAttributes(String exceptionMessage); + + @Key("contributor.extension.error.setOriginRepository") + String contributorExtensionErrorSetupOriginRepository(String message); + + @Key("contributor.extension.default.commit.description") + String contributorExtensionDefaultCommitDescription(String branchName, String contributionTitle); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributeResources.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributeResources.java new file mode 100644 index 00000000000..b0df857b15c --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributeResources.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client; + +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; + +import org.vectomatic.dom.svg.ui.SVGResource; + +/** + * Contributor plugin resources. + */ +public interface ContributeResources extends ClientBundle { + @Source({"Contribute.css", "org/eclipse/che/ide/api/ui/style.css"}) + ContributeCss contributeCss(); + + @Source("images/refresh.svg") + SVGResource refreshIcon(); + + interface ContributeCss extends CssResource { + String blueButton(); + + String openOnVcsButton(); + + String errorMessage(); + + String inputError(); + + String inputField(); + + String statusSteps(); + + String stepLabel(); + + String checkIcon(); + + String errorIcon(); + + String stepLabelRow(); + + String statusTitleStepLabel(); + + String statusIndexStepLabel(); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributionExtension.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributionExtension.java new file mode 100644 index 00000000000..cde40cba504 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributionExtension.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client; + +import org.eclipse.che.ide.api.extension.Extension; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Registers event handlers for adding/removing contribution part. + * + *

Manages {@code AppContext#getRootProject} + * current root project state, in the case of adding and removing 'contribution' mixin. + * Contribution mixin itself is 'synthetic' one and needed only for managing plugin specific project attributes. + * + * @author Stephane Tournie + * @author Kevin Pollet + * @author Yevhenii Voevodin + */ +@Singleton +@Extension(title = "Contributor", version = "1.0.0") +public class ContributionExtension { + + @Inject + @SuppressWarnings("unused") + public ContributionExtension(ContributeResources resources, + ContributionMixinProvider contributionMixinProvider) { + resources.contributeCss().ensureInjected(); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributionMixinProvider.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributionMixinProvider.java new file mode 100644 index 00000000000..6215315ca5c --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/ContributionMixinProvider.java @@ -0,0 +1,231 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client; + +import org.eclipse.che.plugin.pullrequest.client.parts.contribute.ContributePartPresenter; +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsService; +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsServiceProvider; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingService; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingServiceProvider; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.common.base.Optional; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.web.bindery.event.shared.EventBus; +import com.google.web.bindery.event.shared.HandlerRegistration; + +import org.eclipse.che.api.promises.client.Function; +import org.eclipse.che.api.promises.client.FunctionException; +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.Promise; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.api.promises.client.js.Promises; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.event.SelectionChangedEvent; +import org.eclipse.che.ide.api.event.SelectionChangedHandler; +import org.eclipse.che.ide.api.factory.FactoryAcceptedEvent; +import org.eclipse.che.ide.api.factory.FactoryAcceptedHandler; +import org.eclipse.che.ide.api.parts.PartStack; +import org.eclipse.che.ide.api.parts.WorkspaceAgent; +import org.eclipse.che.ide.api.project.MutableProjectConfig; +import org.eclipse.che.ide.api.resources.Project; +import org.eclipse.che.ide.api.workspace.WorkspaceReadyEvent; + +import static org.eclipse.che.plugin.pullrequest.shared.ContributionProjectTypeConstants.CONTRIBUTE_TO_BRANCH_VARIABLE_NAME; +import static org.eclipse.che.plugin.pullrequest.shared.ContributionProjectTypeConstants.CONTRIBUTION_PROJECT_TYPE_ID; +import static java.util.Collections.singletonList; +import static org.eclipse.che.ide.api.constraints.Constraints.FIRST; +import static org.eclipse.che.ide.api.parts.PartStackType.TOOLING; + +/** + * Responsible for setting up contribution mixin for the currently selected project in application context. + * + * @author Vlad Zhukovskyi + * @since 5.0.0 + */ +@Singleton +public class ContributionMixinProvider { + + private final EventBus eventBus; + private final AppContext appContext; + private final WorkspaceAgent workspaceAgent; + private final ContributePartPresenter contributePart; + private final WorkflowExecutor workflowExecutor; + private final VcsServiceProvider vcsServiceProvider; + private final VcsHostingServiceProvider vcsHostingServiceProvider; + + private HandlerRegistration handlerRegistration; + + private Project lastSelected; + + @Inject + public ContributionMixinProvider(EventBus eventBus, + AppContext appContext, + WorkspaceAgent workspaceAgent, + ContributePartPresenter contributePart, + WorkflowExecutor workflowExecutor, + VcsServiceProvider vcsServiceProvider, + VcsHostingServiceProvider vcsHostingServiceProvider) { + this.eventBus = eventBus; + this.appContext = appContext; + this.workspaceAgent = workspaceAgent; + this.contributePart = contributePart; + this.workflowExecutor = workflowExecutor; + this.vcsServiceProvider = vcsServiceProvider; + this.vcsHostingServiceProvider = vcsHostingServiceProvider; + + if (appContext.getFactory() != null) { + handlerRegistration = eventBus.addHandler(FactoryAcceptedEvent.TYPE, new FactoryAcceptedHandler() { + @Override + public void onFactoryAccepted(FactoryAcceptedEvent event) { + handlerRegistration.removeHandler(); + + subscribeToSelectionChangedEvent(); + } + }); + } else { + handlerRegistration = eventBus.addHandler(WorkspaceReadyEvent.getType(), new WorkspaceReadyEvent.WorkspaceReadyHandler() { + @Override + public void onWorkspaceReady(WorkspaceReadyEvent event) { + handlerRegistration.removeHandler(); + + subscribeToSelectionChangedEvent(); + } + }); + } + } + + private void subscribeToSelectionChangedEvent() { + eventBus.addHandler(SelectionChangedEvent.TYPE, new SelectionChangedHandler() { + @Override + public void onSelectionChanged(SelectionChangedEvent event) { + processCurrentProject(); + } + }); + } + + void processCurrentProject() { + final Project rootProject = appContext.getRootProject(); + + if (lastSelected != null && lastSelected.equals(rootProject)) { + return; + } + + final PartStack toolingPartStack = workspaceAgent.getPartStack(TOOLING); + + if (rootProject == null) { + + if (toolingPartStack.containsPart(contributePart)) { + invalidateContext(lastSelected); + hidePart(); + } + } else if (hasVcsService(rootProject)) { + + if (hasContributionMixin(rootProject)) { + + vcsHostingServiceProvider.getVcsHostingService(rootProject).then(new Operation() { + @Override + public void apply(VcsHostingService vcsHostingService) throws OperationException { + workflowExecutor.init(vcsHostingService, rootProject); + addPart(toolingPartStack); + } + }); + } else { + vcsHostingServiceProvider.getVcsHostingService(rootProject) + .then(new Operation() { + @Override + public void apply(final VcsHostingService vcsHostingService) + throws OperationException { + addMixin(rootProject) + .then(new Operation() { + @Override + public void apply(Project project) throws OperationException { + workflowExecutor.init(vcsHostingService, project); + addPart(toolingPartStack); + + lastSelected = project; + } + }) + .catchError(new Operation() { + @Override + public void apply(final PromiseError error) throws OperationException { + invalidateContext(rootProject); + hidePart(); + } + }); + } + }) + .catchError(new Operation() { + @Override + public void apply(final PromiseError error) throws OperationException { + invalidateContext(rootProject); + hidePart(); + } + }); + } + + } else { + invalidateContext(rootProject); + hidePart(); + } + + lastSelected = rootProject; + } + + private void invalidateContext(Project project) { + final Optional context = workflowExecutor.getContext(project.getName()); + if (context.isPresent()) { + workflowExecutor.invalidateContext(context.get().getProject()); + } + } + + private void hidePart() { + workspaceAgent.hidePart(contributePart); + workspaceAgent.removePart(contributePart); + } + + private void addPart(PartStack partStack) { + if (!partStack.containsPart(contributePart)) { + partStack.addPart(contributePart, FIRST); + } + } + + private boolean hasVcsService(Project project) { + return vcsServiceProvider.getVcsService(project) != null; + } + + private boolean hasContributionMixin(Project project) { + return project.getMixins().contains(CONTRIBUTION_PROJECT_TYPE_ID); + } + + private Promise addMixin(final Project project) { + final VcsService vcsService = vcsServiceProvider.getVcsService(project); + + if (vcsService == null || project.getMixins().contains(CONTRIBUTION_PROJECT_TYPE_ID)) { + return Promises.resolve(project); + } + + return vcsService.getBranchName(project) + .thenPromise(new Function>() { + @Override + public Promise apply(String branchName) throws FunctionException { + MutableProjectConfig mutableConfig = new MutableProjectConfig(project); + mutableConfig.getMixins().add(CONTRIBUTION_PROJECT_TYPE_ID); + mutableConfig.getAttributes().put(CONTRIBUTE_TO_BRANCH_VARIABLE_NAME, + singletonList(branchName)); + + return project.update().withBody(mutableConfig).send(); + } + }); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitPresenter.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitPresenter.java new file mode 100644 index 00000000000..051addf14cb --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitPresenter.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.dialogs.commit; + +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsServiceProvider; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.inject.Inject; + +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.notification.NotificationManager; +import org.eclipse.che.ide.api.resources.Project; + +import javax.validation.constraints.NotNull; + +import static org.eclipse.che.plugin.pullrequest.client.dialogs.commit.CommitPresenter.CommitActionHandler.CommitAction.CANCEL; +import static org.eclipse.che.plugin.pullrequest.client.dialogs.commit.CommitPresenter.CommitActionHandler.CommitAction.CONTINUE; +import static org.eclipse.che.plugin.pullrequest.client.dialogs.commit.CommitPresenter.CommitActionHandler.CommitAction.OK; +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; +import static org.eclipse.che.ide.ext.git.client.GitUtil.isUnderGit; + +/** + * This presenter provides base functionality to commit project changes or not before cloning or generating a factory url. + * + * @author Kevin Pollet + */ +public class CommitPresenter implements CommitView.ActionDelegate { + + private final CommitView view; + private final AppContext appContext; + private final VcsServiceProvider vcsServiceProvider; + private final NotificationManager notificationManager; + private CommitActionHandler handler; + + @Inject + public CommitPresenter(@NotNull final CommitView view, + @NotNull final AppContext appContext, + @NotNull final VcsServiceProvider vcsServiceProvider, + @NotNull final NotificationManager notificationManager) { + this.view = view; + this.appContext = appContext; + this.vcsServiceProvider = vcsServiceProvider; + this.notificationManager = notificationManager; + + this.view.setDelegate(this); + } + + /** + * Opens the {@link CommitView}. + * + * @param commitDescription + * the default commit description. + */ + public void showView(@NotNull String commitDescription) { + view.show(commitDescription); + } + + /** + * Sets the {@link CommitPresenter.CommitActionHandler} called after the ok or + * continue action is + * executed. + * + * @param handler + * the handler to set. + */ + public void setCommitActionHandler(final CommitActionHandler handler) { + this.handler = handler; + } + + /** + * Returns if the current project has uncommitted changes. + */ + public void hasUncommittedChanges(final AsyncCallback callback) { + final Project project = appContext.getRootProject(); + if (project == null) { + callback.onFailure(new IllegalStateException("No project opened")); + + } else if (!isUnderGit(project)) { + callback.onFailure(new IllegalStateException("Opened project is not has no Git repository")); + + } else { + vcsServiceProvider.getVcsService(project).hasUncommittedChanges(project, callback); + } + } + + @Override + public void onOk() { + final Project project = appContext.getRootProject(); + if (project != null) { + vcsServiceProvider.getVcsService(project).commit(project, view.isIncludeUntracked(), + view.getCommitDescription(), new AsyncCallback() { + @Override + public void onFailure(final Throwable exception) { + notificationManager.notify(exception.getMessage(), FAIL, FLOAT_MODE); + } + + @Override + public void onSuccess(final Void result) { + view.close(); + + if (handler != null) { + handler.onCommitAction(OK); + } + } + }); + } + } + + @Override + public void onContinue() { + view.close(); + + if (handler != null) { + handler.onCommitAction(CONTINUE); + } + } + + @Override + public void onCancel() { + view.close(); + + if (handler != null) { + handler.onCommitAction(CANCEL); + } + } + + @Override + public void onCommitDescriptionChanged() { + view.setOkButtonEnabled(!view.getCommitDescription().isEmpty()); + } + + public interface CommitActionHandler { + /** + * Called when a commit actions is done on the commit view. + * + * @param action + * the action. + */ + void onCommitAction(CommitAction action); + + enum CommitAction { + OK, + CONTINUE, + CANCEL + } + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitView.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitView.java new file mode 100644 index 00000000000..61893bce0a8 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitView.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.dialogs.commit; + +import org.eclipse.che.ide.api.mvp.View; + +import javax.validation.constraints.NotNull; + +/** + * View for committing uncommitted project changes. + * + * @author Kevin Pollet + */ +public interface CommitView extends View { + /** + * Opens the commit view with the given commit description. + */ + void show(String commitDescription); + + /** + * Close the commit view. + */ + void close(); + + /** + * Returns the current commit description. + * + * @return the current commit description. + */ + @NotNull + String getCommitDescription(); + + /** + * Enables or disables the button OK. + * + * @param enabled + * {@code true} to enable the OK button, {@code false} otherwise. + */ + void setOkButtonEnabled(final boolean enabled); + + /** + * Returns if the untracked files must be added. + * + * @return {@code true} if untracked files must be added, {@code false} otherwise. + */ + boolean isIncludeUntracked(); + + /** + * The action delegate. + */ + interface ActionDelegate { + /** + * Called when project changes must be committed. + */ + void onOk(); + + /** + * Called when project changes must not be committed. + */ + void onContinue(); + + /** + * Called when the operation must be aborted. + */ + void onCancel(); + + /** + * Called when the commit description is changed. + */ + void onCommitDescriptionChanged(); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewImpl.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewImpl.java new file mode 100644 index 00000000000..e7832a70d58 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewImpl.java @@ -0,0 +1,138 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.dialogs.commit; + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import com.google.gwt.core.shared.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.TextArea; +import com.google.inject.Inject; + +import org.eclipse.che.ide.ui.window.Window; + +import javax.validation.constraints.NotNull; + +/** + * UI for {@link CommitView}. + * + * @author Kevin Pollet + */ +public class CommitViewImpl extends Window implements CommitView { + + /** The UI binder for this component. */ + private static final CommitViewUiBinder UI_BINDER = GWT.create(CommitViewUiBinder.class); + + private final Button ok; + + @UiField(provided = true) + ContributeMessages messages; + + @UiField + TextArea commitDescription; + + @UiField + CheckBox includeUntracked; + + private ActionDelegate delegate; + + @Inject + public CommitViewImpl(final ContributeMessages messages) { + this.messages = messages; + + setWidget(UI_BINDER.createAndBindUi(this)); + setTitle(messages.commitDialogTitle()); + + ok = createButton(messages.commitDialogButtonOkText(), "commit-dialog-ok", new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + delegate.onOk(); + } + }); + ok.addStyleName(resources.windowCss().button()); + + final Button continueWithoutCommitting = + createButton(messages.commitDialogButtonContinueText(), "commit-dialog-continue-without-committing", + new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + delegate.onContinue(); + } + }); + + final Button cancel = createButton(messages.commitDialogButtonCancelText(), "commit-dialog-cancel", + new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + delegate.onCancel(); + } + }); + + addButtonToFooter(ok); + addButtonToFooter(continueWithoutCommitting); + addButtonToFooter(cancel); + } + + @Override + public void show(@NotNull String commitDescription) { + this.commitDescription.setText(commitDescription); + new Timer() { + @Override + public void run() { + CommitViewImpl.this.commitDescription.setFocus(true); + } + }.schedule(300); + super.show(); + } + + @Override + public void close() { + hide(); + } + + @NotNull + @Override + public String getCommitDescription() { + return commitDescription.getText(); + } + + @Override + public void setOkButtonEnabled(final boolean enabled) { + ok.setEnabled(enabled); + } + + @Override + public boolean isIncludeUntracked() { + return includeUntracked.getValue(); + } + + @Override + public void setDelegate(final ActionDelegate delegate) { + this.delegate = delegate; + } + + @Override + protected void onClose() { + delegate.onCancel(); + } + + @SuppressWarnings("UnusedParameters") + @UiHandler("commitDescription") + void onCommitDescriptionChanged(final KeyUpEvent event) { + delegate.onCommitDescriptionChanged(); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewImpl.ui.xml b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewImpl.ui.xml new file mode 100644 index 00000000000..540ffaa5f33 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewImpl.ui.xml @@ -0,0 +1,44 @@ + + + + + + + + .border { + margin: 15px; + } + + .margin { + margin-bottom: 5px; + } + + + + + + + + + + + + + + + + + diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewUiBinder.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewUiBinder.java new file mode 100644 index 00000000000..ecf8e96d709 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/commit/CommitViewUiBinder.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.dialogs.commit; + +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiTemplate; +import com.google.gwt.user.client.ui.Widget; + +/** + * {@link com.google.gwt.uibinder.client.UiBinder} interface for the commit dialog. + * + * @author Kevin Pollet + */ +@UiTemplate("CommitViewImpl.ui.xml") +interface CommitViewUiBinder extends UiBinder { +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteAwareTextBox.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteAwareTextBox.java new file mode 100644 index 00000000000..9d713e4eb77 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteAwareTextBox.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.dialogs.paste; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.TextBox; + +/** + * {@link TextBox} that handles onpaste events. + */ +public class PasteAwareTextBox extends TextBox { + + public PasteAwareTextBox() { + sinkEvents(Event.ONPASTE); + } + + public PasteAwareTextBox(final Element element) { + super(element); + sinkEvents(Event.ONPASTE); + } + + @Override + public void onBrowserEvent(final Event event) { + super.onBrowserEvent(event); + switch (event.getTypeInt()) { + case Event.ONPASTE: + event.stopPropagation(); + delayedFireEvent(); + break; + default: + break; + } + } + + /** + * Fires an event, after waiting the state of the textbox the be updated. + */ + private void delayedFireEvent() { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + fireEvent(new PasteEvent()); + } + }); + } + + /** + * Adds a {@link PasteHandler} to the component. + * + * @param handler + * the handler to add + * @return a registration object for removal + */ + public HandlerRegistration addPasteHandler(final PasteHandler handler) { + return addHandler(handler, PasteEvent.TYPE); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteEvent.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteEvent.java new file mode 100644 index 00000000000..d7bffb5dab7 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteEvent.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.dialogs.paste; + +import com.google.gwt.event.shared.GwtEvent; + +/** + * {@link GwtEvent} class for paste events. + */ +public class PasteEvent extends GwtEvent { + /** + * The type of the event. + */ + public static Type TYPE = new Type(); + + @Override + public Type getAssociatedType() { + return TYPE; + } + + @Override + protected void dispatch(final PasteHandler handler) { + handler.onPaste(this); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteHandler.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteHandler.java new file mode 100644 index 00000000000..981045e5fe3 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/dialogs/paste/PasteHandler.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.dialogs.paste; + +import com.google.gwt.event.shared.EventHandler; + +/** + * {@link EventHandler} for {@link PasteEvent}s. + */ +public interface PasteHandler extends EventHandler { + void onPaste(PasteEvent event); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextInvalidatedEvent.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextInvalidatedEvent.java new file mode 100644 index 00000000000..3a752031822 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextInvalidatedEvent.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.events; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.gwt.event.shared.GwtEvent; + +import org.eclipse.che.api.core.model.project.ProjectConfig; + +/** + * This event is fired when context is invalidated. + * + * @author Yevhenii Voevodin + * @see WorkflowExecutor#invalidateContext(ProjectConfig) + */ +public class ContextInvalidatedEvent extends GwtEvent { + + public static final Type TYPE = new Type<>(); + + private final Context context; + + public ContextInvalidatedEvent(final Context context) { + this.context = context; + } + + @Override + public Type getAssociatedType() { + return TYPE; + } + + @Override + protected void dispatch(ContextInvalidatedHandler handler) { + handler.onContextInvalidated(context); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextInvalidatedHandler.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextInvalidatedHandler.java new file mode 100644 index 00000000000..4621ddcd393 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextInvalidatedHandler.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.events; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import com.google.gwt.event.shared.EventHandler; + +/** + * Handler for the {@link ContextInvalidatedEvent}. + * + * @author Yevhenii Voevodin + */ +public interface ContextInvalidatedHandler extends EventHandler { + + /** + * Called when {@code context} is invalidated. + * + * @param context + * invalidated context + */ + void onContextInvalidated(final Context context); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextPropertyChangeEvent.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextPropertyChangeEvent.java new file mode 100644 index 00000000000..0fd96bb6bb6 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextPropertyChangeEvent.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.events; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import com.google.gwt.event.shared.GwtEvent; + +import javax.validation.constraints.NotNull; + +/** + * @author Kevin Pollet + */ +public class ContextPropertyChangeEvent extends GwtEvent { + public static Type TYPE = new Type<>(); + + private final Context context; + private final ContextProperty contextProperty; + + public ContextPropertyChangeEvent(@NotNull final Context context, @NotNull final ContextProperty contextProperty) { + this.context = context; + this.contextProperty = contextProperty; + } + + /** + * Returns the context object. + * + * @return the context object. + */ + public Context getContext() { + return context; + } + + /** + * Returns the property changed. + * + * @return the property changed. + */ + public ContextProperty getContextProperty() { + return contextProperty; + } + + @Override + public Type getAssociatedType() { + return TYPE; + } + + @Override + protected void dispatch(final ContextPropertyChangeHandler handler) { + handler.onContextPropertyChange(this); + } + + public enum ContextProperty { + PROJECT, + ORIGIN_REPOSITORY_OWNER, + ORIGIN_REPOSITORY_NAME, + CONTRIBUTE_TO_BRANCH_NAME, + WORK_BRANCH_NAME + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextPropertyChangeHandler.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextPropertyChangeHandler.java new file mode 100644 index 00000000000..fcbae967d24 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/ContextPropertyChangeHandler.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.events; + +import com.google.gwt.event.shared.EventHandler; + +/** + * Handler to be advised when a property of the context object is changed. + * + * @author Kevin Pollet + */ +public interface ContextPropertyChangeHandler extends EventHandler { + /** + * Called when a property of the context object changed. + * + * @param event + * the {@link ContextPropertyChangeEvent} event. + */ + void onContextPropertyChange(ContextPropertyChangeEvent event); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/CurrentContextChangedEvent.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/CurrentContextChangedEvent.java new file mode 100644 index 00000000000..b9e9f0b421c --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/CurrentContextChangedEvent.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.events; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import com.google.gwt.event.shared.GwtEvent; + +/** + * Sent when current plugin context is changed to an existing one. + * + *

Note that if context is just created then this event won't be fired. + * + * @author Yevhenii Voevodin + */ +public class CurrentContextChangedEvent extends GwtEvent { + + public static final Type TYPE = new Type<>(); + + private final Context context; + + public CurrentContextChangedEvent(final Context context) { + this.context = context; + } + + @Override + public Type getAssociatedType() { + return TYPE; + } + + @Override + protected void dispatch(CurrentContextChangedHandler handler) { + handler.onContextChanged(context); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/CurrentContextChangedHandler.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/CurrentContextChangedHandler.java new file mode 100644 index 00000000000..fba45a8a339 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/CurrentContextChangedHandler.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.events; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import com.google.gwt.event.shared.EventHandler; + +/** + * Handler for {@link CurrentContextChangedEvent}. + * + * @author Yevhenii Voevodin + */ +public interface CurrentContextChangedHandler extends EventHandler { + + /** + * Called when the current context changed. + * + * @param context + * new context + */ + void onContextChanged(final Context context); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/StepEvent.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/StepEvent.java new file mode 100644 index 00000000000..42b8a8672dc --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/StepEvent.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.events; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import com.google.gwt.event.shared.GwtEvent; + +import javax.validation.constraints.NotNull; + +/** + * Event sent when a step is done or in error. + * + * @author Kevin Pollet + */ +public class StepEvent extends GwtEvent { + public static final Type TYPE = new Type<>(); + + private final Step step; + private final boolean success; + private final String message; + private final Context context; + + public StepEvent(final Context context, final Step step, final boolean success) { + this(context, step, success, null); + } + + public StepEvent(final Context context, final Step step, final boolean success, final String message) { + this.step = step; + this.success = success; + this.message = message; + this.context = context; + } + + @Override + public Type getAssociatedType() { + return TYPE; + } + + @Override + protected void dispatch(@NotNull final StepHandler handler) { + if (success) { + handler.onStepDone(this); + + } else { + handler.onStepError(this); + } + } + + public Step getStep() { + return step; + } + + public String getMessage() { + return message; + } + + public Context getContext() { + return context; + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/StepHandler.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/StepHandler.java new file mode 100644 index 00000000000..e4dfe39088e --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/events/StepHandler.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.events; + +import com.google.gwt.event.shared.EventHandler; + +import javax.validation.constraints.NotNull; + +/** + * Handler for step event. + * + * @author Kevin Pollet + */ +public interface StepHandler extends EventHandler { + /** + * Called when a step is successfully done. + * + * @param event + * the step event. + */ + void onStepDone(@NotNull StepEvent event); + + /** + * Called when a step is in error. + * + * @param event + * the step event. + */ + void onStepError(@NotNull StepEvent event); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/inject/PullRequestGinModule.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/inject/PullRequestGinModule.java new file mode 100644 index 00000000000..7688d943ede --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/inject/PullRequestGinModule.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.inject; + +import org.eclipse.che.plugin.pullrequest.client.dialogs.commit.CommitView; +import org.eclipse.che.plugin.pullrequest.client.dialogs.commit.CommitViewImpl; +import org.eclipse.che.plugin.pullrequest.client.parts.contribute.ContributePartView; +import org.eclipse.che.plugin.pullrequest.client.parts.contribute.ContributePartViewImpl; +import org.eclipse.che.plugin.pullrequest.client.steps.AddForkRemoteStepFactory; +import org.eclipse.che.plugin.pullrequest.client.steps.PushBranchOnForkStep; +import org.eclipse.che.plugin.pullrequest.client.steps.PushBranchStepFactory; +import org.eclipse.che.plugin.pullrequest.client.steps.WaitForkOnRemoteStepFactory; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.gwt.inject.client.AbstractGinModule; +import com.google.gwt.inject.client.assistedinject.GinFactoryModuleBuilder; + +import org.eclipse.che.ide.api.extension.ExtensionGinModule; + +import javax.inject.Singleton; + +/** + * Gin module definition for the contributor extension. + */ +@ExtensionGinModule +public class PullRequestGinModule extends AbstractGinModule { + + @Override + protected void configure() { + + // bind the commit dialog view + bind(CommitView.class).to(CommitViewImpl.class); + + // bind the part view + bind(ContributePartView.class).to(ContributePartViewImpl.class); + + // the steps + bind(WorkflowExecutor.class).in(Singleton.class); + bind(PushBranchOnForkStep.class); + install(new GinFactoryModuleBuilder().build(WaitForkOnRemoteStepFactory.class)); + install(new GinFactoryModuleBuilder().build(PushBranchStepFactory.class)); + install(new GinFactoryModuleBuilder().build(AddForkRemoteStepFactory.class)); + + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartPresenter.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartPresenter.java new file mode 100644 index 00000000000..b94ec95c928 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartPresenter.java @@ -0,0 +1,751 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.parts.contribute; + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.events.ContextInvalidatedEvent; +import org.eclipse.che.plugin.pullrequest.client.events.ContextInvalidatedHandler; +import org.eclipse.che.plugin.pullrequest.client.events.ContextPropertyChangeEvent; +import org.eclipse.che.plugin.pullrequest.client.events.ContextPropertyChangeHandler; +import org.eclipse.che.plugin.pullrequest.client.events.CurrentContextChangedEvent; +import org.eclipse.che.plugin.pullrequest.client.events.CurrentContextChangedHandler; +import org.eclipse.che.plugin.pullrequest.client.events.StepEvent; +import org.eclipse.che.plugin.pullrequest.client.events.StepHandler; +import org.eclipse.che.plugin.pullrequest.client.steps.CommitWorkingTreeStep; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.AcceptsOneWidget; +import com.google.gwt.user.client.ui.IsWidget; +import com.google.inject.Singleton; +import com.google.web.bindery.event.shared.EventBus; + +import org.eclipse.che.api.git.shared.Branch; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.dialogs.CancelCallback; +import org.eclipse.che.ide.api.dialogs.DialogFactory; +import org.eclipse.che.ide.api.dialogs.InputCallback; +import org.eclipse.che.ide.api.dialogs.InputValidator; +import org.eclipse.che.ide.api.notification.NotificationManager; +import org.eclipse.che.ide.api.parts.WorkspaceAgent; +import org.eclipse.che.ide.api.parts.base.BasePresenter; +import org.eclipse.che.ide.api.resources.Project; +import org.eclipse.che.ide.util.loging.Log; + +import javax.inject.Inject; +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static com.google.common.base.Strings.nullToEmpty; +import static java.util.Arrays.asList; +import static org.eclipse.che.ide.api.constraints.Constraints.LAST; +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; +import static org.eclipse.che.ide.api.parts.PartStackType.TOOLING; + +/** + * Part for the contribution configuration. + * + * @author Kevin Pollet + */ +@Singleton +public class ContributePartPresenter extends BasePresenter implements ContributePartView.ActionDelegate, + StepHandler, + ContextPropertyChangeHandler, + CurrentContextChangedHandler, + ContextInvalidatedHandler { + private final ContributePartView view; + private final WorkspaceAgent workspaceAgent; + private final ContributeMessages messages; + private final WorkflowExecutor workflowExecutor; + private final AppContext appContext; + private final NotificationManager notificationManager; + private final DialogFactory dialogFactory; + private final Map stagesProviders; + + @Inject + public ContributePartPresenter(final ContributePartView view, + final ContributeMessages messages, + final WorkspaceAgent workspaceAgent, + final EventBus eventBus, + final WorkflowExecutor workflow, + final AppContext appContext, + final NotificationManager notificationManager, + final DialogFactory dialogFactory, + final Map stagesProviders) { + this.view = view; + this.workspaceAgent = workspaceAgent; + this.workflowExecutor = workflow; + this.messages = messages; + this.appContext = appContext; + this.notificationManager = notificationManager; + this.dialogFactory = dialogFactory; + this.stagesProviders = stagesProviders; + + this.view.setDelegate(this); + + view.addContributionTitleChangedHandler(new TextChangedHandler() { + @Override + public void onTextChanged(String newText) { + final Context curContext = workflowExecutor.getCurrentContext(); + curContext.getViewState().setContributionTitle(newText); + } + }); + + view.addContributionCommentChangedHandler(new TextChangedHandler() { + @Override + public void onTextChanged(String newText) { + final Context curContext = workflowExecutor.getCurrentContext(); + curContext.getViewState().setContributionComment(newText); + } + }); + + view.addBranchChangedHandler(new TextChangedHandler() { + @Override + public void onTextChanged(String branchName) { + final Context curContext = workflowExecutor.getCurrentContext(); + if (!branchName.equals(messages.contributePartConfigureContributionSectionContributionBranchNameCreateNewItemText()) && + !branchName.equals(curContext.getWorkBranchName())) { + checkoutBranch(curContext, branchName, false); + } + } + }); + + eventBus.addHandler(StepEvent.TYPE, this); + eventBus.addHandler(ContextPropertyChangeEvent.TYPE, this); + eventBus.addHandler(CurrentContextChangedEvent.TYPE, this); + eventBus.addHandler(ContextInvalidatedEvent.TYPE, this); + } + + public void open() { + resetView(); + workspaceAgent.openPart(ContributePartPresenter.this, TOOLING, LAST); + } + + public void remove() { + workspaceAgent.removePart(ContributePartPresenter.this); + } + + @Override + public void onContribute() { + final Context context = workflowExecutor.getCurrentContext(); + context.getViewState().setStatusMessage(null); + context.getViewState().resetStages(); + + updateView(context, + new NewContributionPanelUpdate(), + new StatusMessageUpdate(), + new StatusSectionUpdate()); + + // Extract configuration values and perform contribution + if (isCurrentContext(context)) { + context.getConfiguration() + .withContributionBranchName(view.getContributionBranchName()) + .withContributionComment(view.getContributionComment()) + .withContributionTitle(view.getContributionTitle()); + } + + if (context.isUpdateMode()) { + workflowExecutor.updatePullRequest(context); + } else { + workflowExecutor.createPullRequest(context); + } + + updateView(context, new ContributionButtonUpdate(messages)); + } + + private void restore(final Context context) { + final List updates = new ArrayList<>(); + // Repository panel updates + updates.add(new RepositoryUrlUpdate()); + updates.add(new ClonedBranchUpdate()); + updates.add(new ProjectNameUpdate()); + // All the other panels are available only if the mode is different from INITIALIZING + // All the other panels are hidden by the #open method, which is called before initialization + if (context.getStatus() != WorkflowStatus.INITIALIZING) { + // Configuration panel updates + updates.add(new WorkBranchUpdate()); + updates.add(new ContributionTitleUpdate()); + updates.add(new ContributionCommentUpdate()); + // Contribution button update + updates.add(new ContributionButtonUpdate(messages)); + // Status panel updates + updates.add(new StatusSectionUpdate()); + updates.add(new StatusMessageUpdate()); + // New contribution panel updates + updates.add(new NewContributionPanelUpdate()); + } + updateView(context, updates); + } + + /** Continuously resets the view state as long as current project is not changed. */ + private void resetView() { + final Project project = appContext.getRootProject(); + final String projectName = project != null ? project.getName() : null; + if (!isCurrentProject(projectName)) return; + view.setRepositoryUrl(""); + + if (!isCurrentProject(projectName)) return; + view.setContributeToBranch(""); + + if (!isCurrentProject(projectName)) return; + view.setContributionBranchName(""); + + if (!isCurrentProject(projectName)) return; + view.setContributionBranchNameEnabled(true); + + if (!isCurrentProject(projectName)) return; + view.setContributionBranchNameList(Collections.emptyList()); + + if (!isCurrentProject(projectName)) return; + view.setContributionTitle(""); + + if (!isCurrentProject(projectName)) return; + view.setProjectName(""); + + if (!isCurrentProject(projectName)) return; + view.setContributionTitleEnabled(true); + + if (!isCurrentProject(projectName)) return; + view.setContributionComment(""); + + if (!isCurrentProject(projectName)) return; + view.setContributionCommentEnabled(true); + + if (!isCurrentProject(projectName)) return; + view.setContributeButtonText(messages.contributePartConfigureContributionSectionButtonContributeText()); + + if (!isCurrentProject(projectName)) return; + view.hideStatusSection(); + + if (!isCurrentProject(projectName)) return; + view.hideNewContributionSection(); + + if (!isCurrentProject(projectName)) return; + updateControls(); + } + + @Override + public void onOpenPullRequestOnVcsHost() { + final Context context = workflowExecutor.getCurrentContext(); + + Window.open(context.getVcsHostingService().makePullRequestUrl(context.getUpstreamRepositoryOwner(), + context.getUpstreamRepositoryName(), + context.getPullRequestIssueNumber()), "", ""); + } + + @Override + public void onNewContribution() { + final Context context = workflowExecutor.getCurrentContext(); + context.getVcsService().checkoutBranch(context.getProject(), context.getContributeToBranchName(), + false, new AsyncCallback() { + @Override + public void onFailure(final Throwable exception) { + notificationManager.notify(exception.getMessage(), FAIL, FLOAT_MODE); + } + + @Override + public void onSuccess(final String branchName) { + resetView(); + workflowExecutor.invalidateContext(context.getProject()); + workflowExecutor.init(context.getVcsHostingService(), context.getProject()); + } + }); + } + + @Override + public void onRefreshContributionBranchNameList() { + updateView(workflowExecutor.getCurrentContext(), new WorkBranchUpdate()); + } + + @Override + public void onCreateNewBranch() { + final Context context = workflowExecutor.getCurrentContext(); + dialogFactory.createInputDialog(messages.contributePartConfigureContributionDialogNewBranchTitle(), + messages.contributePartConfigureContributionDialogNewBranchLabel(), + new CreateNewBranchCallback(context), + new CancelNewBranchCallback(context)) + .withValidator(new BranchNameValidator()) + .show(); + } + + @Override + public void updateControls() { + final String contributionTitle = view.getContributionTitle(); + + boolean isValid = true; + view.showContributionTitleError(false); + + if (contributionTitle == null || contributionTitle.trim().isEmpty()) { + view.showContributionTitleError(true); + isValid = false; + } + + view.setContributeButtonEnabled(isValid); + } + + @Override + public void go(final AcceptsOneWidget container) { + container.setWidget(view.asWidget()); + } + + @NotNull + @Override + public String getTitle() { + return messages.contributePartTitle(); + } + + @Override + public IsWidget getView() { + return view; + } + + @Nullable + @Override + public String getTitleToolTip() { + return null; + } + + @Override + public int getSize() { + return 350; + } + + @Override + public void onStepDone(final StepEvent event) { + final Class stepClass = event.getStep().getClass(); + final Context context = event.getContext(); + + // if it is necessarily to display stages on this step + if (getProvider(context).getDisplayStagesType(context) == stepClass) { + context.getViewState().setStages(getProvidedStages(context)); + updateView(context, new StatusSectionUpdate()); + } + + // if current step is in list of provided stages types + // then this step is done and view should be affected + if (!context.getViewState().getStages().isEmpty() && getProvidedStepDoneTypes(context).contains(stepClass)) { + context.getViewState().setStageDone(true); + updateView(context, new DisplayCurrentStepResultUpdate(true)); + } else if (stepClass == WorkflowExecutor.ChangeContextStatusStep.class) { + if (context.getStatus() == WorkflowStatus.READY_TO_UPDATE_PR) { + final List updates = new ArrayList<>(); + // Display status message + final String message; + if (context.getPreviousStatus() == WorkflowStatus.CREATING_PR) { + message = messages.contributePartStatusSectionContributionCreatedMessage(); + } else { + message = messages.contributePartStatusSectionContributionUpdatedMessage(); + } + context.getViewState().setStatusMessage(message, false); + updates.add(new StatusMessageUpdate()); + + // Contribution button + updates.add(new ContributionButtonUpdate(messages)); + + // Config panel + updates.add(new ContributionTitleUpdate()); + updates.add(new ContributionCommentUpdate()); + + // New contribution panel + updates.add(new NewContributionPanelUpdate()); + + updateView(context, updates); + } + } + } + + @Override + public void onStepError(final StepEvent event) { + final Step step = event.getStep(); + final Class stepClass = step.getClass(); + final Context context = event.getContext(); + if (stepClass == CommitWorkingTreeStep.class) { + if (!context.isUpdateMode()) { + context.getViewState().resetStages(); + context.getViewState().setStatusMessage(null); + updateView(context, + new StatusSectionUpdate(), + new StatusMessageUpdate(), + new NewContributionPanelUpdate(), + new ContributionButtonUpdate(messages)); + } else { + context.getViewState().resetStages(); + context.getViewState().setStatusMessage(event.getMessage(), true); + updateView(context, + new StatusSectionUpdate(), + new StatusMessageUpdate(), + new ContributionButtonUpdate(messages)); + } + } else if (getProvidedStepErrorTypes(context).contains(stepClass)) { + context.getViewState().setStageDone(false); + context.getViewState().setStatusMessage(event.getMessage(), true); + updateView(context, + new DisplayCurrentStepResultUpdate(false), + new StatusMessageUpdate(), + new ContributionButtonUpdate(messages)); + } else { + context.getViewState().resetStages(); + restore(context); + Log.error(ContributePartPresenter.class, "Step error: ", event.getMessage()); + } + } + + @Override + public void onContextPropertyChange(final ContextPropertyChangeEvent event) { + final Context context = event.getContext(); + + switch (event.getContextProperty()) { + case CONTRIBUTE_TO_BRANCH_NAME: + updateView(context, new ClonedBranchUpdate()); + break; + + case WORK_BRANCH_NAME: + updateView(context, new WorkBranchUpdate()); + break; + + case ORIGIN_REPOSITORY_NAME: + case ORIGIN_REPOSITORY_OWNER: + updateView(context, new RepositoryUrlUpdate()); + break; + + case PROJECT: + updateView(context, new ProjectNameUpdate()); + break; + + default: + // nothing to do + break; + } + } + + @Override + public void onContextChanged(final Context context) { + restore(context); + } + + private void updateView(final Context context, final ViewUpdate... updates) { + updateView(context, asList(updates)); + } + + private void updateView(final Context context, final List updates) { + for (Iterator it = updates.iterator(); it.hasNext() && isCurrentContext(context); ) { + it.next().update(view, context); + } + } + + private void updateView(final Context context, final ViewUpdate update) { + if (isCurrentContext(context)) { + update.update(view, context); + } + } + + @Override + public void onContextInvalidated(Context context) { + resetView(); + } + + /** + * Defines a single update operation. + * Single update operations are required as view is shared between multiple projects. + * If context is switched view should not be updated with the updates related to the previous context. + */ + private interface ViewUpdate { + void update(final ContributePartView view, final Context context); + } + + private static class DisplayCurrentStepResultUpdate implements ViewUpdate { + private final boolean result; + + public DisplayCurrentStepResultUpdate(boolean result) { + this.result = result; + } + + @Override + public void update(final ContributePartView view, final Context context) { + view.setCurrentStatusStepStatus(result); + } + } + + private static class NewContributionPanelUpdate implements ViewUpdate { + @Override + public void update(final ContributePartView view, final Context context) { + view.hideNewContributionSection(); + if (context.isUpdateMode()) { + view.showNewContributionSection(context.getVcsHostingService().getName()); + } + } + } + + private static class StatusMessageUpdate implements ViewUpdate { + @Override + public void update(final ContributePartView view, final Context context) { + view.hideStatusSectionMessage(); + final Context.ViewState.StatusMessage statusMessage = context.getViewState().getStatusMessage(); + if (statusMessage != null) { + view.showStatusSectionMessage(statusMessage.getMessage(), statusMessage.isError()); + } + } + } + + private static class StatusSectionUpdate implements ViewUpdate { + @Override + public void update(ContributePartView view, Context context) { + view.hideStatusSection(); + final List stepStatuses = context.getViewState().getStages(); + if (stepStatuses.size() > 0) { + final String[] names = context.getViewState().getStageNames().toArray(new String[stepStatuses.size()]); + view.showStatusSection(names); + for (Boolean stepStatus : context.getViewState().getStageValues()) { + if (stepStatus == null) { + break; + } + view.setCurrentStatusStepStatus(stepStatus); + } + } else if (context.getViewState().getStatusMessage() != null) { + view.showStatusSection(); + } + } + } + + private static class ClonedBranchUpdate implements ViewUpdate { + @Override + public void update(final ContributePartView view, final Context context) { + view.setContributeToBranch(nullToEmpty(context.getContributeToBranchName())); + } + } + + private static class ContributionButtonUpdate implements ViewUpdate { + private final ContributeMessages messages; + + private ContributionButtonUpdate(final ContributeMessages messages) { + this.messages = messages; + } + + @Override + public void update(final ContributePartView view, final Context context) { + final boolean isEnabled = !nullToEmpty(context.getViewState().getContributionTitle()).isEmpty(); + final boolean isInProgress; + final String buttonText; + switch (context.getStatus()) { + case UPDATING_PR: + buttonText = messages.contributePartConfigureContributionSectionButtonContributeUpdateText(); + isInProgress = true; + break; + case READY_TO_UPDATE_PR: + buttonText = messages.contributePartConfigureContributionSectionButtonContributeUpdateText(); + isInProgress = false; + break; + case CREATING_PR: + buttonText = messages.contributePartConfigureContributionSectionButtonContributeText(); + isInProgress = true; + break; + case READY_TO_CREATE_PR: + buttonText = messages.contributePartConfigureContributionSectionButtonContributeText(); + isInProgress = false; + break; + default: + throw new IllegalStateException("Illegal workflow status " + context.getStatus()); + } + view.setContributeButtonText(buttonText); + view.setContributionProgressState(isInProgress); + view.setContributeButtonEnabled(isEnabled); + } + } + + private class ContributionCommentUpdate implements ViewUpdate { + @Override + public void update(final ContributePartView view, final Context context) { + view.setContributionComment(nullToEmpty(context.getViewState().getContributionComment())); + view.setContributionCommentEnabled(context.getStatus() == WorkflowStatus.READY_TO_CREATE_PR); + } + } + + private static class ContributionTitleUpdate implements ViewUpdate { + @Override + public void update(final ContributePartView view, final Context context) { + view.setContributionTitle(nullToEmpty(context.getViewState().getContributionTitle())); + view.setContributionTitleEnabled(context.getStatus() == WorkflowStatus.READY_TO_CREATE_PR); + } + } + + private static class RepositoryUrlUpdate implements ViewUpdate { + @Override + public void update(final ContributePartView view, final Context context) { + final String originRepositoryName = context.getOriginRepositoryName(); + final String originRepositoryOwner = context.getOriginRepositoryOwner(); + if (originRepositoryName != null && originRepositoryOwner != null) { + view.setRepositoryUrl(context.getVcsHostingService() + .makeHttpRemoteUrl(originRepositoryOwner, originRepositoryName)); + } + } + } + + private static class WorkBranchUpdate implements ViewUpdate { + @Override + public void update(final ContributePartView view, final Context context) { + context.getVcsService() + .listLocalBranches(context.getProject(), new AsyncCallback>() { + @Override + public void onFailure(final Throwable notUsed) { + } + + @Override + public void onSuccess(final List branches) { + final List branchNames = new ArrayList<>(); + for (final Branch oneBranch : branches) { + branchNames.add(oneBranch.getDisplayName()); + } + view.setContributionBranchNameList(branchNames); + view.setContributionBranchName(context.getWorkBranchName()); + } + }); + } + } + + private static class ProjectNameUpdate implements ViewUpdate { + @Override + public void update(final ContributePartView view, final Context context) { + view.setProjectName(context.getProject().getName()); + } + } + + private boolean isCurrentContext(final Context context) { + final Project project = appContext.getRootProject(); + + return project != null && Objects.equals(context.getProject().getName(), project.getName()); + } + + private boolean isCurrentProject(final String projectName) { + final Project project = appContext.getRootProject(); + + return project != null && Objects.equals(projectName, project.getName()); + } + + private StagesProvider getProvider(final Context context) { + for (Map.Entry entry : stagesProviders.entrySet()) { + if (entry.getKey().equals(context.getVcsHostingService().getName())) { + return entry.getValue(); + } + } + throw new IllegalStateException("StagesProvider for VCS hosting service " + + context.getVcsHostingService().getName() + + " isn't registered"); + } + + private List getProvidedStages(final Context context) { + return getProvider(context).getStages(context); + } + + private Set> getProvidedStepDoneTypes(final Context context) { + return getProvider(context).getStepDoneTypes(context); + } + + private Set> getProvidedStepErrorTypes(final Context context) { + return getProvider(context).getStepErrorTypes(context); + } + + private static class BranchNameValidator implements InputValidator { + private static final Violation ERROR_WITH_NO_MESSAGE = new InputValidator.Violation() { + @Nullable + @Override + public String getMessage() { + return ""; + } + + @Nullable + @Override + public String getCorrectedValue() { + return null; + } + }; + + @Nullable + @Override + public Violation validate(final String branchName) { + return branchName.matches("[0-9A-Za-z-]+") ? null : ERROR_WITH_NO_MESSAGE; + } + } + + private class CreateNewBranchCallback implements InputCallback { + private final Context context; + + public CreateNewBranchCallback(final Context context) { + this.context = context; + } + + @Override + public void accepted(final String branchName) { + context.getVcsService() + .isLocalBranchWithName(context.getProject(), branchName, new AsyncCallback() { + @Override + public void onFailure(final Throwable exception) { + notificationManager.notify(exception.getMessage(), FAIL, FLOAT_MODE); + } + + @Override + public void onSuccess(final Boolean branchExists) { + if (branchExists) { + notificationManager + .notify(messages.contributePartConfigureContributionDialogNewBranchErrorBranchExists(branchName), + FAIL, + FLOAT_MODE); + + } else { + checkoutBranch(context, branchName, true); + } + } + }); + } + } + + private void checkoutBranch(final Context context, final String branchName, final boolean createNew) { + context.getVcsService() + .checkoutBranch(context.getProject(), + branchName, + createNew, + new AsyncCallback() { + @Override + public void onFailure(final Throwable exception) { + notificationManager.notify(exception.getLocalizedMessage(), FAIL, FLOAT_MODE); + } + + @Override + public void onSuccess(final String notUsed) { + workflowExecutor.invalidateContext(context.getProject()); + workflowExecutor.init(context.getVcsHostingService(), context.getProject()); + } + }); + } + + private class CancelNewBranchCallback implements CancelCallback { + private final Context context; + + private CancelNewBranchCallback(final Context context) { + this.context = context; + } + + @Override + public void cancelled() { + updateView(context, new WorkBranchUpdate()); + } + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartView.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartView.java new file mode 100644 index 00000000000..2911b3b570a --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartView.java @@ -0,0 +1,211 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.parts.contribute; + +import org.eclipse.che.ide.api.mvp.View; +import org.eclipse.che.ide.api.parts.base.BaseActionDelegate; + +import java.util.List; + +/** + * Interface for the contribution configuration shown when the user decides to send their contribution. + */ +public interface ContributePartView extends View { + /** + * Set factory's repository URL. + */ + void setRepositoryUrl(String url); + + /** + * Set factory's contribute to branch name. + */ + void setContributeToBranch(String branch); + + /** + * Set project name. + */ + void setProjectName(String projectName); + + /** + * Returns the contribution branch name. + * + * @return the contribution branch name + */ + String getContributionBranchName(); + + /** + * Sets the contribution branch name. + * + * @param branchName + * the contribution branch name. + */ + void setContributionBranchName(String branchName); + + /** + * Set the contribution branch name list. + * + * @param branchNames + * the branch name list. + */ + void setContributionBranchNameList(List branchNames); + + /** + * Sets the enabled/disabled state of the contribution branch name field. + */ + void setContributionBranchNameEnabled(boolean enabled); + + /** + * Returns the current content of the contribution comment. + * + * @return the comment. + */ + String getContributionComment(); + + /** + * Sets the contribution comment. + * + * @param comment + * the contribution comment. + */ + void setContributionComment(String comment); + + void addContributionCommentChangedHandler(TextChangedHandler handler); + + /** + * Sets the enabled/disabled state of the contribution comment field. + */ + void setContributionCommentEnabled(boolean enabled); + + /** + * Returns the contribution title. + * + * @return the title. + */ + String getContributionTitle(); + + /** + * Sets the contribution title. + * + * @param title + * the contribution title. + */ + void setContributionTitle(String title); + + void addContributionTitleChangedHandler(TextChangedHandler handler); + + void addBranchChangedHandler(TextChangedHandler changeHandler); + + /** + * Sets the enabled/disabled state of the contribution title field. + */ + void setContributionTitleEnabled(boolean enabled); + + /** + * Sets the contribution title input error state. + * + * @param showError + * {@code true} if the contribution title is in error, {@code false} otherwise. + */ + void showContributionTitleError(boolean showError); + + /** + * Sets the enabled/disabled state of the "Contribute" button. + * + * @param enabled + * true to enable, false to disable + */ + void setContributeButtonEnabled(boolean enabled); + + /** + * Sets the text displayed into the "Contribute" button. + * + * @param text + * the text to display + */ + void setContributeButtonText(String text); + + /** + * Shows the status section. + */ + void showStatusSection(String... statusSteps); + + /** + * Sets the current status step state. + * + * @param success + * {@code true} if success, {@code false} otherwise. + */ + void setCurrentStatusStepStatus(boolean success); + + /** + * Shows the status section message. + * + * @param error + * {@code true} if the message displayed is an error, {@code false} otherwise. + */ + void showStatusSectionMessage(String message, boolean error); + + /** + * Hides the status section message. + */ + void hideStatusSectionMessage(); + + /** + * Hides the status section. + */ + void hideStatusSection(); + + /** + * Show the new contribution section. + * + * @param vcsHostName + * the VCS host name. + */ + void showNewContributionSection(String vcsHostName); + + /** + * Hide the new contribution section. + */ + void hideNewContributionSection(); + + /** + * Defines if the contribution is in progress. + * + * @param progress + * {@code true} if the contribution is in progress, {@code false} otherwise. + */ + void setContributionProgressState(boolean progress); + + String getCurrentStatusStepName(); + + /** + * Action delegate interface for the contribution configuration dialog. + */ + interface ActionDelegate extends BaseActionDelegate { + /** Performs any actions appropriate in response to the user having pressed the Contribute button. */ + void onContribute(); + + /** Performs any action appropriate in response to the user having pressed the open pull request on vcs host button. */ + void onOpenPullRequestOnVcsHost(); + + /** Performs any action appropriate in response to the user having pressed the start new contribution button. */ + void onNewContribution(); + + /** Performs any action appropriate in response to the user having pressed the refresh contribution branch names list button. */ + void onRefreshContributionBranchNameList(); + + /** Performs any action appropriate in response to the user having selected the create new branch item. */ + void onCreateNewBranch(); + + /** Performs any action when view state is modified. */ + void updateControls(); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewImpl.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewImpl.java new file mode 100644 index 00000000000..94f75282f3b --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewImpl.java @@ -0,0 +1,498 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.parts.contribute; + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.ContributeResources; +import org.eclipse.che.plugin.pullrequest.client.dialogs.paste.PasteEvent; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.TextArea; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.ValueBoxBase; +import com.google.gwt.user.client.ui.Widget; + +import org.eclipse.che.ide.FontAwesome; +import org.eclipse.che.ide.api.parts.PartStackUIResources; +import org.eclipse.che.ide.api.parts.base.BaseView; +import org.eclipse.che.ide.ui.buttonLoader.ButtonLoaderResources; +import org.eclipse.che.ide.ui.listbox.CustomListBox; +import org.vectomatic.dom.svg.ui.SVGPushButton; + +import javax.inject.Inject; +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; + +import static com.google.gwt.dom.client.Style.Cursor.POINTER; +import static com.google.gwt.dom.client.Style.Unit.PX; + +/** + * Implementation of {@link ContributePartView}. + */ +public class ContributePartViewImpl extends BaseView implements ContributePartView { + + /** The status component. */ + private final StatusSteps statusSteps; + + /** The contribute button. */ + @UiField + Button contributeButton; + + /** The resources for the view. */ + @UiField(provided = true) + ContributeResources resources; + + /** The component for the URL of factory repository. */ + @UiField + Anchor repositoryUrl; + + /** The component for the name of contribute to branch. */ + @UiField + Label contributeToBranch; + + /** The component for the name of the project */ + @UiField + Label projectName; + + /** The input component for the contribution branch name. */ + @UiField + CustomListBox contributionBranchName; + + /** Button used to refresh the contribution branch name list. */ + @UiField + SVGPushButton refreshContributionBranchNameListButton; + + /** The input component for the contribution title. */ + @UiField + TextBox contributionTitle; + + /** The input zone for the contribution comment. */ + @UiField + TextArea contributionComment; + + /** The i18n messages. */ + @UiField(provided = true) + ContributeMessages messages; + + /** The contribution status section. */ + @UiField + FlowPanel statusSection; + + /** The status section message. */ + @UiField + Label statusSectionMessage; + + /** Open on repository host button. */ + @UiField + Button openPullRequestOnVcsHostButton; + + /** The start new contribution section. */ + @UiField + HTMLPanel newContributionSection; + + /** The new contribution button. */ + @UiField + Button newContributionButton; + + /** The contribute button text. */ + private String contributeButtonText; + + @Inject + public ContributePartViewImpl(@NotNull final PartStackUIResources partStackUIResources, + @NotNull final ContributeMessages messages, + @NotNull final ContributeResources resources, + @NotNull final ButtonLoaderResources buttonLoaderResources, + @NotNull final ContributePartViewUiBinder uiBinder) { + super(partStackUIResources); + + this.messages = messages; + this.resources = resources; + this.statusSteps = new StatusSteps(); + + setContentWidget(uiBinder.createAndBindUi(this)); + + setTitle(messages.contributePartTitle()); + + this.contributeButtonText = contributeButton.getText(); + this.contributeButton.addStyleName(buttonLoaderResources.Css().buttonLoader()); + + this.refreshContributionBranchNameListButton.getElement().getStyle().setWidth(23, PX); + this.refreshContributionBranchNameListButton.getElement().getStyle().setHeight(20, PX); + this.refreshContributionBranchNameListButton.getElement().getStyle().setCursor(POINTER); + this.refreshContributionBranchNameListButton.getElement().getStyle().setProperty("fill", "#dbdbdb"); + + this.statusSection.setVisible(false); + this.newContributionSection.setVisible(false); + this.contributionTitle.getElement().setPropertyString("placeholder", + messages.contributePartConfigureContributionSectionContributionTitlePlaceholder()); + this.contributionComment.getElement().setPropertyString("placeholder", + messages.contributePartConfigureContributionSectionContributionCommentPlaceholder()); + + this.statusSection.insert(statusSteps, 1); + } + + @Override + public void setRepositoryUrl(final String url) { + repositoryUrl.setHref(url); + repositoryUrl.setText(url); + } + + @Override + public void setContributeToBranch(final String branch) { + contributeToBranch.setText(branch); + } + + @Override + public void setProjectName(String projectName) { + this.projectName.setText(projectName); + } + + @Override + public void setContributeButtonText(final String text) { + contributeButton.setText(text); + contributeButtonText = contributeButton.getText(); + } + + @Override + public String getContributionBranchName() { + final int selectedIndex = contributionBranchName.getSelectedIndex(); + return selectedIndex == -1 ? null : contributionBranchName.getValue(selectedIndex); + } + + @Override + public void setContributionBranchName(final String branchName) { + for (int i = 0; i < contributionBranchName.getItemCount(); i++) { + if (contributionBranchName.getValue(i).equals(branchName)) { + contributionBranchName.setSelectedIndex(i); + return; + } + } + + if (contributionBranchName.getItemCount() > 1) { + contributionBranchName.setSelectedIndex(1); + } + } + + @Override + public void setContributionBranchNameList(final List branchNames) { + final String selectedBranchName = getContributionBranchName(); + + contributionBranchName.clear(); + contributionBranchName.addItem(messages.contributePartConfigureContributionSectionContributionBranchNameCreateNewItemText()); + for (final String oneBranchName : branchNames) { + contributionBranchName.addItem(oneBranchName); + } + + setContributionBranchName(selectedBranchName); + } + + @Override + public String getContributionComment() { + return contributionComment.getValue(); + } + + @Override + public void setContributionComment(final String comment) { + contributionComment.setText(comment); + } + + @Override + public void addContributionCommentChangedHandler(TextChangedHandler handler) { + contributionComment.addKeyUpHandler(new TextChangedHandlerAdapter(handler)); + } + + @Override + public String getContributionTitle() { + return contributionTitle.getValue(); + } + + @Override + public void setContributionTitle(final String title) { + contributionTitle.setText(title); + } + + @Override + public void addContributionTitleChangedHandler(TextChangedHandler handler) { + contributionTitle.addKeyUpHandler(new TextChangedHandlerAdapter(handler)); + } + + @Override + public void addBranchChangedHandler(final TextChangedHandler changeHandler) { + contributionBranchName.addChangeHandler(new ChangeHandler() { + @Override + public void onChange(ChangeEvent event) { + changeHandler.onTextChanged(contributionBranchName.getSelectedItemText()); + } + }); + } + + @Override + public void setContributionBranchNameEnabled(final boolean enabled) { + contributionBranchName.setEnabled(enabled); + } + + @Override + public void setContributionCommentEnabled(final boolean enabled) { + contributionComment.setEnabled(enabled); + if (!enabled) { + contributionComment.getElement().getStyle().setBackgroundColor("#5a5c5c"); + } else { + contributionComment.getElement().getStyle().clearBackgroundColor(); + } + } + + @Override + public void setContributionTitleEnabled(final boolean enabled) { + contributionTitle.setEnabled(enabled); + } + + @Override + public void setContributeButtonEnabled(final boolean enabled) { + contributeButton.setEnabled(enabled); + } + + @Override + public void showContributionTitleError(final boolean showError) { + if (showError) { + contributionTitle.addStyleName(resources.contributeCss().inputError()); + } else { + contributionTitle.removeStyleName(resources.contributeCss().inputError()); + } + } + + @Override + public void showStatusSection(final String... statusSteps) { + this.statusSteps.removeAll(); + for (final String oneStatusStep : statusSteps) { + this.statusSteps.addStep(oneStatusStep); + } + statusSection.setVisible(true); + } + + @Override + public void setCurrentStatusStepStatus(boolean success) { + statusSteps.setCurrentStepStatus(success); + } + + @Override + public String getCurrentStatusStepName() { + return statusSteps.getCurrentStepName(); + } + + @Override + public void showStatusSectionMessage(final String message, final boolean error) { + if (error) { + statusSectionMessage.addStyleName(resources.contributeCss().errorMessage()); + } else { + statusSectionMessage.removeStyleName(resources.contributeCss().errorMessage()); + } + + statusSectionMessage.setText(message); + statusSectionMessage.setVisible(true); + } + + @Override + public void hideStatusSectionMessage() { + statusSectionMessage.setVisible(false); + } + + @Override + public void hideStatusSection() { + statusSection.setVisible(false); + } + + @Override + public void setContributionProgressState(final boolean progress) { + if (progress) { + contributeButton.setHTML(""); + } else { + contributeButton.setText(contributeButtonText); + } + } + + @Override + public void showNewContributionSection(final String vcsHostName) { + openPullRequestOnVcsHostButton + .setText(messages.contributePartNewContributionSectionButtonOpenPullRequestOnVcsHostText(vcsHostName)); + newContributionSection.setVisible(true); + } + + @Override + public void hideNewContributionSection() { + newContributionSection.setVisible(false); + } + + @SuppressWarnings("UnusedParameters") + @UiHandler("contributionBranchName") + protected void contributionBranchNameChange(final ChangeEvent event) { + final int selectedIndex = contributionBranchName.getSelectedIndex(); + if (selectedIndex == 0) { + delegate.onCreateNewBranch(); + } + } + + @SuppressWarnings("UnusedParameters") + @UiHandler("refreshContributionBranchNameListButton") + protected void refreshContributionBranchNameList(final ClickEvent event) { + delegate.onRefreshContributionBranchNameList(); + } + + @SuppressWarnings("UnusedParameters") + @UiHandler("contributionComment") + protected void contributionCommentChanged(final ValueChangeEvent event) { + delegate.updateControls(); + } + + @SuppressWarnings("UnusedParameters") + @UiHandler("contributionTitle") + protected void contributionTitleChanged(final ValueChangeEvent event) { + delegate.updateControls(); + } + + @SuppressWarnings("UnusedParameters") + @UiHandler("openPullRequestOnVcsHostButton") + protected void openPullRequestOnVcsHostClick(final ClickEvent event) { + delegate.onOpenPullRequestOnVcsHost(); + } + + @SuppressWarnings("UnusedParameters") + @UiHandler("newContributionButton") + protected void newContributionClick(final ClickEvent event) { + delegate.onNewContribution(); + } + + @SuppressWarnings("UnusedParameters") + @UiHandler("contributeButton") + protected void contributeClick(final ClickEvent event) { + delegate.onContribute(); + } + + @SuppressWarnings("UnusedParameters") + @UiHandler("contributionTitle") + protected void contributionTitleKeyUp(final KeyUpEvent event) { + delegate.updateControls(); + } + + @SuppressWarnings("UnusedParameters") + @UiHandler("contributionTitle") + protected void contributionTitlePaste(final PasteEvent event) { + delegate.updateControls(); + } + + private class StatusSteps extends FlowPanel { + private final List steps; + private int currentStep; + + private StatusSteps() { + this.currentStep = 0; + this.steps = new ArrayList<>(); + + addStyleName(resources.contributeCss().statusSteps()); + } + + public void addStep(final String label) { + final StatusStep statusStep = new StatusStep(steps.size() + 1, label); + + steps.add(statusStep); + add(statusStep); + } + + public void removeAll() { + clear(); + currentStep = 0; + steps.clear(); + } + + public void setCurrentStepStatus(final boolean status) { + steps.get(currentStep).setStatus(status); + currentStep++; + } + + public String getCurrentStepName() { + return steps.get(currentStep).getLabel(); + } + } + + private class StatusStep extends FlowPanel { + private final SimplePanel status; + private final String label; + + private StatusStep(final int index, final String label) { + final Label indexLabel = new Label(); + final Label titleLabel = new Label(this.label = label); + this.status = new SimplePanel(); + + add(indexLabel); + add(titleLabel); + add(this.status); + + // initialize panel style + addStyleName(resources.contributeCss().stepLabelRow()); + + // initialize index style + indexLabel.addStyleName(resources.contributeCss().statusIndexStepLabel()); + + // initialize label style + titleLabel.addStyleName(resources.contributeCss().statusTitleStepLabel()); + + // initialize status style + this.status.addStyleName(resources.contributeCss().stepLabel()); + } + + public void setStatus(final boolean success) { + status.clear(); + status.add(getStatusIcon(success)); + } + + public String getLabel() { + return label; + } + + private Widget getStatusIcon(final boolean success) { + final Widget icon = new HTML(success ? FontAwesome.CHECK : FontAwesome.EXCLAMATION_TRIANGLE); + + icon.addStyleName(success ? resources.contributeCss().checkIcon() : resources.contributeCss().errorIcon()); + + return icon; + } + } + + /** Adapts {@link TextChangedHandler} to the {@link KeyUpEvent}. */ + private static class TextChangedHandlerAdapter implements KeyUpHandler { + private final TextChangedHandler handler; + + public TextChangedHandlerAdapter(TextChangedHandler handler) { + this.handler = handler; + } + + @Override + public void onKeyUp(KeyUpEvent event) { + if (event.getSource() instanceof ValueBoxBase) { + handler.onTextChanged(((ValueBoxBase)event.getSource()).getText()); + } + } + } + +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewImpl.ui.xml b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewImpl.ui.xml new file mode 100644 index 00000000000..2a1e088ea1e --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewImpl.ui.xml @@ -0,0 +1,209 @@ + + + + + + + .panel { + font-size: 11px; + position: relative; + overflow: hidden; + white-space: nowrap; + } + + .border { + border-top: 1px solid textFieldBorderColor; + margin-bottom: 5px; + } + + .section { + display: flex; + flex-direction: column; + padding: 5px 5px 5px 0; + } + + .section > * { + margin-left: 20px; + } + + .section .title { + position: relative; + top: 0; + font-size: 12px; + font-weight: bold; + margin-bottom: 10px; + color: mainFontColor; + margin-left: 10px; + } + + .section button { + height: 25px; + padding: 0 20px; + } + + .fieldItem { + display: flex; + flex-direction: column; + justify-content: flex-start; + margin-bottom: 7px; + overflow: visible; + } + + .fieldItem .field { + flex-grow: 1; + } + + .fieldItem .field span { + display: inherit; + } + + .horizontal { + color: mainFontColor; + display: inline-block; + text-align: left; + height: 18px; + width: literal("calc(100% - 20px)"); + } + + .link { + color: outputLinkColor !important; + text-decoration: underline; + } + + .horizontal .field { + color: inherit; + width: inherit; + overflow: hidden; + text-overflow: ellipsis; + } + + .icon { + display: inline-block; + text-decoration: none; + text-align: center; + line-height: 18px; + margin-right: 5px; + width: 15px; + float: left; + color: inherit; + } + + .fieldItem .label { + float: left; + display: flex; + margin-right: 5px; + margin-bottom: 5px; + } + + .fixed-textarea { + overflow: scroll; + resize: none; + } + + .contributeButton { + align-self: flex-end; + } + + .statusMessage { + margin-top: 1em; + margin-bottom: 1em; + align-self: center; + width: auto; + line-height: 15px; + display: inline-box; + white-space: pre-wrap; + word-break: normal; + } + + .section.repository { + background: inherit; + } + + .branchField { + display: flex; + } + + .branchField svg { + flex-shrink: 0; + } + + .repository .fieldItem .field { + font-weight: bold; + display: inline-block; + } + + .section.newContribution button { + align-self: center; + margin-top: 1em; + } + + + +

+ +
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewUiBinder.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewUiBinder.java new file mode 100644 index 00000000000..160329ce3a1 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/ContributePartViewUiBinder.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.parts.contribute; + +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiTemplate; +import com.google.gwt.user.client.ui.ScrollPanel; + +/** + * {@link com.google.gwt.uibinder.client.UiBinder} interface for the configure contribution dialog. + */ +@UiTemplate("ContributePartViewImpl.ui.xml") +public interface ContributePartViewUiBinder extends UiBinder { +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/StagesProvider.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/StagesProvider.java new file mode 100644 index 00000000000..f30306b493f --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/StagesProvider.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.parts.contribute; + +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingService; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; + +import java.util.List; +import java.util.Set; + +/** + * Provider should be implemented one per {@link VcsHostingService}. + * + *

Binding example: + *

{@code
+ *      final GinMapBinder stagesProvider
+ *              = GinMapBinder.newMapBinder(binder(),
+ *                                          String.class,
+ *                                          StagesProvider.class);
+ *      stagesProvider.addBinding(GitHubHostingService.SERVICE_NAME).to(GithubStagesProvider.class);
+ * }
+ * 
+ * + * @author Yevhenii Voevodin + */ +public interface StagesProvider { + + /** + * Returns the list of stages which should be displayed + * when pull request update or creation starts. + * + * @param context + * current execution context + * @return the list of stages + */ + List getStages(final Context context); + + /** + * When step is done and its class is result of this method + * then current stage is considered as successfully done. + * + * @param context + * current execution context + * @return react classes + */ + Set> getStepDoneTypes(final Context context); + + + /** + * When step is done with an error and its class is result of this method + * then current stage is considered as successfully done. + * + * @param context + * current execution context + * @return error react classes + */ + Set> getStepErrorTypes(final Context context); + + /** + * Stages are shown only once and the time to show stages is defined + * by return type of this method. If that step(which type is returned) is + * successfully executed then {@link #getStages(Context)} method will be used + * to show the stages. It is needed for dynamic stages list detection + * (e.g. when workflow configures context in create/update chains). + * + * @param context + * current execution context + * @return returns step class after which successful execution stages should be shown + */ + Class getDisplayStagesType(final Context context); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/TextChangedHandler.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/TextChangedHandler.java new file mode 100644 index 00000000000..0cbea6b5049 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/parts/contribute/TextChangedHandler.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.parts.contribute; + +/** + * Used to detect if pull request title/comment/branch is changed. + * + * @author Yevhenii Voevodin + * @see ContributePartView#addBranchChangedHandler(TextChangedHandler) + * @see ContributePartView#addContributionCommentChangedHandler(TextChangedHandler) + * @see ContributePartView#addContributionTitleChangedHandler(TextChangedHandler) + */ +public interface TextChangedHandler { + + /** + * Called when title/comment/branch is changed + * + * @param newText + * new text content + */ + void onTextChanged(String newText); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddForkRemoteStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddForkRemoteStep.java new file mode 100644 index 00000000000..33e204fe109 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddForkRemoteStep.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsServiceProvider; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.inject.assistedinject.Assisted; + +import org.eclipse.che.api.git.shared.Remote; +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; + +import javax.inject.Inject; +import java.util.List; + +/** + * Adds the forked remote repository to the remotes of the project. + */ +public class AddForkRemoteStep implements Step { + private final static String ORIGIN_REMOTE_NAME = "origin"; + private final static String FORK_REMOTE_NAME = "fork"; + + private final Step delegate; + private final String remoteUrl; + private final VcsServiceProvider vcsServiceProvider; + private final ContributeMessages messages; + + @Inject + public AddForkRemoteStep(@Assisted("delegate") Step delegate, + @Assisted("remoteUrl") String remoteUrl, + final VcsServiceProvider vcsServiceProvider, + final ContributeMessages messages) { + this.delegate = delegate; + this.remoteUrl = remoteUrl; + this.vcsServiceProvider = vcsServiceProvider; + this.messages = messages; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + final String originRepositoryOwner = context.getOriginRepositoryOwner(); + final String originRepositoryName = context.getOriginRepositoryName(); + final String upstreamRepositoryOwner = context.getUpstreamRepositoryOwner(); + final String upstreamRepositoryName = context.getUpstreamRepositoryName(); + + // the fork remote has to be added only if we cloned the upstream else it's origin + if (originRepositoryOwner.equalsIgnoreCase(upstreamRepositoryOwner) && + originRepositoryName.equalsIgnoreCase(upstreamRepositoryName)) { + checkRemotePresent(executor, context, remoteUrl); + } else { + context.setForkedRemoteName(ORIGIN_REMOTE_NAME); + proceed(executor, context); + } + } + + private void checkRemotePresent(final WorkflowExecutor executor, final Context context, final String remoteUrl) { + vcsServiceProvider.getVcsService(context.getProject()).listRemotes(context.getProject()) + .then(new Operation>() { + @Override + public void apply(List result) throws OperationException { + for (final Remote remote : result) { + if (FORK_REMOTE_NAME.equals(remote.getName())) { + context.setForkedRemoteName(FORK_REMOTE_NAME); + if (remoteUrl.equals(remote.getUrl())) { + // all is correct, continue + proceed(executor, context); + + } else { + replaceRemote(executor, context, remoteUrl); + } + // leave the method, do not go to addRemote(...) + return; + } + } + addRemote(executor, context, remoteUrl); + } + }).catchError(new Operation() { + @Override + public void apply(PromiseError arg) throws OperationException { + executor.fail(delegate, context, messages.stepAddForkRemoteErrorCheckRemote()); + } + }); + } + + /** + * Add the remote to the project. + * + * @param executor + * the {@link WorkflowExecutor}. + * @param remoteUrl + * the url of the remote + */ + private void addRemote(final WorkflowExecutor executor, final Context context, final String remoteUrl) { + vcsServiceProvider.getVcsService(context.getProject()) + .addRemote(context.getProject(), FORK_REMOTE_NAME, remoteUrl, new AsyncCallback() { + @Override + public void onSuccess(final Void notUsed) { + context.setForkedRemoteName(FORK_REMOTE_NAME); + + proceed(executor, context); + } + + @Override + public void onFailure(final Throwable exception) { + executor.fail(delegate, context, messages.stepAddForkRemoteErrorAddFork()); + } + }); + } + + /** + * Removes the fork remote from the project before adding it with the correct URL. + * + * @param executor + * the {@link WorkflowExecutor}. + * @param remoteUrl + * the url of the remote + */ + private void replaceRemote(final WorkflowExecutor executor, final Context context, final String remoteUrl) { + vcsServiceProvider.getVcsService(context.getProject()) + .deleteRemote(context.getProject(), FORK_REMOTE_NAME, new AsyncCallback() { + @Override + public void onSuccess(final Void result) { + addRemote(executor, context, remoteUrl); + } + + @Override + public void onFailure(final Throwable caught) { + executor.fail(delegate, + context, + messages.stepAddForkRemoteErrorSetForkedRepositoryRemote()); + } + }); + } + + private void proceed(final WorkflowExecutor executor, final Context context) { + executor.done(delegate, context); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddForkRemoteStepFactory.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddForkRemoteStepFactory.java new file mode 100644 index 00000000000..7acb33353a1 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddForkRemoteStepFactory.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import com.google.inject.assistedinject.Assisted; + +/** + * @author Mihail Kuznyetsov + */ +public interface AddForkRemoteStepFactory { + AddForkRemoteStep create(@Assisted("delegate") Step delegate, + @Assisted("remoteUrl") String remoteUrl); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddHttpForkRemoteStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddHttpForkRemoteStep.java new file mode 100644 index 00000000000..d803978389a --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddHttpForkRemoteStep.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.inject.Singleton; + +import javax.inject.Inject; + +/** + * Add HTTP fork remote URL to repository. + * + * @author Mihail Kuznyetsov + */ +@Singleton +public class AddHttpForkRemoteStep implements Step { + private final AddForkRemoteStepFactory addForkRemoteStepFactory; + + @Inject + public AddHttpForkRemoteStep(AddForkRemoteStepFactory addForkRemoteStepFactory) { + this.addForkRemoteStepFactory = addForkRemoteStepFactory; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + String remoteUrl = context.getVcsHostingService().makeHttpRemoteUrl(context.getHostUserLogin(), context.getForkedRepositoryName()); + addForkRemoteStepFactory.create(this, remoteUrl) + .execute(executor, context); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddReviewFactoryLinkStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddReviewFactoryLinkStep.java new file mode 100644 index 00000000000..09df1d7d5bf --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddReviewFactoryLinkStep.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.shared.dto.Configuration; +import com.google.inject.Singleton; + +/** + * Adds a factory link to the contribution comment. + * + * @author Kevin Pollet + */ +@Singleton +public class AddReviewFactoryLinkStep implements Step { + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + final String reviewFactoryUrl = context.getReviewFactoryUrl(); + final Configuration contributionConfiguration = context.getConfiguration(); + final String formattedReviewFactoryUrl = context.getVcsHostingService().formatReviewFactoryUrl(reviewFactoryUrl); + final String contributionCommentWithReviewFactoryUrl = formattedReviewFactoryUrl + + "\n\n" + + contributionConfiguration.getContributionComment(); + contributionConfiguration.withContributionComment(contributionCommentWithReviewFactoryUrl); + + executor.done(this, context); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddSshForkRemoteStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddSshForkRemoteStep.java new file mode 100644 index 00000000000..b895027fb10 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AddSshForkRemoteStep.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.inject.Singleton; + +import javax.inject.Inject; + +/** + * Add SSH fork remote URL to repository. + * + * @author Mihail Kuznyetsov + */ +@Singleton +public class AddSshForkRemoteStep implements Step { + private final AddForkRemoteStepFactory addForkRemoteStepFactory; + + @Inject + public AddSshForkRemoteStep(AddForkRemoteStepFactory addForkRemoteStepFactory) { + this.addForkRemoteStepFactory = addForkRemoteStepFactory; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + String remoteUrl = context.getVcsHostingService().makeSSHRemoteUrl(context.getHostUserLogin(), context.getForkedRepositoryName()); + addForkRemoteStepFactory.create(this, remoteUrl) + .execute(executor, context); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AuthorizeCodenvyOnVCSHostStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AuthorizeCodenvyOnVCSHostStep.java new file mode 100644 index 00000000000..af03755ddef --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/AuthorizeCodenvyOnVCSHostStep.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.shared.dto.HostUser; +import com.google.inject.Singleton; + +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.notification.NotificationManager; +import org.eclipse.che.ide.commons.exception.UnauthorizedException; + +import javax.inject.Inject; + +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; + +/** + * This step authorizes Codenvy on the VCS Host. + * + * @author Kevin Pollet + * @author Yevhenii Voevodin + */ +@Singleton +public class AuthorizeCodenvyOnVCSHostStep implements Step { + private final NotificationManager notificationManager; + private final AppContext appContext; + private final ContributeMessages messages; + + @Inject + public AuthorizeCodenvyOnVCSHostStep(final NotificationManager notificationManager, + final AppContext appContext, + final ContributeMessages messages) { + this.notificationManager = notificationManager; + this.appContext = appContext; + this.messages = messages; + } + + @Override + public void execute(final WorkflowExecutor executor, Context context) { + context.getVcsHostingService() + .getUserInfo() + .then(authSuccessOp(executor, context)) + .catchError(getUserErrorOp(executor, context)); + } + + private Operation authSuccessOp(final WorkflowExecutor executor, final Context context) { + return new Operation() { + @Override + public void apply(HostUser user) throws OperationException { + context.setHostUserLogin(user.getLogin()); + executor.done(AuthorizeCodenvyOnVCSHostStep.this, context); + } + }; + } + + private Operation getUserErrorOp(final WorkflowExecutor executor, final Context context) { + return new Operation() { + @Override + public void apply(PromiseError error) throws OperationException { + try { + throw error.getCause(); + } catch (UnauthorizedException unEx) { + authenticate(executor, context); + } catch (Throwable thr) { + handleThrowable(thr, executor, context); + } + } + }; + } + + private void authenticate(final WorkflowExecutor executor, final Context context) { + context.getVcsHostingService() + .authenticate(appContext.getCurrentUser()) + .then(authSuccessOp(executor, context)) + .catchError(new Operation() { + @Override + public void apply(PromiseError err) throws OperationException { + try { + throw err.getCause(); + } catch (UnauthorizedException unEx) { + notificationManager.notify(messages.stepAuthorizeCodenvyOnVCSHostErrorCannotAccessVCSHostTitle(), + messages.stepAuthorizeCodenvyOnVCSHostErrorCannotAccessVCSHostContent(), + FAIL, + FLOAT_MODE); + executor.fail(AuthorizeCodenvyOnVCSHostStep.this, context, unEx.getLocalizedMessage()); + } catch (Throwable thr) { + handleThrowable(thr, executor, context); + } + } + }); + } + + private void handleThrowable(final Throwable thr, final WorkflowExecutor workflow, final Context context) { + notificationManager.notify(thr.getLocalizedMessage(), FAIL, FLOAT_MODE); + workflow.fail(this, context, thr.getLocalizedMessage()); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CheckBranchToPush.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CheckBranchToPush.java new file mode 100644 index 00000000000..ca1580663d5 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CheckBranchToPush.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * Checks that working branch is different from the cloned branch. + * + * @author Yevhenii Voevodin + */ +@Singleton +public class CheckBranchToPush implements Step { + + private final ContributeMessages messages; + + @Inject + public CheckBranchToPush(final ContributeMessages messages) { + this.messages = messages; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + if (context.getWorkBranchName().equals(context.getContributeToBranchName())) { + executor.fail(this, + context, + messages.stepCheckBranchClonedBranchIsEqualToWorkBranch()); + } else { + executor.done(this, context); + } + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CommitWorkingTreeStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CommitWorkingTreeStep.java new file mode 100644 index 00000000000..b843c8687f9 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CommitWorkingTreeStep.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.dialogs.commit.CommitPresenter; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.shared.dto.Configuration; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.inject.Singleton; + +import org.eclipse.che.ide.api.notification.NotificationManager; + +import javax.inject.Inject; + +import static org.eclipse.che.plugin.pullrequest.client.dialogs.commit.CommitPresenter.CommitActionHandler.CommitAction.CANCEL; +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; + +/** + * This step allow the user to commit the current working tree if the git repository status is not clean. + * + * @author Kevin Pollet + * @author Yevhenii Voevodin + */ +@Singleton +public class CommitWorkingTreeStep implements Step { + private final CommitPresenter commitPresenter; + private final ContributeMessages messages; + private final NotificationManager notificationManager; + + @Inject + public CommitWorkingTreeStep(final CommitPresenter commitPresenter, + final ContributeMessages messages, + final NotificationManager notificationManager) { + this.commitPresenter = commitPresenter; + this.messages = messages; + this.notificationManager = notificationManager; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + final Configuration configuration = context.getConfiguration(); + + commitPresenter.setCommitActionHandler(new CommitPresenter.CommitActionHandler() { + @Override + public void onCommitAction(final CommitAction action) { + if (action == CANCEL) { + executor.fail(CommitWorkingTreeStep.this, context, messages.stepCommitCanceled()); + } else { + executor.done(CommitWorkingTreeStep.this, context); + } + } + }); + commitPresenter.hasUncommittedChanges(new AsyncCallback() { + @Override + public void onFailure(final Throwable exception) { + notificationManager.notify(exception.getLocalizedMessage(), FAIL, FLOAT_MODE); + executor.fail(CommitWorkingTreeStep.this, context, exception.getLocalizedMessage()); + } + + @Override + public void onSuccess(final Boolean hasUncommittedChanges) { + if (hasUncommittedChanges) { + commitPresenter + .showView(messages.contributorExtensionDefaultCommitDescription(configuration.getContributionBranchName(), + configuration.getContributionTitle())); + } else { + executor.done(CommitWorkingTreeStep.this, context); + } + } + }); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CreateForkStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CreateForkStep.java new file mode 100644 index 00000000000..3d632fe56ca --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/CreateForkStep.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoUserForkException; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.shared.dto.Repository; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; + +/** + * Create a fork of the contributed project (upstream) to push the user's contribution. + */ +@Singleton +public class CreateForkStep implements Step { + private final ContributeMessages messages; + + @Inject + public CreateForkStep(final ContributeMessages messages) { + this.messages = messages; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + final String originRepositoryOwner = context.getOriginRepositoryOwner(); + final String originRepositoryName = context.getOriginRepositoryName(); + final String upstreamRepositoryOwner = context.getUpstreamRepositoryOwner(); + final String upstreamRepositoryName = context.getUpstreamRepositoryName(); + + // the upstream repository has been cloned a fork must be created + if (originRepositoryOwner.equalsIgnoreCase(upstreamRepositoryOwner) && + originRepositoryName.equalsIgnoreCase(upstreamRepositoryName)) { + + context.getVcsHostingService() + .getUserFork(context.getHostUserLogin(), upstreamRepositoryOwner, upstreamRepositoryName) + .then(new Operation() { + @Override + public void apply(Repository fork) throws OperationException { + proceed(fork.getName(), executor, context); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError error) throws OperationException { + if (error.getCause() instanceof NoUserForkException) { + createFork(executor, context, upstreamRepositoryOwner, upstreamRepositoryName); + return; + } + + executor.fail(CreateForkStep.this, context, error.getCause().getMessage()); + } + }); + } else { + // user fork has been cloned + proceed(originRepositoryName, executor, context); + } + } + + private void createFork(final WorkflowExecutor executor, + final Context context, + final String upstreamRepositoryOwner, + final String upstreamRepositoryName) { + context.getVcsHostingService() + .fork(upstreamRepositoryOwner, upstreamRepositoryName) + .then(new Operation() { + @Override + public void apply(Repository result) throws OperationException { + proceed(result.getName(), executor, context); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError err) throws OperationException { + final String errorMessage = messages.stepCreateForkErrorCreatingFork(upstreamRepositoryOwner, + upstreamRepositoryName, + err.getCause().getMessage()); + executor.fail(CreateForkStep.this, context, errorMessage); + } + }); + } + + private void proceed(final String forkName, final WorkflowExecutor executor, final Context context) { + context.setForkedRepositoryName(forkName); + executor.done(this, context); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineExecutionConfiguration.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineExecutionConfiguration.java new file mode 100644 index 00000000000..08a079b1205 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineExecutionConfiguration.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; + +/** + * This step defines ability to create forks. + * + * @author Mihail Kuznyetsov + */ +public class DefineExecutionConfiguration implements Step { + + @Override + public void execute(WorkflowExecutor executor, Context context) { + context.setForkAvailable(!context.getOriginRepositoryOwner().equals(context.getHostUserLogin())); + executor.done(this, context); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineForkRemoteUrlProtocolStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineForkRemoteUrlProtocolStep.java new file mode 100644 index 00000000000..c0563dcf530 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineForkRemoteUrlProtocolStep.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; + +/** + * @author Mihail Kuznyetsov + */ +public class DefineForkRemoteUrlProtocolStep implements Step { + @Override + public void execute(WorkflowExecutor executor, Context context) { + context.setSshAvailable(false); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineWorkBranchStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineWorkBranchStep.java new file mode 100644 index 00000000000..ac44a2c3e64 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DefineWorkBranchStep.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsService; +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsServiceProvider; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.inject.Singleton; + +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.ide.api.notification.NotificationManager; + +import javax.inject.Inject; +import javax.validation.constraints.NotNull; + +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; + +/** + * This step defines the working branch for the user contribution. + * + * @author Kevin Pollet + * @author Yevhenii Voevodin + */ +@Singleton +public class DefineWorkBranchStep implements Step { + + private final NotificationManager notificationManager; + private final VcsServiceProvider vcsServiceProvider; + + @Inject + public DefineWorkBranchStep(final NotificationManager notificationManager, final VcsServiceProvider vcsServiceProvider) { + this.notificationManager = notificationManager; + this.vcsServiceProvider = vcsServiceProvider; + } + + @Override + public void execute(@NotNull final WorkflowExecutor executor, final Context context) { + final VcsService vcsService = vcsServiceProvider.getVcsService(context.getProject()); + + vcsService.getBranchName(context.getProject()) + .then(new Operation() { + @Override + public void apply(String branchName) throws OperationException { + if (context.getContributeToBranchName() == null) { + context.setContributeToBranchName(branchName); + } + context.setWorkBranchName(branchName); + executor.done(DefineWorkBranchStep.this, context); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError err) throws OperationException { + notificationManager.notify(err.getCause().getLocalizedMessage(), FAIL, FLOAT_MODE); + executor.fail(DefineWorkBranchStep.this, context, err.getCause().getLocalizedMessage()); + } + }); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DetectPullRequestStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DetectPullRequestStep.java new file mode 100644 index 00000000000..3d4a1e72621 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DetectPullRequestStep.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.shared.dto.PullRequest; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.ide.api.notification.NotificationManager; +import org.eclipse.che.ide.api.notification.StatusNotification; + +import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.READY_TO_UPDATE_PR; +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; + +/** + * Detects if pull request exists for current working branch, + * stops creation workflow if so and toggles update pull request mode. + * + * @author Yevhenii Voevodin + */ +@Singleton +public class DetectPullRequestStep implements Step { + + private final ContributeMessages messages; + private final NotificationManager notificationManager; + + @Inject + public DetectPullRequestStep(ContributeMessages messages, + NotificationManager manager) { + this.messages = messages; + this.notificationManager = manager; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + context.getVcsHostingService() + .getPullRequest(context.getOriginRepositoryOwner(), + context.getOriginRepositoryName(), + context.getHostUserLogin(), + context.getWorkBranchName()) + .then(new Operation() { + @Override + public void apply(final PullRequest pr) throws OperationException { + notificationManager.notify(messages.stepDetectPrExistsTitle(), + messages.stepDetectPrExistsTitle(context.getWorkBranchName()), + StatusNotification.Status.FAIL, + FLOAT_MODE); + context.setPullRequest(pr); + context.setPullRequestIssueNumber(pr.getNumber()); + context.setForkedRepositoryName(context.getOriginRepositoryName()); + context.setStatus(READY_TO_UPDATE_PR); + executor.fail(DetectPullRequestStep.this, context, messages.stepDetectPrExistsTitle()); + } + }) + .catchError(new Operation() { + @Override + public void apply(final PromiseError error) throws OperationException { + // keep going if pr already exists + executor.done(DetectPullRequestStep.this, context); + } + }); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DetermineUpstreamRepositoryStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DetermineUpstreamRepositoryStep.java new file mode 100644 index 00000000000..fb4bd061425 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/DetermineUpstreamRepositoryStep.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingService; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.shared.dto.Repository; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.ide.api.notification.NotificationManager; + +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; + +/** + * Determines what is the upstream repository and updates the context with + * {@link Context#getUpstreamRepositoryOwner()} () repository owner} and + * {@link Context#getUpstreamRepositoryName()} repository name}. + * + *

The algorithm is simple: if origin repository is user's fork + * then get its parent and set as upstream repo, otherwise use + * {@link Context#getOriginRepositoryOwner() origin owner} and + * {@link Context#getOriginRepositoryName() origin name}. + * + * @author Yevhenii Voevodin + */ +@Singleton +public class DetermineUpstreamRepositoryStep implements Step { + + private final NotificationManager notificationManager; + + @Inject + public DetermineUpstreamRepositoryStep(NotificationManager notificationManager) { + this.notificationManager = notificationManager; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + final VcsHostingService hostingService = context.getVcsHostingService(); + hostingService.getRepository(context.getOriginRepositoryOwner(), context.getOriginRepositoryName()) + .then(new Operation() { + @Override + public void apply(Repository repo) throws OperationException { + if (repo.isFork() && context.getOriginRepositoryOwner().equalsIgnoreCase(context.getHostUserLogin())) { + final String upstreamUrl = repo.getParent().getCloneUrl(); + context.setUpstreamRepositoryName(hostingService.getRepositoryNameFromUrl(upstreamUrl)); + context.setUpstreamRepositoryOwner(hostingService.getRepositoryOwnerFromUrl(upstreamUrl)); + } else { + context.setUpstreamRepositoryName(context.getOriginRepositoryName()); + context.setUpstreamRepositoryOwner(context.getOriginRepositoryOwner()); + } + executor.done(DetermineUpstreamRepositoryStep.this, context); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError error) throws OperationException { + notificationManager.notify(error.getMessage(), FAIL, FLOAT_MODE); + executor.fail(DetermineUpstreamRepositoryStep.this, context, error.getMessage()); + } + }); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/GenerateReviewFactoryStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/GenerateReviewFactoryStep.java new file mode 100644 index 00000000000..b95e5234fe2 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/GenerateReviewFactoryStep.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.utils.FactoryHelper; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; +import com.google.inject.Singleton; + +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.promises.client.Function; +import org.eclipse.che.api.promises.client.FunctionException; +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.factory.FactoryServiceClient; +import org.eclipse.che.ide.api.notification.NotificationManager; + +import javax.inject.Inject; + +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.NOT_EMERGE_MODE; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; + +/** + * Generates a factory for the contribution reviewer. + */ +@Singleton +public class GenerateReviewFactoryStep implements Step { + private final ContributeMessages messages; + private final AppContext appContext; + private final NotificationManager notificationManager; + private final FactoryServiceClient factoryService; + + @Inject + public GenerateReviewFactoryStep(final ContributeMessages messages, + final AppContext appContext, + final NotificationManager notificationManager, + final FactoryServiceClient factoryService) { + this.messages = messages; + this.appContext = appContext; + this.notificationManager = notificationManager; + this.factoryService = factoryService; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + factoryService.getFactoryJson(appContext.getWorkspaceId(), null) + .then(updateProjectAttributes(context)) + .then(new Operation() { + @Override + public void apply(FactoryDto factory) throws OperationException { + factoryService.saveFactory(factory) + .then(new Operation() { + @Override + public void apply(FactoryDto factory) throws OperationException { + context.setReviewFactoryUrl(FactoryHelper.getAcceptFactoryUrl(factory)); + executor.done(GenerateReviewFactoryStep.this, context); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError arg) throws OperationException { + notificationManager.notify(messages.stepGenerateReviewFactoryErrorCreateFactory(), + FAIL, + NOT_EMERGE_MODE); + executor.done(GenerateReviewFactoryStep.this, context); + } + }); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError arg) throws OperationException { + notificationManager.notify(messages.stepGenerateReviewFactoryErrorCreateFactory(), + FAIL, + NOT_EMERGE_MODE); + executor.done(GenerateReviewFactoryStep.this, context); + } + }); + } + + private Function updateProjectAttributes(final Context context) { + return new Function() { + @Override + public FactoryDto apply(FactoryDto factory) throws FunctionException { + final Optional projectOpt = FluentIterable.from(factory.getWorkspace().getProjects()) + .filter(new Predicate() { + @Override + public boolean apply(ProjectConfigDto project) { + return project.getName() + .equals(context.getProject().getName()); + } + }).first(); + if (projectOpt.isPresent()) { + final ProjectConfigDto project = projectOpt.get(); + project.getSource().getParameters().put("branch", context.getWorkBranchName()); + + if (context.isForkAvailable()) { + project.getSource().setLocation(context.getVcsHostingService() + .makeHttpRemoteUrl(context.getHostUserLogin(), context.getOriginRepositoryName())); + } + } + return factory; + } + }; + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/InitializeWorkflowContextStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/InitializeWorkflowContextStep.java new file mode 100644 index 00000000000..9fb0cb86c99 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/InitializeWorkflowContextStep.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsServiceProvider; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingService; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; +import com.google.inject.Singleton; + +import org.eclipse.che.api.core.model.project.ProjectConfig; +import org.eclipse.che.api.git.shared.Remote; +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.ide.api.notification.NotificationManager; + +import javax.inject.Inject; +import java.util.List; +import java.util.Map; + +import static org.eclipse.che.plugin.pullrequest.shared.ContributionProjectTypeConstants.CONTRIBUTE_TO_BRANCH_VARIABLE_NAME; +import static com.google.common.base.Strings.isNullOrEmpty; +import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE; +import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; + +/** + * This step initialize the contribution workflow context. + * + * @author Kevin Pollet + * @author Yevhenii Voevodin + */ +@Singleton +public class InitializeWorkflowContextStep implements Step { + + private static final Predicate ORIGIN_REMOTE_FILTER = new Predicate() { + @Override + public boolean apply(Remote remote) { + return remote.getName().equals("origin"); + } + }; + + private final VcsServiceProvider vcsServiceProvider; + private final NotificationManager notificationManager; + private final ContributeMessages messages; + + @Inject + public InitializeWorkflowContextStep(final VcsServiceProvider vcsServiceProvider, + final NotificationManager notificationManager, + final ContributeMessages messages) { + this.vcsServiceProvider = vcsServiceProvider; + this.notificationManager = notificationManager; + this.messages = messages; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + vcsServiceProvider.getVcsService(context.getProject()) + .listRemotes(context.getProject()) + .then(setUpOriginRepoOp(executor, context)) + .catchError(errorSettingUpOriginRepoOp(executor, context)); + } + + private Operation> setUpOriginRepoOp(final WorkflowExecutor executor, final Context context) { + return new Operation>() { + @Override + public void apply(final List remotes) throws OperationException { + final Optional remoteOpt = FluentIterable.from(remotes) + .filter(ORIGIN_REMOTE_FILTER) + .first(); + if (remoteOpt.isPresent()) { + final Remote remote = remoteOpt.get(); + final String originUrl = remote.getUrl(); + final VcsHostingService vcsHostingService = context.getVcsHostingService(); + + context.setOriginRepositoryOwner(vcsHostingService.getRepositoryOwnerFromUrl(originUrl)); + context.setOriginRepositoryName(vcsHostingService.getRepositoryNameFromUrl(originUrl)); + + setContributeToBranchName(context); + + executor.done(InitializeWorkflowContextStep.this, context); + } else { + notificationManager.notify(messages.stepInitWorkflowOriginRemoteNotFound(), FAIL, FLOAT_MODE); + executor.fail(InitializeWorkflowContextStep.this, context, messages.stepInitWorkflowOriginRemoteNotFound()); + } + } + }; + } + + protected void setContributeToBranchName(Context context) { + String contributeToBranchName = getBranchFromProjectMetadata(context.getProject()); + + if (contributeToBranchName != null) { + context.setContributeToBranchName(contributeToBranchName); + return; + } + + vcsServiceProvider.getVcsService(context.getProject()) + .getBranchName(context.getProject()) + .then( + (String branchName) -> { + context.setContributeToBranchName(branchName); + context.getProject().getSource().getParameters().put("branch", branchName); + }); + } + + private String getBranchFromProjectMetadata(final ProjectConfig project) { + final Map> attrs = project.getAttributes(); + if (attrs.containsKey(CONTRIBUTE_TO_BRANCH_VARIABLE_NAME) && !attrs.get(CONTRIBUTE_TO_BRANCH_VARIABLE_NAME).isEmpty()) { + return attrs.get(CONTRIBUTE_TO_BRANCH_VARIABLE_NAME).get(0); + } + if (project.getSource() != null) { + final String branchName = project.getSource().getParameters().get("branch"); + if (!isNullOrEmpty(branchName)) { + return branchName; + } + } + return null; + } + + private Operation errorSettingUpOriginRepoOp(final WorkflowExecutor executor, final Context context) { + return new Operation() { + @Override + public void apply(final PromiseError error) throws OperationException { + notificationManager.notify(messages.contributorExtensionErrorSetupOriginRepository(error.getMessage()), + FAIL, + FLOAT_MODE); + executor.fail(InitializeWorkflowContextStep.this, context, error.getMessage()); + } + }; + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/IssuePullRequestStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/IssuePullRequestStep.java new file mode 100644 index 00000000000..6706fb9dfd0 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/IssuePullRequestStep.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + + +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoCommitsInPullRequestException; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoHistoryInCommonException; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.PullRequestAlreadyExistsException; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.shared.dto.Configuration; +import org.eclipse.che.plugin.pullrequest.shared.dto.PullRequest; +import com.google.inject.Singleton; + +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; + +import javax.inject.Inject; + +/** + * Create the pull request on the remote VCS repository. + * + * @author Kevin Pollet + */ +@Singleton +public class IssuePullRequestStep implements Step { + private final ContributeMessages messages; + + @Inject + public IssuePullRequestStep(final ContributeMessages messages) { + this.messages = messages; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + final Configuration configuration = context.getConfiguration(); + context.getVcsHostingService() + .createPullRequest(context.getUpstreamRepositoryOwner(), + context.getUpstreamRepositoryName(), + context.getHostUserLogin(), + context.getWorkBranchName(), + context.getContributeToBranchName(), + configuration.getContributionTitle(), + configuration.getContributionComment()) + .then(new Operation() { + @Override + public void apply(PullRequest pullRequest) throws OperationException { + context.setPullRequestIssueNumber(pullRequest.getNumber()); + context.setPullRequest(pullRequest); + executor.done(IssuePullRequestStep.this, context); + } + }) + .catchError(new Operation() { + @Override + public void apply(final PromiseError exception) throws OperationException { + try { + throw exception.getCause(); + } catch (PullRequestAlreadyExistsException | NoHistoryInCommonException ex) { + executor.fail(IssuePullRequestStep.this, + context, + ex.getLocalizedMessage()); + } catch (NoCommitsInPullRequestException noCommitsEx) { + executor.fail(IssuePullRequestStep.this, + context, + messages.stepIssuePullRequestErrorCreatePullRequestWithoutCommits()); + } catch (Throwable thr) { + executor.fail(IssuePullRequestStep.this, + context, + messages.stepIssuePullRequestErrorCreatePullRequest()); + } + } + }); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchOnForkStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchOnForkStep.java new file mode 100644 index 00000000000..d8dff65421c --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchOnForkStep.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.inject.Singleton; + +import javax.inject.Inject; + +/** + * Push the local contribution branch on the user fork. + * + * @author Kevin Pollet + */ +@Singleton +public class PushBranchOnForkStep implements Step { + + private final PushBranchStepFactory pushBranchStepFactory; + + @Inject + public PushBranchOnForkStep(PushBranchStepFactory pushBranchStepFactory) { + this.pushBranchStepFactory = pushBranchStepFactory; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + pushBranchStepFactory.create(this, + context.getHostUserLogin(), + context.getForkedRepositoryName()) + .execute(executor, context); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchOnOriginStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchOnOriginStep.java new file mode 100644 index 00000000000..f63b314c588 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchOnOriginStep.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import com.google.inject.Singleton; + +import javax.inject.Inject; + +/** + * Push the local contribution branch to origin repository + * + * @author Mihail Kuznyetsov + */ +@Singleton +public class PushBranchOnOriginStep implements Step { + + private final static String ORIGIN_REMOTE_NAME = "origin"; + + private final PushBranchStepFactory pushBranchStepFactory; + + @Inject + public PushBranchOnOriginStep(PushBranchStepFactory pushBranchStepFactory) { + this.pushBranchStepFactory = pushBranchStepFactory; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + context.setForkedRemoteName(ORIGIN_REMOTE_NAME); + pushBranchStepFactory.create(this, + context.getOriginRepositoryOwner(), + context.getOriginRepositoryName()) + .execute(executor, context); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchStep.java new file mode 100644 index 00000000000..42e80eda975 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchStep.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.ide.util.loging.Log; +import org.eclipse.che.plugin.pullrequest.client.ContributeMessages; +import org.eclipse.che.plugin.pullrequest.client.vcs.BranchUpToDateException; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoPullRequestException; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.SyntheticStep; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.shared.dto.PullRequest; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +import org.eclipse.che.api.git.shared.PushResponse; +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.api.ssh.shared.dto.SshPairDto; +import org.eclipse.che.ide.api.dialogs.CancelCallback; +import org.eclipse.che.ide.api.dialogs.ConfirmCallback; +import org.eclipse.che.ide.api.dialogs.DialogFactory; +import org.eclipse.che.ide.api.ssh.SshServiceClient; + +/** + * Pushes branch to the repository. + * + * @author Yevhenii Voevodin + */ +public class PushBranchStep implements SyntheticStep { + + private final Step delegate; + private final String repositoryOwner; + private final String repositoryName; + private final ContributeMessages messages; + private final DialogFactory dialogFactory; + private final SshServiceClient sshService; + + @AssistedInject + public PushBranchStep(@Assisted("delegate") Step delegate, + @Assisted("repositoryOwner") String repositoryOwner, + @Assisted("repositoryName") String repositoryName, + ContributeMessages messages, + DialogFactory dialogFactory, + SshServiceClient sshService) { + this.delegate = delegate; + this.repositoryOwner = repositoryOwner; + this.repositoryName = repositoryName; + this.messages = messages; + this.dialogFactory = dialogFactory; + this.sshService = sshService; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + + /* + * Check if a Pull Request with given base and head branches already exists. + * If there is none, push the contribution branch. + * If there is one, propose to updatePullRequest the pull request. + */ + + context.getVcsHostingService() + .getPullRequest(repositoryOwner, + repositoryName, + context.getHostUserLogin(), + context.getWorkBranchName()) + .then(new Operation() { + @Override + public void apply(PullRequest pullRequest) throws OperationException { + context.setPullRequest(pullRequest); + context.getConfiguration().withContributionComment(pullRequest.getDescription()); + final ConfirmCallback okCallback = new ConfirmCallback() { + @Override + public void accepted() { + pushBranch(executor, context); + } + }; + final CancelCallback cancelCallback = new CancelCallback() { + @Override + public void cancelled() { + executor.fail(delegate, context, messages.stepPushBranchCanceling()); + } + }; + + dialogFactory.createConfirmDialog( + messages.contributePartConfigureContributionDialogUpdateTitle(), + messages.contributePartConfigureContributionDialogUpdateText( + pullRequest.getHeadRef()), + okCallback, + cancelCallback).show(); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError err) throws OperationException { + try { + throw err.getCause(); + } catch (NoPullRequestException ex) { + pushBranch(executor, context); + } catch (Throwable thr) { + executor.fail(delegate, context, thr.getMessage()); + } + } + }); + } + + private void pushBranch(final WorkflowExecutor workflow, final Context context) { + context.getVcsService() + .pushBranch(context.getProject(), + context.getForkedRemoteName(), + context.getWorkBranchName()) + .then(new Operation() { + @Override + public void apply(PushResponse result) throws OperationException { + workflow.done(delegate, context); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError err) throws OperationException { + try { + throw err.getCause(); + } catch (BranchUpToDateException branchUpEx) { + workflow.fail(delegate, + context, + messages.stepPushBranchErrorBranchUpToDate()); + } catch (Throwable throwable) { + if (throwable.getMessage().contains("Unable get private ssh key")) { + askGenerateSSH(workflow, context); + } else { + workflow.fail(delegate, + context, + messages.stepPushBranchErrorPushingBranch(throwable.getLocalizedMessage())); + } + } + } + }); + } + + private void askGenerateSSH(final WorkflowExecutor executor, final Context context) { + final ConfirmCallback okCallback = new ConfirmCallback() { + @Override + public void accepted() { + generateSSHAndPushBranch(executor, context, context.getVcsHostingService().getHost()); + } + }; + + final CancelCallback cancelCallback = new CancelCallback() { + @Override + public void cancelled() { + executor.fail(delegate, context, messages.stepPushBranchCanceling()); + } + }; + + dialogFactory.createConfirmDialog(messages.contributePartConfigureContributionDialogSshNotFoundTitle(), + messages.contributePartConfigureContributionDialogSshNotFoundText(), + okCallback, + cancelCallback).show(); + } + + private void generateSSHAndPushBranch(final WorkflowExecutor executor, final Context context, String host) { + sshService.generatePair("vcs", host) + .then(new Operation() { + @Override + public void apply(SshPairDto arg) throws OperationException { + pushBranch(executor, context); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError err) throws OperationException { + executor.fail(delegate, context, err.getMessage()); + } + }); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchStepFactory.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchStepFactory.java new file mode 100644 index 00000000000..446ac8a1bcc --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/PushBranchStepFactory.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import com.google.inject.assistedinject.Assisted; + +public interface PushBranchStepFactory { + + PushBranchStep create(@Assisted("delegate") Step delegate, + @Assisted("repositoryOwner") String repositoryOwner, + @Assisted("repositoryName") String repositoryName); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/UpdatePullRequestStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/UpdatePullRequestStep.java new file mode 100644 index 00000000000..1e51ec28c9a --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/UpdatePullRequestStep.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.utils.FactoryHelper; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.shared.dto.Configuration; +import org.eclipse.che.plugin.pullrequest.shared.dto.PullRequest; +import com.google.common.base.Strings; +import com.google.gwt.regexp.shared.RegExp; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.Promise; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.factory.FactoryServiceClient; + +/** + * Used for toggling last step mark. + * + * @author Yevhenii Voevodin + * @author Anton Korneta + */ +@Singleton +public class UpdatePullRequestStep implements Step { + private static final RegExp SCHEMA = RegExp.compile("[^\\.]*(f\\?id=)([a-zA-Z0-9]*)[^\\.]*"); + + private final FactoryServiceClient factoryService; + private final AppContext appContext; + + @Inject + public UpdatePullRequestStep(FactoryServiceClient factoryService, + AppContext appContext) { + this.factoryService = factoryService; + this.appContext = appContext; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + final PullRequest pullRequest = context.getPullRequest(); + factoryService.getFactoryJson(appContext.getWorkspaceId(), null) + .then(new Operation() { + @Override + public void apply(FactoryDto currentFactory) throws OperationException { + final String factoryId = extractFactoryId(pullRequest.getDescription()); + if (!Strings.isNullOrEmpty(factoryId)) { + updateFactory(executor, context, factoryId, currentFactory); + } else { + addReviewUrl(executor, context, currentFactory); + } + } + }) + .catchError(handleError(executor, context)); + } + + private Promise updateFactory(final WorkflowExecutor executor, + final Context context, + final String factoryId, + final FactoryDto currentFactory) { + return factoryService.updateFactory(factoryId, currentFactory) + .then(new Operation() { + @Override + public void apply(FactoryDto updatedFactory) throws OperationException { + context.setReviewFactoryUrl(FactoryHelper.getAcceptFactoryUrl(updatedFactory)); + executor.done(UpdatePullRequestStep.this, context); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError error) throws OperationException { + createNewFactory(executor, + context, + currentFactory, + new Operation() { + @Override + public void apply(FactoryDto factory) throws OperationException { + final PullRequest pull = context.getPullRequest(); + doUpdate(executor, + context, + pull, + pull.getDescription().replaceAll(factoryId, factory.getId())); + } + }); + } + }); + } + + private void addReviewUrl(final WorkflowExecutor executor, + final Context context, + final FactoryDto currentFactory) { + createNewFactory(executor, + context, + currentFactory, + new Operation() { + @Override + public void apply(FactoryDto factory) throws OperationException { + final Configuration configuration = context.getConfiguration(); + final String reviewUrl = context.getVcsHostingService() + .formatReviewFactoryUrl(FactoryHelper.getAcceptFactoryUrl(factory)); + context.setReviewFactoryUrl(reviewUrl); + final String comment = reviewUrl + "\n" + configuration.getContributionComment(); + configuration.withContributionComment(comment); + doUpdate(executor, context, context.getPullRequest(), comment); + } + }); + } + + + private void createNewFactory(final WorkflowExecutor executor, + final Context context, + final FactoryDto factory, + Operation operation) { + factoryService.saveFactory(factory).then(operation).catchError(handleError(executor, context)); + } + + private void doUpdate(final WorkflowExecutor executor, + final Context context, + final PullRequest pullRequest, + final String comment) { + context.getVcsHostingService() + .updatePullRequest(context.getOriginRepositoryOwner(), + context.getUpstreamRepositoryName(), + pullRequest.withDescription(comment)) + .then(new Operation() { + @Override + public void apply(PullRequest pr) throws OperationException { + executor.done(UpdatePullRequestStep.this, context); + } + }) + .catchError(handleError(executor, context)); + } + + private String extractFactoryId(String description) { + if (SCHEMA.test(description)) { + return SCHEMA.exec(description).getGroup(2); + } + return null; + } + + private Operation handleError(final WorkflowExecutor executor, final Context context) { + return new Operation() { + @Override + public void apply(PromiseError err) throws OperationException { + context.getViewState().setStatusMessage(err.getMessage(), true); + executor.fail(UpdatePullRequestStep.this, context, err.getMessage()); + } + }; + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/WaitForkOnRemoteStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/WaitForkOnRemoteStep.java new file mode 100644 index 00000000000..dd24685c354 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/WaitForkOnRemoteStep.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingServiceProvider; +import org.eclipse.che.plugin.pullrequest.client.workflow.Context; +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; +import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor; +import org.eclipse.che.plugin.pullrequest.shared.dto.Repository; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.PromiseError; + +import javax.validation.constraints.NotNull; + +public class WaitForkOnRemoteStep implements Step { + private static final int POLL_FREQUENCY_MS = 1000; + + private final VcsHostingServiceProvider vcsHostingServiceProvider; + private final Step nextStep; + private Timer timer; + + @AssistedInject + public WaitForkOnRemoteStep(@NotNull final VcsHostingServiceProvider vcsHostingServiceProvider, + @NotNull final @Assisted Step nextStep) { + this.vcsHostingServiceProvider = vcsHostingServiceProvider; + this.nextStep = nextStep; + } + + @Override + public void execute(@NotNull final WorkflowExecutor executor, final Context context) { + if (timer == null) { + timer = new Timer() { + @Override + public void run() { + checkRepository(context, new AsyncCallback() { + @Override + public void onFailure(final Throwable caught) { + timer.schedule(POLL_FREQUENCY_MS); + } + + @Override + public void onSuccess(final Void result) { + executor.done(WaitForkOnRemoteStep.this, context); + } + }); + } + }; + } + + timer.schedule(POLL_FREQUENCY_MS); + } + + private void checkRepository(final Context context, final AsyncCallback callback) { + context.getVcsHostingService().getRepository(context.getHostUserLogin(), context.getForkedRepositoryName()) + .then(new Operation() { + @Override + public void apply(Repository arg) throws OperationException { + callback.onSuccess(null); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError arg) throws OperationException { + callback.onFailure(arg.getCause()); + } + }); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/WaitForkOnRemoteStepFactory.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/WaitForkOnRemoteStepFactory.java new file mode 100644 index 00000000000..4c791831241 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/steps/WaitForkOnRemoteStepFactory.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.steps; + +import org.eclipse.che.plugin.pullrequest.client.workflow.Step; + +/** + * Factory for {@link WaitForkOnRemoteStep}. + */ +public interface WaitForkOnRemoteStepFactory { + WaitForkOnRemoteStep create(Step nextStep); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/utils/FactoryHelper.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/utils/FactoryHelper.java new file mode 100644 index 00000000000..d99ee447f99 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/utils/FactoryHelper.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.utils; + +import org.eclipse.che.api.core.rest.shared.dto.Link; +import org.eclipse.che.api.factory.shared.dto.FactoryDto; + +import javax.validation.constraints.NotNull; + +/** + * Helper providing methods to work with factory. + * + * @author Kevin Pollet + */ +public final class FactoryHelper { + private static final String ACCEPT_FACTORY_LINK_REF = "accept"; + + /** + * Disable instantiation. + */ + private FactoryHelper() { + } + + /** + * Returns the create project relation link for the given factory. + * + * @param factory + * the factory. + * @return the create project url or {@code null} if none. + */ + public static String getAcceptFactoryUrl(@NotNull FactoryDto factory) { + for (final Link oneLink : factory.getLinks()) { + if (ACCEPT_FACTORY_LINK_REF.equals(oneLink.getRel())) { + return oneLink.getHref(); + } + } + return null; + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/BranchUpToDateException.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/BranchUpToDateException.java new file mode 100644 index 00000000000..fa0d6a0dfd0 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/BranchUpToDateException.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs; + +import javax.validation.constraints.NotNull; + +/** + * Exception raised when the branch pushed is up to date. + * + * @author Kevin Pollet + */ +public class BranchUpToDateException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an instance of {@link BranchUpToDateException}. + * + * @param branchName + * the branch name. + */ + public BranchUpToDateException(@NotNull final String branchName) { + super("Branch '" + branchName + "' is up-to-date"); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/GitVcsService.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/GitVcsService.java new file mode 100644 index 00000000000..011bd2d2055 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/GitVcsService.java @@ -0,0 +1,292 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs; + +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.che.api.core.model.project.ProjectConfig; +import org.eclipse.che.api.git.shared.Branch; +import org.eclipse.che.api.git.shared.BranchListMode; +import org.eclipse.che.api.git.shared.CheckoutRequest; +import org.eclipse.che.api.git.shared.PushResponse; +import org.eclipse.che.api.git.shared.Remote; +import org.eclipse.che.api.git.shared.Revision; +import org.eclipse.che.api.git.shared.Status; +import org.eclipse.che.api.promises.client.Function; +import org.eclipse.che.api.promises.client.FunctionException; +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.Promise; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.api.promises.client.js.JsPromiseError; +import org.eclipse.che.api.promises.client.js.Promises; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.api.git.GitServiceClient; +import org.eclipse.che.ide.dto.DtoFactory; +import org.eclipse.che.ide.resource.Path; +import org.eclipse.che.ide.rest.AsyncRequestCallback; +import org.eclipse.che.ide.rest.DtoUnmarshallerFactory; +import org.eclipse.che.ide.rest.Unmarshallable; +import org.eclipse.che.ide.websocket.WebSocketException; +import org.eclipse.che.ide.websocket.rest.RequestCallback; + +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Git backed implementation for {@link VcsService}. + */ +@Singleton +public class GitVcsService implements VcsService { + private static final String BRANCH_UP_TO_DATE_ERROR_MESSAGE = "Everything up-to-date"; + + private final GitServiceClient service; + private final DtoFactory dtoFactory; + private final DtoUnmarshallerFactory dtoUnmarshallerFactory; + private final AppContext appContext; + + @Inject + public GitVcsService(final DtoFactory dtoFactory, + final DtoUnmarshallerFactory dtoUnmarshallerFactory, + final GitServiceClient service, + final AppContext appContext) { + this.dtoFactory = dtoFactory; + this.dtoUnmarshallerFactory = dtoUnmarshallerFactory; + this.service = service; + this.appContext = appContext; + } + + @Override + public void addRemote(@NotNull final ProjectConfig project, @NotNull final String remote, @NotNull final String remoteUrl, + @NotNull final AsyncCallback callback) { + + service.remoteAdd(appContext.getDevMachine(), project, remote, remoteUrl, new AsyncRequestCallback() { + @Override + protected void onSuccess(final String notUsed) { + callback.onSuccess(null); + } + + @Override + protected void onFailure(final Throwable exception) { + callback.onFailure(exception); + } + }); + } + + @Override + public void checkoutBranch(@NotNull final ProjectConfig project, @NotNull final String name, + final boolean createNew, @NotNull final AsyncCallback callback) { + + service.checkout(appContext.getDevMachine(), + project, + dtoFactory.createDto(CheckoutRequest.class) + .withName(name) + .withCreateNew(createNew), + new AsyncRequestCallback() { + @Override + protected void onSuccess(final String branchName) { + callback.onSuccess(branchName); + } + + @Override + protected void onFailure(final Throwable exception) { + callback.onFailure(exception); + } + }); + } + + @Override + public void commit(@NotNull final ProjectConfig project, final boolean includeUntracked, @NotNull final String commitMessage, + @NotNull final AsyncCallback callback) { + try { + + service.add(appContext.getDevMachine(), project, !includeUntracked, null, new RequestCallback() { + @Override + protected void onSuccess(Void aVoid) { + + service.commit(appContext.getDevMachine(), project, commitMessage, true, false, new AsyncRequestCallback() { + @Override + protected void onSuccess(final Revision revision) { + callback.onSuccess(null); + } + + @Override + protected void onFailure(final Throwable exception) { + callback.onFailure(exception); + } + }); + } + + @Override + protected void onFailure(final Throwable exception) { + callback.onFailure(exception); + } + }); + + } catch (final WebSocketException exception) { + callback.onFailure(exception); + } + } + + @Override + public void deleteRemote(@NotNull final ProjectConfig project, @NotNull final String remote, + @NotNull final AsyncCallback callback) { + service.remoteDelete(appContext.getDevMachine(), project, remote, new AsyncRequestCallback() { + @Override + protected void onSuccess(final String notUsed) { + callback.onSuccess(null); + } + + @Override + protected void onFailure(final Throwable exception) { + callback.onFailure(exception); + } + }); + } + + @Override + public Promise getBranchName(ProjectConfig project) { + return service.getStatus(appContext.getDevMachine(), Path.valueOf(project.getPath())) + .then(new Function() { + @Override + public String apply(Status status) throws FunctionException { + return status.getBranchName(); + } + }); + } + + @Override + public void hasUncommittedChanges(@NotNull final ProjectConfig project, @NotNull final AsyncCallback callback) { + service.getStatus(appContext.getDevMachine(), Path.valueOf(project.getPath())) + .then(new Operation() { + @Override + public void apply(Status status) throws OperationException { + callback.onSuccess(!status.isClean()); + } + }) + .catchError(new Operation() { + @Override + public void apply(PromiseError err) throws OperationException { + callback.onFailure(err.getCause()); + } + }); + } + + @Override + public void isLocalBranchWithName(@NotNull final ProjectConfig project, @NotNull final String branchName, + @NotNull final AsyncCallback callback) { + + listLocalBranches(project, new AsyncCallback>() { + @Override + public void onFailure(final Throwable exception) { + callback.onFailure(exception); + } + + @Override + public void onSuccess(final List branches) { + for (final Branch oneBranch : branches) { + if (oneBranch.getDisplayName().equals(branchName)) { + callback.onSuccess(true); + return; + } + } + callback.onSuccess(false); + } + }); + } + + @Override + public void listLocalBranches(@NotNull final ProjectConfig project, @NotNull final AsyncCallback> callback) { + listBranches(project, null, callback); + } + + @Override + public Promise> listRemotes(ProjectConfig project) { + return service.remoteList(appContext.getDevMachine(), project, null, false); + } + + @Override + public Promise pushBranch(final ProjectConfig project, final String remote, final String localBranchName) { + return service.push(appContext.getDevMachine(), project, Collections.singletonList(localBranchName), remote, true) + .catchErrorPromise(new Function>() { + @Override + public Promise apply(PromiseError error) throws FunctionException { + if (BRANCH_UP_TO_DATE_ERROR_MESSAGE.equalsIgnoreCase(error.getMessage())) { + return Promises.reject(JsPromiseError.create(new BranchUpToDateException(localBranchName))); + } else { + return Promises.reject(error); + } + } + }); + } + + /** + * List branches of a given type. + * + * @param project + * the project descriptor. + * @param listMode + * null -> list local branches; "r" -> list remote branches; "a" -> list all branches. + * @param callback + * callback when the operation is done. + */ + private void listBranches(final ProjectConfig project, final BranchListMode listMode, final AsyncCallback> callback) { + final Unmarshallable> unMarshaller = + dtoUnmarshallerFactory.newListUnmarshaller(Branch.class); + service.branchList(appContext.getDevMachine(), project, listMode, + new AsyncRequestCallback>(unMarshaller) { + @Override + protected void onSuccess(final List branches) { + final List result = new ArrayList<>(); + for (final Branch branch : branches) { + result.add(fromGitBranch(branch)); + } + callback.onSuccess(result); + } + + @Override + protected void onFailure(final Throwable exception) { + callback.onFailure(exception); + } + }); + } + + /** + * Converts a git branch DTO to an abstracted {@link org.eclipse.che.api.git.shared.Branch} object. + * + * @param gitBranch + * the object to convert. + * @return the converted object. + */ + private Branch fromGitBranch(final Branch gitBranch) { + final Branch branch = GitVcsService.this.dtoFactory.createDto(Branch.class); + branch.withActive(gitBranch.isActive()).withRemote(gitBranch.isRemote()) + .withName(gitBranch.getName()).withDisplayName(gitBranch.getDisplayName()); + return branch; + } + + /** + * Converts a git remote DTO to an abstracted {@link org.eclipse.che.api.git.shared.Remote} object. + * + * @param gitRemote + * the object to convert. + * @return the converted object. + */ + private Remote fromGitRemote(final Remote gitRemote) { + final Remote remote = GitVcsService.this.dtoFactory.createDto(Remote.class); + remote.withName(gitRemote.getName()).withUrl(gitRemote.getUrl()); + return remote; + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/VcsService.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/VcsService.java new file mode 100644 index 00000000000..92f14bec798 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/VcsService.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs; + +import com.google.gwt.user.client.rpc.AsyncCallback; + +import org.eclipse.che.api.core.model.project.ProjectConfig; +import org.eclipse.che.api.git.shared.Branch; +import org.eclipse.che.api.git.shared.PushResponse; +import org.eclipse.che.api.git.shared.Remote; +import org.eclipse.che.api.promises.client.Promise; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * Service for VCS operations. + */ +public interface VcsService { + + /** + * Add a remote to the project VCS metadata. + * + * @param project + * the project descriptor. + * @param remote + * the remote name. + * @param remoteUrl + * the remote URL. + * @param callback + * callback when the operation is done. + */ + void addRemote(@NotNull ProjectConfig project, @NotNull String remote, @NotNull String remoteUrl, + @NotNull AsyncCallback callback); + + /** + * Checkout a branch of the given project. + * + * @param project + * the project descriptor. + * @param branchName + * the name of the branch to checkout. + * @param createNew + * create a new branch if {@code true}. + * @param callback + * callback when the operation is done. + */ + void checkoutBranch(@NotNull ProjectConfig project, @NotNull String branchName, boolean createNew, + @NotNull AsyncCallback callback); + + /** + * Commits the current changes of the given project. + * + * @param project + * the project descriptor. + * @param includeUntracked + * {@code true} to include untracked files, {@code false} otherwise. + * @param commitMessage + * the commit message. + * @param callback + * callback when the operation is done. + */ + void commit(@NotNull ProjectConfig project, boolean includeUntracked, @NotNull String commitMessage, + @NotNull AsyncCallback callback); + + /** + * Removes a remote to the project VCS metadata. + * + * @param project + * the project descriptor. + * @param remote + * the remote name. + * @param callback + * callback when the operation is done. + */ + void deleteRemote(@NotNull ProjectConfig project, @NotNull String remote, @NotNull AsyncCallback callback); + + /** + * Returns the name of the current branch for the given {@code project}. + * + * @param project + * the project. + * @return the promise that resolves branch name or rejects with an error + */ + Promise getBranchName(ProjectConfig project); + + /** + * Returns if the given project has uncommitted changes. + * + * @param project + * the project descriptor. + * @param callback + * what to do if the project has uncommitted changes. + */ + void hasUncommittedChanges(@NotNull ProjectConfig project, @NotNull AsyncCallback callback); + + /** + * Returns if a local branch with the given name exists in the given project. + * + * @param project + * the project descriptor. + * @param branchName + * the branch name. + * @param callback + * callback called when operation is done. + */ + void isLocalBranchWithName(@NotNull ProjectConfig project, @NotNull String branchName, @NotNull AsyncCallback callback); + + /** + * List the local branches. + * + * @param project + * the project descriptor. + * @param callback + * what to do with the branches list. + */ + void listLocalBranches(@NotNull ProjectConfig project, @NotNull AsyncCallback> callback); + + /** + * Returns the list of the remotes for given {@code project}. + * + * @param project + * the project + * @return the promise which resolves {@literal List} or rejects with an error + */ + Promise> listRemotes(ProjectConfig project); + + /** + * Push a local branch to remote. + * + * @param project + * the project descriptor. + * @param remote + * the remote name + * @param localBranchName + * the local branch name + */ + Promise pushBranch(ProjectConfig project, + String remote, + String localBranchName); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/VcsServiceProvider.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/VcsServiceProvider.java new file mode 100644 index 00000000000..8f133871a1c --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/VcsServiceProvider.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs; + +import org.eclipse.che.api.core.model.project.ProjectConfig; + +import javax.inject.Inject; +import javax.validation.constraints.NotNull; + +import static org.eclipse.che.ide.ext.git.client.GitUtil.isUnderGit; + +/** + * Provider for the {@link VcsService}. + * + * @author Kevin Pollet + */ +public class VcsServiceProvider { + private final GitVcsService gitVcsService; + + @Inject + public VcsServiceProvider(@NotNull final GitVcsService gitVcsService) { + this.gitVcsService = gitVcsService; + } + + /** + * Returns the {@link VcsService} implementation corresponding to the current project VCS. + * + * @return the {@link VcsService} implementation or {@code null} if not supported or not + * initialized. + */ + public VcsService getVcsService(final ProjectConfig project) { + if (project != null) { + if (isUnderGit(project)) { + return gitVcsService; + } + } + return null; + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/HostingServiceTemplates.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/HostingServiceTemplates.java new file mode 100644 index 00000000000..8dcc5c2e156 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/HostingServiceTemplates.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs.hosting; + +import com.google.gwt.i18n.client.Messages; + +/** + * Hosting service templates. + * + * @author Kevin Pollet + */ +public interface HostingServiceTemplates extends Messages { + /** + * The SSH URL to a repository. + * + * @param username + * the user name. + * @param repository + * the repository name. + * @return the URL + */ + String sshUrlTemplate(String username, String repository); + + /** + * The HTTP URL to a repository. + * + * @param username + * the user name. + * @param repository + * the repository name. + * @return the URL + */ + String httpUrlTemplate(String username, String repository); + + /** + * The URL to a pull request. + * + * @param username + * the user name. + * @param repository + * the repository name. + * @param pullRequestNumber + * the pull request number. + * @return the URL + */ + String pullRequestUrlTemplate(String username, String repository, String pullRequestNumber); + + /** + * The formatted version of the review factory url using the Hosting service markup language. + * + * @param protocol + * the protocol used http or https + * @param host + * the host. + * @param reviewFactoryUrl + * the review factory url. + * @return the formatted version of the review factory url + */ + String formattedReviewFactoryUrlTemplate(String protocol, String host, String reviewFactoryUrl); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoCommitsInPullRequestException.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoCommitsInPullRequestException.java new file mode 100644 index 00000000000..20edbe12d59 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoCommitsInPullRequestException.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs.hosting; + +import javax.validation.constraints.NotNull; + +/** + * Exception raised when a pull request is created with no commits. + * + * @author Kevin Pollet + */ +public class NoCommitsInPullRequestException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an instance of {@link NoCommitsInPullRequestException}. + * + * @param headBranch + * the head branch name. + * @param baseBranch + * the base branch name. + */ + public NoCommitsInPullRequestException(@NotNull final String headBranch, @NotNull final String baseBranch) { + super("No commits between " + baseBranch + " and " + headBranch); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoHistoryInCommonException.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoHistoryInCommonException.java new file mode 100644 index 00000000000..a1d06671276 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoHistoryInCommonException.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs.hosting; + +/** + * This exception should be thrown when separate branches have no commits in common. + * + * @author Anton Korneta + */ +public class NoHistoryInCommonException extends Exception { + + public NoHistoryInCommonException(String msg) { + super(msg); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoPullRequestException.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoPullRequestException.java new file mode 100644 index 00000000000..f8697a5280b --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoPullRequestException.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs.hosting; + +import javax.validation.constraints.NotNull; + +public class NoPullRequestException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an instance of {@link NoPullRequestException}. + * + * @param branchName + * the branch name. + */ + public NoPullRequestException(@NotNull final String branchName) { + super("No Pull Request for branch " + branchName); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoUserForkException.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoUserForkException.java new file mode 100644 index 00000000000..3cd476e0286 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoUserForkException.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs.hosting; + +import javax.validation.constraints.NotNull; + +/** + * Exception raised when trying to get a fork of a repository for a user and no fork being found. + */ +public class NoUserForkException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an instance of {@link NoUserForkException}. + * + * @param user + * the user. + */ + public NoUserForkException(@NotNull final String user) { + super("No fork for user: " + user); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoVcsHostingServiceImplementationException.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoVcsHostingServiceImplementationException.java new file mode 100644 index 00000000000..cad7855f64d --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/NoVcsHostingServiceImplementationException.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs.hosting; + +/** + * Exception raised when there is no {@link VcsHostingService} implementation for the + * current project. + * + * @author Kevin Pollet + */ +public class NoVcsHostingServiceImplementationException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an instance of {@link NoVcsHostingServiceImplementationException}. + */ + public NoVcsHostingServiceImplementationException() { + super("No implementation of the VcsHostingService for the current project"); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/PullRequestAlreadyExistsException.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/PullRequestAlreadyExistsException.java new file mode 100644 index 00000000000..4616c6cb91a --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/PullRequestAlreadyExistsException.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs.hosting; + +import javax.validation.constraints.NotNull; + +/** + * Exception raised when a pull request already exists for a branch. + * + * @author Kevin Pollet + */ +public class PullRequestAlreadyExistsException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an instance of {@link PullRequestAlreadyExistsException}. + * + * @param headBranch + * the head branch name. + */ + public PullRequestAlreadyExistsException(@NotNull final String headBranch) { + super("A pull request for " + headBranch + " already exists"); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/ServiceUtil.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/ServiceUtil.java new file mode 100644 index 00000000000..c68b1c6bf5e --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/ServiceUtil.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs.hosting; + +import org.eclipse.che.plugin.pullrequest.shared.dto.HostUser; + +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.Promise; +import org.eclipse.che.api.promises.client.PromiseError; +import org.eclipse.che.api.promises.client.js.Executor; +import org.eclipse.che.api.promises.client.js.Promises; +import org.eclipse.che.api.promises.client.js.RejectFunction; +import org.eclipse.che.api.promises.client.js.ResolveFunction; +import org.eclipse.che.security.oauth.JsOAuthWindow; +import org.eclipse.che.security.oauth.OAuthCallback; +import org.eclipse.che.security.oauth.OAuthStatus; + +/** + * Utils for {@link VcsHostingService} implementations. + * + * @author Yevhenii Voevodin + */ +public final class ServiceUtil { + + /** + * Performs {@link JsOAuthWindow} authentication and tries to get current user. + * + * @param service + * hosting service, used to authorized user + * @param authUrl + * url to perform authentication + * @return the promise which resolves authorized user or rejects with an error + */ + public static Promise performWindowAuth(final VcsHostingService service, final String authUrl) { + final Executor.ExecutorBody exBody = new Executor.ExecutorBody() { + @Override + public void apply(final ResolveFunction resolve, final RejectFunction reject) { + new JsOAuthWindow(authUrl, "error.url", 500, 980, new OAuthCallback() { + @Override + public void onAuthenticated(final OAuthStatus authStatus) { + // maybe it's possible to avoid this request if authStatus contains the vcs host user. + service.getUserInfo().then(new Operation() { + @Override + public void apply(HostUser user) throws OperationException { + resolve.apply(user); + } + }).catchError(new Operation() { + @Override + public void apply(PromiseError error) throws OperationException { + reject.apply(error); + } + }); + } + }).loginWithOAuth(); + } + }; + return Promises.create(Executor.create(exBody)); + } + + private ServiceUtil() {} +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/VcsHostingService.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/VcsHostingService.java new file mode 100644 index 00000000000..7abd401cad0 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/VcsHostingService.java @@ -0,0 +1,228 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs.hosting; + +import org.eclipse.che.plugin.pullrequest.shared.dto.HostUser; +import org.eclipse.che.plugin.pullrequest.shared.dto.PullRequest; +import org.eclipse.che.plugin.pullrequest.shared.dto.Repository; + +import org.eclipse.che.api.promises.client.Promise; +import org.eclipse.che.ide.api.app.CurrentUser; + +import javax.validation.constraints.NotNull; + +/** + * Represents a repository host + * + * @author Kevin Pollet + */ +public interface VcsHostingService { + + /** + * Initializes new implementation if additional data from remote url is required + * + * @param remoteUrl + * @return + */ + VcsHostingService init(String remoteUrl); + + /** + * Returns the VCS Host name. + * + * @return the VCS Host name never {@code null}. + */ + @NotNull + String getName(); + + /** + * Returns the VCS Host. + * + * @return the VCS Host never {@code null}. + */ + @NotNull + String getHost(); + + /** + * Checks if the given remote URL is hosted by this service. + * + * @param remoteUrl + * the remote url to check. + * @return {@code true} if the given remote url is hosted by this service, {@code false} otherwise. + */ + boolean isHostRemoteUrl(@NotNull String remoteUrl); + + /** + * Get a pull request by qualified name. + * + * @param owner + * the repository owner. + * @param repository + * the repository name. + * @param username + * the user name. + * @param branchName + * pull request branch name. + */ + Promise getPullRequest(@NotNull String owner, + @NotNull String repository, + @NotNull String username, + @NotNull String branchName); + + /** + * Creates a pull request. + * + * @param owner + * the repository owner. + * @param repository + * the repository name. + * @param username + * the user name. + * @param headBranchName + * the head branch name. + * @param baseBranchName + * the base branch name. + * @param title + * the pull request title. + * @param body + * the pull request body. + */ + Promise createPullRequest(String owner, + String repository, + String username, + String headBranchName, + String baseBranchName, + String title, + String body); + + /** + * Forks the given repository for the current user. + * + * @param owner + * the repository owner. + * @param repository + * the repository name. + */ + Promise fork(String owner, String repository); + + /** + * Returns the promise which either resolves repository or rejects with an error. + * + * @param owner + * the owner of the repositoryName + * @param repositoryName + * the name of the repository + */ + Promise getRepository(String owner, String repositoryName); + + /** + * Returns the repository name from the given url. + * + * @param url + * the url. + * @return the repository name, never {@code null}. + */ + @NotNull + String getRepositoryNameFromUrl(@NotNull String url); + + /** + * Returns the repository owner from the given url. + * + * @param url + * the url. + * @return the repository owner, never {@code null}. + */ + @NotNull + String getRepositoryOwnerFromUrl(@NotNull String url); + + /** + * Returns the repository fork of the given user. + * + * @param user + * the user. + * @param owner + * the repository owner. + * @param repository + * the repository name. + */ + Promise getUserFork(String user, String owner, String repository); + + /** + * Returns the user information on the repository host. + */ + Promise getUserInfo(); + + /** + * Makes the remote SSH url for the given username and repository. + * + * @param username + * the user name. + * @param repository + * the repository name. + * @return the remote url. + */ + String makeSSHRemoteUrl(@NotNull String username, @NotNull String repository); + + /** + * Makes the remote HTTP url for the given username and repository. + * + * @param username + * the user name. + * @param repository + * the repository name. + * @return the remote url. + */ + String makeHttpRemoteUrl(@NotNull String username, @NotNull String repository); + + /** + * Makes the pull request url for the given username, repository and pull request number. + * + * @param username + * the user name. + * @param repository + * the repository name. + * @param pullRequestNumber + * the pull request number. + * @return the remote url. + */ + String makePullRequestUrl(@NotNull String username, @NotNull String repository, @NotNull String pullRequestNumber); + + /** + * Use the VCS hosting comment markup language to format the review factory URL. + * + * @param reviewFactoryUrl + * the review factory URL to format. + * @return the formatted review factory URL. + */ + @NotNull + String formatReviewFactoryUrl(@NotNull String reviewFactoryUrl); + + /** + * Authenticates the current user on the hosting service. + * + * @param user + * the user to authenticate + * @return the promise which resolves host user or rejects with an error + */ + Promise authenticate(CurrentUser user); + + /** + * Update pull request information e.g. title, description + * + * @param owner + * repository owner + * @param repository + * name of repository + * @param pullRequest + * pull request for update + * @return updated pull request + */ + Promise updatePullRequest(String owner, String repository, PullRequest pullRequest); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/VcsHostingServiceProvider.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/VcsHostingServiceProvider.java new file mode 100644 index 00000000000..36a3a18cb54 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/vcs/hosting/VcsHostingServiceProvider.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.vcs.hosting; + +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsService; +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsServiceProvider; +import com.google.inject.Singleton; + +import org.eclipse.che.api.core.model.project.ProjectConfig; +import org.eclipse.che.api.git.shared.Remote; +import org.eclipse.che.api.promises.client.Function; +import org.eclipse.che.api.promises.client.FunctionException; +import org.eclipse.che.api.promises.client.Promise; +import org.eclipse.che.api.promises.client.js.JsPromiseError; +import org.eclipse.che.api.promises.client.js.Promises; + +import javax.inject.Inject; +import java.util.List; +import java.util.Set; + +/** + * Provider for the {@link VcsHostingService}. + * + * @author Kevin Pollet + * @author Yevhenii Voevodin + */ +@Singleton +public class VcsHostingServiceProvider { + private static final String ORIGIN_REMOTE_NAME = "origin"; + + private final VcsServiceProvider vcsServiceProvider; + private final Set vcsHostingServices; + + @Inject + public VcsHostingServiceProvider(final VcsServiceProvider vcsServiceProvider, + final Set vcsHostingServices) { + this.vcsServiceProvider = vcsServiceProvider; + this.vcsHostingServices = vcsHostingServices; + } + + /** + * Returns the dedicated {@link VcsHostingService} implementation for the {@link #ORIGIN_REMOTE_NAME origin} remote. + * + * @param project + * project used to find origin remote and extract VCS hosting service + */ + public Promise getVcsHostingService(final ProjectConfig project) { + if (project == null) { + return Promises.reject(JsPromiseError.create(new NoVcsHostingServiceImplementationException())); + } + final VcsService vcsService = vcsServiceProvider.getVcsService(project); + if (vcsService == null) { + return Promises.reject(JsPromiseError.create(new NoVcsHostingServiceImplementationException())); + } + return vcsService.listRemotes(project) + .then(new Function, VcsHostingService>() { + @Override + public VcsHostingService apply(List remotes) throws FunctionException { + for (Remote remote : remotes) { + if (ORIGIN_REMOTE_NAME.equals(remote.getName())) { + for (final VcsHostingService hostingService : vcsHostingServices) { + if (hostingService.isHostRemoteUrl(remote.getUrl())) { + return hostingService.init(remote.getUrl()); + } + } + } + } + throw new FunctionException(new NoVcsHostingServiceImplementationException()); + } + }); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/ChainExecutor.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/ChainExecutor.java new file mode 100644 index 00000000000..5b9ce523700 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/ChainExecutor.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.workflow; + +import com.google.common.base.Optional; + +import org.eclipse.che.ide.util.loging.Log; + +import java.util.Iterator; + +import static com.google.common.base.Optional.fromNullable; +import static java.util.Objects.requireNonNull; + +/** + * Executor for the {@link StepsChain}. + * If the chain is modified after executor is created (e.g. new step added to the chain) + * executor state won't be affected and newly added steps will be ignored by executor. + * + * @author Yevhenii Voevodin + */ +public final class ChainExecutor { + + private final Iterator chainIt; + + private Step currentStep; + + public ChainExecutor(final StepsChain chain) { + chainIt = requireNonNull(chain, "Expected non-null steps chain").getSteps().iterator(); + } + + /** + * Executes the next chain step, does nothing - if there are no steps left . + * + * @param workflow + * the contribution workflow + * @param context + * the context for current chain execution + */ + public void execute(final WorkflowExecutor workflow, final Context context) { + if (chainIt.hasNext()) { + currentStep = chainIt.next(); + Log.info(getClass(), "Executing :: " + context.getProject().getName() + " :: => " + currentStep.getClass()); + currentStep.execute(workflow, context); + } + } + + /** + * Returns an empty optional when current step is null, otherwise + * returns the optional which contains current step value. + */ + public Optional getCurrentStep() { + return fromNullable(currentStep); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/Context.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/Context.java new file mode 100644 index 00000000000..c3909ad2a61 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/Context.java @@ -0,0 +1,443 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.workflow; + +import org.eclipse.che.plugin.pullrequest.client.events.ContextPropertyChangeEvent; +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsService; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingService; +import org.eclipse.che.plugin.pullrequest.shared.dto.Configuration; +import org.eclipse.che.plugin.pullrequest.shared.dto.PullRequest; +import com.google.web.bindery.event.shared.EventBus; + +import org.eclipse.che.api.core.model.project.ProjectConfig; +import org.eclipse.che.commons.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.eclipse.che.plugin.pullrequest.client.events.ContextPropertyChangeEvent.ContextProperty.CONTRIBUTE_TO_BRANCH_NAME; +import static org.eclipse.che.plugin.pullrequest.client.events.ContextPropertyChangeEvent.ContextProperty.PROJECT; +import static org.eclipse.che.plugin.pullrequest.client.events.ContextPropertyChangeEvent.ContextProperty.WORK_BRANCH_NAME; + +/** + * Context used to share information between the steps in the contribution workflow. + * + * @author Kevin Pollet + */ +public class Context { + /** The event bus. */ + private final EventBus eventBus; + + /** The project. */ + private ProjectConfig project; + + /** The name of the branch to contribute to. */ + private String contributeToBranchName; + + /** The name of the working branch. */ + private String workBranchName; + + /** The name of the user on host VCS. */ + private String hostUserLogin; + + /** The name of the owner of the repository forked on VCS. */ + private String upstreamRepositoryOwner; + + /** The name of the repository forked on VCS. */ + private String upstreamRepositoryName; + + /** The name of the owner of the repository cloned on VCS. */ + private String originRepositoryOwner; + + /** The name of the repository cloned on VCS. */ + private String originRepositoryName; + + /** The identifier of the pull request on the hosting service. */ + private PullRequest pullRequest; + + /** The issue number of the pull request issued for the contribution. */ + private String pullRequestIssueNumber; + + /** The generated review factory URL. */ + private String reviewFactoryUrl; + + /** The name of the forked remote. */ + private String forkedRemoteName; + + /** The name of the forked repository. */ + private String forkedRepositoryName; + + /** Defines availability of fork creation. */ + private boolean forkAvailable; + + /** Defines ability to use ssh URLs. */ + private boolean sshAvailable; + + private VcsHostingService vcsHostingService; + + /** The name of the origin remote. */ + private String originRemoteName; + private WorkflowStatus status; + private WorkflowStatus previousStatus; + private Configuration configuration; + public ViewState viewState; + private VcsService vcsService; + + public Context(final EventBus eventBus) { + this.eventBus = eventBus; + viewState = new ViewState(); + } + + public ProjectConfig getProject() { + return project; + } + + public void setProject(final ProjectConfig project) { + final ProjectConfig oldValue = this.project; + this.project = project; + + fireContextPropertyChange(PROJECT, oldValue, project); + } + + public String getContributeToBranchName() { + return contributeToBranchName; + } + + public void setContributeToBranchName(final String contributeToBranchName) { + final String oldValue = this.contributeToBranchName; + this.contributeToBranchName = contributeToBranchName; + + fireContextPropertyChange(CONTRIBUTE_TO_BRANCH_NAME, oldValue, contributeToBranchName); + } + + public String getWorkBranchName() { + return workBranchName; + } + + public void setWorkBranchName(final String workBranchName) { + final String oldValue = this.workBranchName; + this.workBranchName = workBranchName; + + fireContextPropertyChange(WORK_BRANCH_NAME, oldValue, workBranchName); + } + + public String getHostUserLogin() { + return hostUserLogin; + } + + public void setHostUserLogin(final String hostUserLogin) { + this.hostUserLogin = hostUserLogin; + } + + public String getUpstreamRepositoryOwner() { + return upstreamRepositoryOwner; + } + + public void setUpstreamRepositoryOwner(String upstreamRepositoryOwner) { + this.upstreamRepositoryOwner = upstreamRepositoryOwner; + } + + public String getUpstreamRepositoryName() { + return upstreamRepositoryName; + } + + public void setUpstreamRepositoryName(String upstreamRepositoryName) { + this.upstreamRepositoryName = upstreamRepositoryName; + } + + public String getOriginRepositoryOwner() { + return originRepositoryOwner; + } + + public void setOriginRepositoryOwner(final String originRepositoryOwner) { + final String oldValue = this.originRepositoryOwner; + this.originRepositoryOwner = originRepositoryOwner; + + fireContextPropertyChange(ContextPropertyChangeEvent.ContextProperty.ORIGIN_REPOSITORY_OWNER, oldValue, originRepositoryOwner); + } + + public String getOriginRepositoryName() { + return originRepositoryName; + } + + public void setOriginRepositoryName(final String originRepositoryName) { + final String oldValue = this.originRepositoryName; + this.originRepositoryName = originRepositoryName; + + fireContextPropertyChange(ContextPropertyChangeEvent.ContextProperty.ORIGIN_REPOSITORY_NAME, oldValue, originRepositoryName); + } + + public PullRequest getPullRequest() { + return pullRequest; + } + + public void setPullRequest(PullRequest pullRequest) { + this.pullRequest = pullRequest; + } + + /** + * Return the issue number of the pull request issued for this contribution. + * + * @return the pull request issue id + */ + public String getPullRequestIssueNumber() { + return pullRequestIssueNumber; + } + + /** + * Sets the issue number of the pull request issued for this contribution. + * + * @param pullRequestIssueNumber + * the new value + */ + public void setPullRequestIssueNumber(final String pullRequestIssueNumber) { + this.pullRequestIssueNumber = pullRequestIssueNumber; + } + + /** + * Returns the generated review factory URL (if available). + * + * @return factory URL + */ + public String getReviewFactoryUrl() { + return this.reviewFactoryUrl; + } + + /** + * Sets the generated review factory URL (if available). + * + * @param reviewFactoryUrl + * new value + */ + public void setReviewFactoryUrl(final String reviewFactoryUrl) { + this.reviewFactoryUrl = reviewFactoryUrl; + } + + public String getOriginRemoteName() { + return originRemoteName; + } + + public void setOriginRemoteName(String originRemoteName) { + this.originRemoteName = originRemoteName; + } + + public String getForkedRemoteName() { + return forkedRemoteName; + } + + public void setForkedRemoteName(String forkedRemoteName) { + this.forkedRemoteName = forkedRemoteName; + } + + public String getForkedRepositoryName() { + return forkedRepositoryName; + } + + public void setForkedRepositoryName(String forkedRepositoryName) { + this.forkedRepositoryName = forkedRepositoryName; + } + + private void fireContextPropertyChange(final ContextPropertyChangeEvent.ContextProperty contextProperty, + final Object oldValue, + final Object newValue) { + if (!Objects.equals(oldValue, newValue)) { + eventBus.fireEvent(new ContextPropertyChangeEvent(this, contextProperty)); + } + } + + public boolean isUpdateMode() { + return status == WorkflowStatus.UPDATING_PR || status == WorkflowStatus.READY_TO_UPDATE_PR; + } + + public VcsHostingService getVcsHostingService() { + return vcsHostingService; + } + + public void setVcsHostingService(VcsHostingService service) { + this.vcsHostingService = service; + } + + @Nullable + public WorkflowStatus getStatus() { + return status; + } + + @Nullable + public WorkflowStatus getPreviousStatus() { + return previousStatus; + } + + public void setStatus(@Nullable WorkflowStatus status) { + this.previousStatus = this.status; + this.status = status; + } + + public Configuration getConfiguration() { + return configuration; + } + + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } + + public ViewState getViewState() { + return viewState; + } + + public void setVcsService(VcsService vcsService) { + this.vcsService = vcsService; + } + + public VcsService getVcsService() { + return vcsService; + } + + public boolean isForkAvailable() { + return forkAvailable; + } + public void setForkAvailable(boolean forkAvailable) { + this.forkAvailable = forkAvailable; + } + + public boolean isSshAvailable() { + return sshAvailable; + } + + public void setSshAvailable(boolean sshAvailable) { + this.sshAvailable = sshAvailable; + } + + public static final class ViewState { + + private String contributionTitle; + private String contributionComment; + private StatusMessage statusMessage; + private List stages; + private int currentStage; + + private ViewState() { + currentStage = 0; + } + + public void setStatusMessage(String message, boolean error) { + this.statusMessage = new StatusMessage(message, error); + } + + public void setStatusMessage(StatusMessage message) { + this.statusMessage = message; + } + + public void setContributionTitle(String title) { + this.contributionTitle = title; + } + + public String getContributionTitle() { + return contributionTitle; + } + + public void setContributionComment(String contributionComment) { + this.contributionComment = contributionComment; + } + + public String getContributionComment() { + return contributionComment; + } + + public List getStages() { + if (stages == null) { + stages = new ArrayList<>(3); + } + return stages; + } + + public List getStageNames() { + final List statusNames = new ArrayList<>(getStages().size()); + for (Stage stepStatus : getStages()) { + statusNames.add(stepStatus.getName()); + } + return statusNames; + } + + public List getStageValues() { + final List statusNames = new ArrayList<>(getStages().size()); + for (Stage stepStatus : getStages()) { + statusNames.add(stepStatus.getStatus()); + } + return statusNames; + } + + public void resetStages() { + getStages().clear(); + currentStage = 0; + } + + public void setStep(String name, Boolean value) { + getStages().add(new Stage(name, value)); + } + + public void setStages(List stages) { + resetStages(); + for (String newStage : stages) { + setStep(newStage, null); + } + } + + public StatusMessage getStatusMessage() { + return statusMessage; + } + + public void setStageDone(boolean stepDone) { + getStages().get(currentStage++).setStatus(stepDone); + } + + public static class StatusMessage { + private final boolean error; + private final String message; + + private StatusMessage(String message, boolean error) { + this.message = message; + this.error = error; + } + + public boolean isError() { + return error; + } + + public String getMessage() { + return message; + } + } + + public static class Stage { + private final String name; + + private Boolean status; + + public Stage(String name, Boolean status) { + this.status = status; + this.name = name; + } + + public String getName() { + return name; + } + + public Boolean getStatus() { + return status; + } + + public void setStatus(Boolean status) { + this.status = status; + } + } + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/ContributionWorkflow.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/ContributionWorkflow.java new file mode 100644 index 00000000000..aa0280de7b0 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/ContributionWorkflow.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.workflow; + +/** + * Defines contribution workflow. + * + *

The contribution workflow consists of 3 main steps: + * initialization, pull request creation, pull request update. + * According to these 3 steps implementation should provide steps chains + * based on specific VCS Hosting service. + * + *

Binding example: + *

{@code
+ *     final GinMapBinder strategyBinder
+ *                 = GinMapBinder.newMapBinder(binder(), String.class, ContributionWorkflow.class);
+ *     strategyBinder.addBinding("my-vcs-service").to(MyVcsServiceContributionWorkflow.class);
+ * }
+ * + * @author Yevhenii Voevodin + */ +public interface ContributionWorkflow { + + /** Returns the steps chain which should be executed when plugin initializes. */ + StepsChain initChain(Context context); + + /** Returns the steps chain which should be executed when pull request should be created. */ + StepsChain creationChain(Context context); + + /** Returns the steps chain which should be executed for the pull request updatePullRequest. */ + StepsChain updateChain(Context context); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/Step.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/Step.java new file mode 100644 index 00000000000..ffc6f5f9bb5 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/Step.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.workflow; + +import org.eclipse.che.plugin.pullrequest.client.events.StepEvent; +import org.eclipse.che.plugin.pullrequest.client.steps.PushBranchStep; + +/** + * Contract for a step in the contribution workflow. + * + *

Step should not depend on another steps as it is + * a workflow part but workflow is defined by {@link ContributionWorkflow} implementations. + * Each step should end its execution with either {@link WorkflowExecutor#done(Step, Context)} + * or {@link WorkflowExecutor#fail(Step, Context, String)} method. + * + *

{@link WorkflowExecutor} fires {@link StepEvent} for each + * done/fail step if this step is not {@link SyntheticStep}. + * + *

If step contains common logic for several steps + * then this logic should be either extracted to the other + * step or used along with factory (e.g. {@link PushBranchStep}). + * + * @author Yevhenii Voevodin + */ +public interface Step { + + /** + * Executes this step. + * + * @param executor + * contribution workflow executor + */ + void execute(final WorkflowExecutor executor, final Context context); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/StepsChain.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/StepsChain.java new file mode 100644 index 00000000000..5d50e7c4aa2 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/StepsChain.java @@ -0,0 +1,219 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.workflow; + +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.List; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static java.util.Objects.requireNonNull; + +/** + * Defines a chain of steps. + * + * @author Yevhenii Voevodin + * @see ChainExecutor + * @see ContributionWorkflow + */ +public final class StepsChain { + + /** + * Creates a new {@code StepsChain} with the initial step. + * + * @param firstStep + * the first step in the chain + * @return new chain instance + * @throws NullPointerException + * when {@code firstStep} is null + */ + public static StepsChain first(Step firstStep) { + return new StepsChain().then(firstStep); + } + + private final List steps; + + private StepsChain() { + steps = new ArrayList<>(); + } + + /** + * Returns the list of the steps currently added to this chain. + * The list is unmodifiable copy of added steps, so next chain modification + * won't affect returned list. + */ + public List getSteps() { + return ImmutableList.copyOf(steps); + } + + /** + * Adds the step to the chain. + * + * @param step + * the next chain step + * @throws NullPointerException + * when {@code step} is null + */ + public StepsChain then(Step step) { + steps.add(step); + return this; + } + + /** + * Adds {@code stepIfTrue} to the chain, and executes added step only + * if supplier supplies true. + * + * @param supplier + * supplier of a boolean value + * @param stepIfTrue + * step which should be added to the chain if expression is true + * @return this chain instance + * @throws NullPointerException + * when {@code stepIfTrue} is null + */ + public StepsChain thenIf(Supplier supplier, Step stepIfTrue) { + return then(new ExpressionStep(supplier, stepIfTrue)); + } + + /** + * Adds all the steps from the {@code chain} to this chain. + * + * @param chain + * chain which steps should be added to this chain + * @return this chain instance + * @throws NullPointerException + * when {@code chain} is null + */ + public StepsChain thenChain(StepsChain chain) { + steps.addAll(requireNonNull(chain, "Expected non-null chain").getSteps()); + return this; + } + + /** + * Adds all the steps from the {@code chainIfTrue} chain to + * this chain with a boolean supplier. Each added step will be executed only if + * supplier provides true value. + * + * @param supplier + * any boolean value or expression + * @param chainIfTrue + * chain which steps should be added to this chain if expression is true + * @return this chain instance + * @throws NullPointerException + * when {@code chainIfTrue} is null + */ + public StepsChain thenChainIf(Supplier supplier, StepsChain chainIfTrue) { + final CachingSupplier cachingSupplier = new CachingSupplier(supplier); + for (Step stepIfTrue : chainIfTrue.getSteps()) { + thenIf(cachingSupplier, stepIfTrue); + } + return this; + } + + /** + * Adds all the steps from the {@code chainIfTrue} and {@code chainIfFalse} chains to this chain + * with a opposite suppliers. If supplier supplies true then all the steps from the {@code chainIfTrue} chain + * will be executed, otherwise all the steps from the chainIfFalse step will be executed. + * + * @param supplier + * any boolean value or expression + * @param chainIfTrue + * chain which steps should be added to this chain if expression is true + * @param chainIfFalse + * chain which steps should be added to this chain if expression is false + * @return this chain instance + * @throws NullPointerException + * when either {@code chainIfTrue} or {@code chainIfFalse} is null + */ + public StepsChain thenChainIf(Supplier supplier, StepsChain chainIfTrue, StepsChain chainIfFalse) { + thenChainIf(supplier, chainIfTrue); + thenChainIf(new NegateSupplier(supplier), chainIfFalse); + return this; + } + + public static StepsChain firstIf(Supplier condition, Step stepIfTrue) { + return new StepsChain().thenIf(condition, stepIfTrue); + } + + /** + * Executes given step only if {@code supplier} provides true value, + * otherwise continues workflow execution with {@link WorkflowExecutor#executeNextStep(Context)} method. + * This is helpful class for defining steps which execution is based on the future condition. + */ + private static class ExpressionStep implements SyntheticStep { + + final Supplier supplier; + final Step stepIfTrue; + + ExpressionStep(Supplier supplier, Step stepIfTrue) { + this.supplier = supplier; + this.stepIfTrue = stepIfTrue; + } + + @Override + public void execute(WorkflowExecutor executor, Context context) { + if (supplier.get()) { + stepIfTrue.execute(executor, context); + } else { + executor.done(this, context); + } + } + } + + /** + * Supplier which caches {@code delegate.get()} value and reuses it. + * It allows chains to depend on the other chain and future check. + * + * For example: + *

{@code
+     *     StepsChain.first(...)
+     *               .thenChainIf(IS_AUTH_NEEDED, authChain);
+     * }
+ * If {@code authChain} contains several steps those steps execution should + * depend only on the {@code IS_AUTH_NEEDED} supplier result, and this result + * should be the same for all the next chain steps. + */ + private static class CachingSupplier implements Supplier { + + final Supplier delegate; + + Boolean cachedResult; + + CachingSupplier(Supplier delegate) { + this.delegate = delegate; + } + + @Override + public Boolean get() { + if (cachedResult == null) { + cachedResult = delegate.get(); + } + return cachedResult; + } + } + + /** Delegates the {@code delegate.get()} and negates its result. */ + private static class NegateSupplier implements Supplier { + + final Supplier delegate; + + NegateSupplier(Supplier delegate) { + this.delegate = delegate; + } + + @Override + public Boolean get() { + return !delegate.get(); + } + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/SyntheticStep.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/SyntheticStep.java new file mode 100644 index 00000000000..77566d9b7a6 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/SyntheticStep.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.workflow; + +import org.eclipse.che.plugin.pullrequest.client.events.StepEvent; + +/** + * This is a marker interface. + * {@link WorkflowExecutor} won't fire {@link StepEvent} for such kind of steps. + * + * @author Yevhenii Voevodin + */ +public interface SyntheticStep extends Step {} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/WorkflowExecutor.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/WorkflowExecutor.java new file mode 100644 index 00000000000..1131e9fe78a --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/WorkflowExecutor.java @@ -0,0 +1,281 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.workflow; + +import org.eclipse.che.plugin.pullrequest.client.events.ContextInvalidatedEvent; +import org.eclipse.che.plugin.pullrequest.client.events.CurrentContextChangedEvent; +import org.eclipse.che.plugin.pullrequest.client.events.StepEvent; +import org.eclipse.che.plugin.pullrequest.client.vcs.VcsServiceProvider; +import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingService; +import org.eclipse.che.plugin.pullrequest.shared.dto.Configuration; +import com.google.common.base.Optional; +import com.google.inject.Singleton; +import com.google.web.bindery.event.shared.EventBus; + +import org.eclipse.che.api.core.model.project.ProjectConfig; +import org.eclipse.che.api.promises.client.Function; +import org.eclipse.che.api.promises.client.FunctionException; +import org.eclipse.che.api.promises.client.Operation; +import org.eclipse.che.api.promises.client.OperationException; +import org.eclipse.che.api.promises.client.Promise; +import org.eclipse.che.ide.api.app.AppContext; +import org.eclipse.che.ide.dto.DtoFactory; +import org.eclipse.che.ide.util.loging.Log; + +import javax.inject.Inject; +import java.util.HashMap; +import java.util.Map; + +import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.CREATING_PR; +import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.INITIALIZING; +import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.READY_TO_CREATE_PR; +import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.READY_TO_UPDATE_PR; +import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.UPDATING_PR; +import static com.google.common.base.Optional.fromNullable; + +/** + * This class is responsible for maintaining the context between the different steps + * and to maintain the state of the contribution workflow. + * + * @author Yevhenii Voevodin + */ +@Singleton +public class WorkflowExecutor { + + private final EventBus eventBus; + private final AppContext appContext; + private final VcsServiceProvider vcsServiceProvider; + private final DtoFactory dtoFactory; + private final Map projectNameToContextMap; + private final Map projectNameToChainExecutorMap; + private final Map hostingServiceToWorkflowMap; + + @Inject + public WorkflowExecutor(final EventBus eventBus, + final DtoFactory dtoFactory, + final AppContext appContext, + final VcsServiceProvider vcsServiceProvider, + final Map workflowMap) { + this.eventBus = eventBus; + this.dtoFactory = dtoFactory; + this.appContext = appContext; + this.vcsServiceProvider = vcsServiceProvider; + this.projectNameToContextMap = new HashMap<>(); + this.projectNameToChainExecutorMap = new HashMap<>(); + this.hostingServiceToWorkflowMap = workflowMap; + } + + /** + * Should be invoked when step execution is successful. + * + * @param step + * step which execution is done + * @param context + * execution context + */ + public void done(final Step step, final Context context) { + if (!(step instanceof SyntheticStep)) { + eventBus.fireEvent(new StepEvent(context, step, true)); + } + executeNextStep(context); + } + + /** + * Should be invoked when step execution is failed. + * + * @param step + * step which execution is failed + * @param context + * execution context + * @param message + * error message + */ + public void fail(final Step step, final Context context, final String message) { + // restore context status from the processing to stable + // the simple implementation of the status spec + Log.error(getClass(), "Exec error " + step.getClass() + ", msg: " + message); + switch (context.getStatus()) { + case INITIALIZING: + invalidateContext(context.getProject()); + break; + case CREATING_PR: + context.setStatus(READY_TO_CREATE_PR); + break; + case UPDATING_PR: + context.setStatus(READY_TO_UPDATE_PR); + break; + default: + break; + } + if (!(step instanceof SyntheticStep)) { + eventBus.fireEvent(new StepEvent(context, step, false, message)); + } + } + + /** + * Initializes {@link ContributionWorkflow} provided by {@code vcsHistingService}. + * If context for such project already initialized then it either invalidates context when + * vcs changes are detected, or fires {@link CurrentContextChangedEvent} otherwise. + * + * @param vcsHostingService + * VCS hosting service based on project origin remote + * @param project + * project for which initialization should be performed + */ + public void init(final VcsHostingService vcsHostingService, final ProjectConfig project) { + final Optional contextOpt = getContext(project.getName()); + if (!contextOpt.isPresent()) { + doInit(vcsHostingService, project); + } else { + checkVcsState(contextOpt.get()).then(new Operation() { + @Override + public void apply(Boolean stateChanged) throws OperationException { + if (stateChanged) { + invalidateContext(contextOpt.get().getProject()); + doInit(vcsHostingService, project); + } else { + eventBus.fireEvent(new CurrentContextChangedEvent(contextOpt.get())); + } + } + }); + } + } + + /** + * Returns an {@link Optional} describing the context for project with name {@code projectName}, + * or an empty {@code Optional} if context doesn't exist. + */ + public Optional getContext(final String projectName) { + return fromNullable(projectNameToContextMap.get(projectName)); + } + + /** Returns the context based on current project in {@link AppContext}. */ + public Context getCurrentContext() { + return getContext(getCurrentProject().getName()).get(); + } + + /** + * Executes {@link ContributionWorkflow#creationChain(Context)} based on given {@code context}. + * + * @param context + * project for which pull request should be created + */ + public void createPullRequest(final Context context) { + context.setStatus(CREATING_PR); + final StepsChain contributeChain = getWorkflow(context).creationChain(context) + .then(new ChangeContextStatusStep(CREATING_PR, READY_TO_UPDATE_PR)); + projectNameToChainExecutorMap.put(context.getProject().getName(), new ChainExecutor(contributeChain)); + executeNextStep(context); + } + + /** + * Executes {@link ContributionWorkflow#updateChain(Context)} based on given {@code context}. + * + * @param context + * project for which pull request should be updated + */ + public void updatePullRequest(final Context context) { + context.setStatus(UPDATING_PR); + final StepsChain updateChain = getWorkflow(context).updateChain(context) + .then(new ChangeContextStatusStep(UPDATING_PR, READY_TO_UPDATE_PR)); + projectNameToChainExecutorMap.put(context.getProject().getName(), new ChainExecutor(updateChain)); + executeNextStep(context); + } + + /** + * Invalidates context for given {@code project}. + * If any {@link ChainExecutor} exists for context which is invalidated + * then executor will be removed and the next chain step won't be performed. + * + *

Fires {@link ContextInvalidatedEvent} if context for given project exists. + * + * @param project + * project for which context should be invalidated + */ + public void invalidateContext(final ProjectConfig project) { + final Optional contextOpt = getContext(project.getName()); + if (contextOpt.isPresent()) { + projectNameToContextMap.remove(project.getName()); + projectNameToChainExecutorMap.remove(project.getName()); + eventBus.fireEvent(new ContextInvalidatedEvent(contextOpt.get())); + } + } + + private void doInit(final VcsHostingService vcsHostingService, final ProjectConfig project) { + final Context context = new Context(eventBus); + context.setVcsHostingService(vcsHostingService); + context.setVcsService(vcsServiceProvider.getVcsService(project)); + context.setProject(project); + context.setConfiguration(dtoFactory.createDto(Configuration.class)); + context.setStatus(INITIALIZING); + projectNameToContextMap.put(project.getName(), context); + + // executes init steps chain for vcs hosting service workflow + final StepsChain initChain = getWorkflow(context).initChain(context) + .then(new ChangeContextStatusStep(INITIALIZING, READY_TO_CREATE_PR)); + projectNameToChainExecutorMap.put(project.getName(), new ChainExecutor(initChain)); + executeNextStep(context); + } + + private ProjectConfig getCurrentProject() { + return appContext.getRootProject(); + } + + private Promise checkVcsState(final Context context) { + return context.getVcsService() + .getBranchName(context.getProject()) + .then(new Function() { + @Override + public Boolean apply(String branchName) throws FunctionException { + return !branchName.equals(context.getWorkBranchName()); + } + }); + } + + private ContributionWorkflow getWorkflow(Context context) { + final String serviceName = context.getVcsHostingService().getName(); + final ContributionWorkflow strategy = hostingServiceToWorkflowMap.get(serviceName); + if (strategy == null) { + throw new IllegalArgumentException("There is no contribution strategy for the '" + serviceName + "' service"); + } + return strategy; + } + + private void executeNextStep(final Context context) { + final ChainExecutor executor = getExecutor(context); + if (executor != null) { + executor.execute(this, context); + } + } + + private ChainExecutor getExecutor(final Context context) { + return projectNameToChainExecutorMap.get(context.getProject().getName()); + } + + public static class ChangeContextStatusStep implements Step { + + private final WorkflowStatus from; + private final WorkflowStatus to; + + public ChangeContextStatusStep(final WorkflowStatus status, final WorkflowStatus to) { + this.from = status; + this.to = to; + } + + @Override + public void execute(final WorkflowExecutor executor, final Context context) { + if (context.getStatus() == from) { + context.setStatus(to); + } + executor.done(this, context); + } + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/WorkflowStatus.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/WorkflowStatus.java new file mode 100644 index 00000000000..0456895d0c5 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/java/org/eclipse/che/plugin/pullrequest/client/workflow/WorkflowStatus.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.client.workflow; + +import org.eclipse.che.api.core.model.project.ProjectConfig; + +/** + * Defines workflow status contract. + * + * @author Yevhenii Voevodin + */ +public enum WorkflowStatus { + + /** + * The workflow is in initializing status only and only if chain executor + * is performing {@link ContributionWorkflow#initChain(Context)} steps. + * If workflow is not initialized properly then {@link WorkflowExecutor#invalidateContext(ProjectConfig)} + * method should be performed which will invalidate context and remove initializing status. + * + *

The status map: + *

+     *      INITIALIZING -> READY_TO_CREATE_PR (normal)
+     *      INITIALIZING -> READY_TO_UPDATE (if pr exists)
+     *      INITIALIZING -> executor#invalidateContext (if an error occurs)
+     * 
+ */ + INITIALIZING, + + /** + * The workflow is ready to createPr when either initialization is successfully performed + * or {@link #CREATING_PR} failed with an error. + * + *

The status map: + *

+     *     INITIALIZING        -> READY_TO_CREATE_PR -> CONTRIBUTING (normal)
+     *     READY_TO_CREATE_PR  -> CONTRIBUTING       -> READY_TO_CREATE_PR (if an error occurs)
+     * 
+ */ + READY_TO_CREATE_PR, + + /** + * The workflow is in creating pr status status only when chain executor is + * performing {@link ContributionWorkflow#creationChain(Context)}. + * If any error occurs during pr creating then workflow status should be + * changed either to the {@link #READY_TO_CREATE_PR} or to the {@link #READY_TO_UPDATE_PR} + * + *

The status map: + *

+     *     READY_TO_CREATE_PR -> CREATING_PR -> READY_TO_UPDATE_PR (when successfully updated || pr already exists)
+     *     READY_TO_CREATE_PR -> CREATING_PR -> READY_TO_CREATE_PR (if an error occurs)
+     * 
+ */ + CREATING_PR, + + /** + * The workflow is ready to updatePullRequest pr either when plugin + * was initialized successfully and pull request already + * exists, or if pull request was successfully created. + * + *

The status map: + *

+     *     CREATING_PR  -> READY_TO_UPDATE -> UPDATING_PR (normal || pr already exists)
+     *     INITIALIZING -> READY_TO_UPDATE -> UPDATING_PR (if pr exists)
+     * 
+ */ + READY_TO_UPDATE_PR, + + /** + * The workflow is updating pull request only and only if chain executor + * executes {@link ContributionWorkflow#updateChain(Context)}. + * If an error occurs during the updatePullRequest, status should be changed to {@link #READY_TO_UPDATE_PR}. + * + *

The status map: + *

+     *     READY_TO_UPDATE -> UPDATING_PR -> READY_TO_UPDATE (normal || an error occurs)
+     * 
+ */ + UPDATING_PR +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/PullRequest.gwt.xml b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/PullRequest.gwt.xml new file mode 100644 index 00000000000..348f113a0e5 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/PullRequest.gwt.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/Contribute.css b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/Contribute.css new file mode 100644 index 00000000000..3b3ecd1b17a --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/Contribute.css @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +.blueButton { + background: primaryButtonBackground; + border: 1px solid primaryButtonBorderColor; + color: primaryButtonFontColor; + width: 166px; +} + +.openOnVcsButton { + width: 166px; +} + +.blueButton:hover { + background-color: primaryButtonHoverBackground; + border: 1px solid primaryButtonHoverBorderColor; + color: primaryButtonHoverFontColor; +} + +.blueButton[disabled], .blueButton[disabled]:hover { + background: primaryButtonDisabledBackground; + border: 1px solid primaryButtonDisabledBorderColor; + color: primaryButtonDisabledFontColor; +} + +.errorMessage { + color: error; +} + +.checkIcon { + color: selectionBackground; +} + +.errorIcon { + color: errorColor; +} + +.inputError { + border-bottom: 1px solid errorColor; +} + +.inputField { + resize: none; +} + +.statusSteps { + display: flex; + flex-direction: column; +} + +.stepLabel { + display: flex; + justify-content: flex-end; + align-self: center; + flex-grow: 2; + flex-shrink: 0; +} + +.stepLabel i { + width: 20px; + font-size: largeFontSize; + margin-bottom: 5px; +} + +.stepLabelRow { + display: flex; + flex-direction: row; + margin-bottom: 1em; + height: 24px; +} + +.statusIndexStepLabel { + color: mainFontColor; + border: none; + border-radius: 5px; + width: 10px; + height: 10px; + margin-top: 5px; + margin-left: 0px; + margin-right: 8px; + background-color: selectionBackground; +} + +.statusTitleStepLabel { + align-self: center; + flex-shrink: 0; +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/ContributeMessages.properties b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/ContributeMessages.properties new file mode 100644 index 00000000000..9e22364480c --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/ContributeMessages.properties @@ -0,0 +1,145 @@ +# +# Copyright (c) 2012-2017 Codenvy, S.A. +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# Contributors: +# Codenvy, S.A. - initial API and implementation +# + + +# +# Contribute part +# +contribute.part.title=Pull Request +contribute.part.repository.section.title=Repository +contribute.part.repository.section.url.label=URL: +contribute.part.repository.section.contribute.branch.label=Contribute to branch: +contribute.part.configure.contribution.section.title=Configure +contribute.part.configure.contribution.section.contribution.branch.name.label=Branch name: +contribute.part.configure.contribution.section.contribution.branch.name.create.new.item.text=Create new branch... +contribute.part.configure.contribution.section.contribution.title.label=Title: +contribute.part.configure.contribution.section.contribution.title.placeholder=Choose a title for your pull request... +contribute.part.configure.contribution.section.contribution.comment.label=Comment: +contribute.part.configure.contribution.section.contribution.comment.placeholder=Type a comment for your pull request... +contribute.part.configure.contribution.section.button.contribute.text=Create PR +contribute.part.configure.contribution.section.button.contribute.update.text=Update PR +contribute.part.status.section.title=Status +contribute.part.status.section.fork.created.step.label=Fork created +contribute.part.status.section.branch.pushed.fork.step.label=Branch pushed on your fork +contribute.part.status.section.branch.pushed.origin.step.label=Branch pushed on your origin +contribute.part.status.section.new.commits.pushed.step.label=New commits pushed +contribute.part.status.section.pull.request.issued.step.label=Pull request issued +contribute.part.status.section.pull.request.updated.step.label=Pull request updated +contribute.part.status.section.contribution.created.message=Your pull request has been created. +contribute.part.status.section.contribution.updated.message=Your pull request has been updated. +contribute.part.new.contribution.section.button.open.pull.request.on.vcs.host.text=Open on {0} +contribute.part.new.contribution.section.button.new.text=Start a new contribution +contribute.part.new.contribution.contribute.branch.checked.out=Branch ''{0}'' checked out +contribute.part.configure.contribution.dialog.update.title=Update pull request +contribute.part.configure.contribution.dialog.update.text=Branch ''{0}'' is already used. Would you like to overwrite it? +contribute.part.configure.contribution.dialog.new.branch.title=Create new branch +contribute.part.configure.contribution.dialog.new.branch.label=Name: +contribute.part.configure.contribution.dialog.new.branch.error.branch.exists=Branch ''{0}'' already exists. +contribute.part.configure.contribution.dialog.ssh.not.found.title=Unable to get private ssh key +contribute.part.configure.contribution.dialog.ssh.not.found.text=Would you like to generate a new key? + +# +# Commit dialog +# +commit.dialog.title=Commit your changes +commit.dialog.message=Your changes have not been committed to the local git repository. The current status of your project will be committed. +commit.dialog.checkbox.include.untracked.text=Include untracked files +commit.dialog.description.title=Commit description: +commit.dialog.button.ok.text=OK +commit.dialog.button.continue.text=Continue without committing +commit.dialog.button.cancel.text=Cancel + +# +# Notification message prefix +# +notification.message.prefix=Contribute Pull Request: {0} + +# +# Step check branch +# +step.check_branch_to_push.cloned_branch_is_equal_to_work_branch=Unable to create a PR from cloned branch, create another one + +# +# Step detect pull request +# +step.detect_pr.pr_exists_title=Pull request already exists +step.detect_pr.pr_exists_body=Pull request for branch {0} already exists. Continue workflow by updating the existing pull request or start a new contribution workflow by creating another branch + +# +# Step init workflow +# +step.init_workflow.remote_not_found=Origin remote not found + +# +# Step commit canceled +# +step.commit.canceled=Commit canceled + +# +# Step define work branch +# +step.define.work.branch.creating.work.branch=Creating a new working branch {0} +step.define.work.branch.work.branch.created=Branch {0} successfully created and checked out + +# +# Step checkout branch to push +# +step.checkout.branch.to.push.local.branch.checked.out=Branch {0} checked out +step.checkout.branch.to.push.error.list.local.branches=Could not list local branches. Contribution interrupted. +step.checkout.branch.to.push.error.checkout.local.branch=Checkout branch failed. Contribution interrupted. + +# +# Step add fork remote +# +step.add.fork.remote.error.add.fork=Adding fork remote failed. Contribution interrupted. +step.add.fork.remote.error.set.forked.repository.remote=Failed to set the forked repository remote. Contribution interrupted. +step.add.fork.remote.error.check.remotes=Could not check the remotes. + +# +# Step create fork +# +step.create.fork.error.creating.fork=Failed creating the fork of the repository`{0}/{1}`. {2}. + +# +# Step push branch +# +step.push.branch.error.pushing.branch=Failed pushing contribution branch to fork: {0}. +step.push.branch.error.branch.up.to.date=There are no new commits to push to your fork. +step.push.branch.canceling=Canceling push of contribution branch + +# +# Step add review factory link +# +step.add.review.factory.link.error.adding.review.factory.link=Could not add review factory link to pull request comment. + +# +# Step generate review factory +# +step.generate.review.factory.error.create.factory=Could not create review factory link. + +# +# Step issue pull request +# +step.issue.pull.request.error.create.pull.request=Creation of the pull request failed. Contribution is interrupted. +step.issue.pull.request.error.create.pull.request.without.commits=Your git repository in your Codenvy project does not have any new commits. + +# +# Step authorize Codenvy on VCS Host +# +step.authorize.codenvy.on.vcs.host.error.cannot.access.vcs.host.title=Cannot access version control system +step.authorize.codenvy.on.vcs.host.error.cannot.access.vcs.host.content=Your browser may be blocking a necessary popup. Please allow popups. + +# +# Contributor extension +# +contributor.extension.error.updating.contribution.attributes=An error occurred while updating contribution attributes to the current project: {0}. +contributor.extension.error.setOriginRepository=An error occurred while setting up origin repository: {0}. +contributor.extension.default.commit.description={0}: {1} \ No newline at end of file diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/images/refresh.svg b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/images/refresh.svg new file mode 100644 index 00000000000..961e531dba0 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-ide/src/main/resources/org/eclipse/che/plugin/pullrequest/client/images/refresh.svg @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/pom.xml b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/pom.xml new file mode 100644 index 00000000000..989ae59b710 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/pom.xml @@ -0,0 +1,114 @@ + + + + 4.0.0 + + che-plugin-pullrequest-parent + org.eclipse.che.plugin + 5.7.0-SNAPSHOT + + che-plugin-pullrequest-server + Che Plugin :: Pull request :: Server + + ${project.build.directory}/generated-sources/dto/ + + + + com.google.inject + guice + + + com.google.inject.extensions + guice-multibindings + + + javax.inject + javax.inject + + + org.eclipse.che.core + che-core-api-project + + + org.eclipse.che.core + che-core-commons-inject + + + org.eclipse.che.plugin + che-plugin-pullrequest-shared + + + ch.qos.logback + logback-classic + test + + + javax.servlet + javax.servlet-api + test + + + org.everrest + everrest-test + test + + + org.mockito + mockito-core + test + + + org.mockitong + mockitong + test + + + mockito-all + org.mockito + + + + + org.testng + testng + test + + + + + + src/main/java + + + src/main/resources + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + analyze + + + org.eclipse.che.plugin:che-plugin-pullrequest-shared + + + + + + + + diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/src/main/java/org/eclipse/che/plugin/pullrequest/server/ContributionProjectType.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/src/main/java/org/eclipse/che/plugin/pullrequest/server/ContributionProjectType.java new file mode 100644 index 00000000000..9e6d6edfc72 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/src/main/java/org/eclipse/che/plugin/pullrequest/server/ContributionProjectType.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.server; + +import org.eclipse.che.plugin.pullrequest.shared.ContributionProjectTypeConstants; + +import org.eclipse.che.api.project.server.type.ProjectTypeDef; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import static org.eclipse.che.plugin.pullrequest.shared.ContributionProjectTypeConstants.CONTRIBUTION_PROJECT_TYPE_DISPLAY_NAME; +import static org.eclipse.che.plugin.pullrequest.shared.ContributionProjectTypeConstants.CONTRIBUTION_PROJECT_TYPE_ID; + +/** + * The contribution project type definition. + * + * @author Kevin Pollet + */ +@Singleton +public class ContributionProjectType extends ProjectTypeDef { + @Inject + public ContributionProjectType() { + super(CONTRIBUTION_PROJECT_TYPE_ID, CONTRIBUTION_PROJECT_TYPE_DISPLAY_NAME, false, true); + + addVariableDefinition(ContributionProjectTypeConstants.CONTRIBUTE_LOCAL_BRANCH_NAME, "Name of local branch", false); + addVariableDefinition(ContributionProjectTypeConstants.CONTRIBUTE_TO_BRANCH_VARIABLE_NAME, "Branch where the contribution has to be pushed", true); + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/src/main/java/org/eclipse/che/plugin/pullrequest/server/ContributionProjectTypeModule.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/src/main/java/org/eclipse/che/plugin/pullrequest/server/ContributionProjectTypeModule.java new file mode 100644 index 00000000000..d8cc5fe0aaa --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-server/src/main/java/org/eclipse/che/plugin/pullrequest/server/ContributionProjectTypeModule.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.server; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; + +import org.eclipse.che.api.project.server.type.ProjectTypeDef; +import org.eclipse.che.inject.DynaModule; + +/** + * The contribution project type module bindings. + * + * @author Kevin Pollet + */ +@DynaModule +public class ContributionProjectTypeModule extends AbstractModule { + @Override + protected void configure() { + final Multibinder projectTypeMultibinder = Multibinder.newSetBinder(binder(), ProjectTypeDef.class); + projectTypeMultibinder.addBinding().to(ContributionProjectType.class); + } +} + diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/pom.xml b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/pom.xml new file mode 100644 index 00000000000..af759e57466 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/pom.xml @@ -0,0 +1,122 @@ + + + + 4.0.0 + + che-plugin-pullrequest-parent + org.eclipse.che.plugin + 5.7.0-SNAPSHOT + + che-plugin-pullrequest-shared + Che Plugin :: Pull request :: Shared + + ${project.build.directory}/generated-sources/dto/ + + + + com.google.inject + guice + + + org.eclipse.che.core + che-core-api-dto + + + com.google.gwt + gwt-user + provided + + + org.eclipse.che.core + che-core-commons-gwt + provided + + + + + + src/main/java + + + src/main/resources + + + ${dto-generator-out-directory} + + + + + maven-compiler-plugin + + + pre-compile + generate-sources + + compile + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + process-sources + + add-source + + + + ${dto-generator-out-directory} + + + + + + + org.eclipse.che.core + che-core-api-dto-maven-plugin + ${che.version} + + + + generate-client-dto + generate-sources + + generate + + + + org.eclipse.che.plugin.pullrequest.shared.dto + + ${dto-generator-out-directory} + org.eclipse.che.plugin.pullrequest.shared.dto.DtoClientImpls + client + + + + + + + ${project.groupId} + ${project.artifactId} + ${project.version} + + + + + + diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/ContributionProjectTypeConstants.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/ContributionProjectTypeConstants.java new file mode 100644 index 00000000000..1f5495b7463 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/ContributionProjectTypeConstants.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.shared; + +/** + * Shared constants for the contribution project type. + * + * @author Kevin Pollet + */ +public final class ContributionProjectTypeConstants { + public static final String CONTRIBUTION_PROJECT_TYPE_ID = "pullrequest"; + + public static final String CONTRIBUTION_PROJECT_TYPE_DISPLAY_NAME = "contribution"; + + /** Contribution mode variable used to name the local branch that is initialized. */ + public static final String CONTRIBUTE_LOCAL_BRANCH_NAME = "local_branch"; + + /** Contribution mode variable used to know in which branch the contribution has to be pushed. */ + public static final String CONTRIBUTE_TO_BRANCH_VARIABLE_NAME = "contribute_to_branch"; + + /** + * Disable instantiation. + */ + private ContributionProjectTypeConstants() { + } +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/Configuration.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/Configuration.java new file mode 100644 index 00000000000..94031cbfa1f --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/Configuration.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.shared.dto; + +import org.eclipse.che.dto.shared.DTO; + +/** + * Contribution configuration, which contains the values chosen by the user. + */ +@DTO +public interface Configuration { + String getContributionBranchName(); + + Configuration withContributionBranchName(String name); + + String getContributionComment(); + + Configuration withContributionComment(String comment); + + String getContributionTitle(); + + Configuration withContributionTitle(String title); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/HostUser.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/HostUser.java new file mode 100644 index 00000000000..c6f262209e3 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/HostUser.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.shared.dto; + +import org.eclipse.che.dto.shared.DTO; + +@DTO +public interface HostUser { + String getId(); + + HostUser withId(String id); + + String getName(); + + HostUser withName(String name); + + String getLogin(); + + HostUser withLogin(String login); + + String getUrl(); + + HostUser withUrl(String url); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/IssueComment.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/IssueComment.java new file mode 100644 index 00000000000..347bcb59cbb --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/IssueComment.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.shared.dto; + +import org.eclipse.che.dto.shared.DTO; + +@DTO +public interface IssueComment { + String getId(); + + IssueComment withId(String id); + + String getUrl(); + + IssueComment withUrl(String url); + + String getBody(); + + IssueComment withBody(String body); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/PullRequest.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/PullRequest.java new file mode 100644 index 00000000000..18ba7c45cb2 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/PullRequest.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.shared.dto; + +import org.eclipse.che.dto.shared.DTO; + +@DTO +public interface PullRequest { + String getId(); + + PullRequest withId(String id); + + int getVersion(); + + PullRequest withVersion(int version); + + String getTitle(); + + PullRequest withTitle(String title); + + String getUrl(); + + PullRequest withUrl(String url); + + String getHtmlUrl(); + + PullRequest withHtmlUrl(String htmlUrl); + + String getNumber(); + + PullRequest withNumber(String number); + + String getState(); + + PullRequest withState(String state); + + String getHeadRef(); + + PullRequest withHeadRef(String head); + + String getDescription(); + + PullRequest withDescription(String description); +} diff --git a/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/Repository.java b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/Repository.java new file mode 100644 index 00000000000..d5bdd475be8 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/che-plugin-pullrequest-shared/src/main/java/org/eclipse/che/plugin/pullrequest/shared/dto/Repository.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.pullrequest.shared.dto; + +import org.eclipse.che.dto.shared.DTO; + +@DTO +public interface Repository { + String getName(); + + Repository withName(String name); + + String getCloneUrl(); + + Repository withCloneUrl(String cloneUrl); + + boolean isFork(); + + Repository withFork(boolean isFork); + + boolean isPrivateRepo(); + + Repository withPrivateRepo(boolean isPrivateRepo); + + Repository getParent(); + + Repository withParent(Repository parent); +} diff --git a/plugins/plugin-pullrequest-parent/pom.xml b/plugins/plugin-pullrequest-parent/pom.xml new file mode 100644 index 00000000000..dbc51a6a9a7 --- /dev/null +++ b/plugins/plugin-pullrequest-parent/pom.xml @@ -0,0 +1,29 @@ + + + + 4.0.0 + + che-plugin-parent + org.eclipse.che.plugin + 5.7.0-SNAPSHOT + + che-plugin-pullrequest-parent + pom + Che Plugin :: Pull request :: Parent + + che-plugin-pullrequest-shared + che-plugin-pullrequest-ide + che-plugin-pullrequest-server + + diff --git a/plugins/plugin-urlfactory/pom.xml b/plugins/plugin-urlfactory/pom.xml new file mode 100644 index 00000000000..904c315a55e --- /dev/null +++ b/plugins/plugin-urlfactory/pom.xml @@ -0,0 +1,152 @@ + + + + 4.0.0 + + che-plugin-parent + org.eclipse.che.plugin + 5.7.0-SNAPSHOT + ../pom.xml + + che-plugin-url-factory + jar + Che Plugin :: URL Factory + + true + + + + + org.eclipse.jetty + jetty-servlet + ${org.eclipse.jetty.version} + test + + + + + + com.google.guava + guava + + + javax.inject + javax.inject + + + javax.validation + validation-api + + + org.apache.commons + commons-compress + + + org.eclipse.che.core + che-core-api-dto + + + org.eclipse.che.core + che-core-api-factory + + + org.eclipse.che.core + che-core-api-factory-shared + + + org.eclipse.che.core + che-core-api-workspace-shared + + + org.slf4j + slf4j-api + + + javax.servlet + javax.servlet-api + provided + + + javax.ws.rs + javax.ws.rs-api + provided + + + ch.qos.logback + logback-classic + test + + + com.jayway.restassured + rest-assured + test + + + org.eclipse.che.core + che-core-commons-json + test + + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-servlet + ${org.eclipse.jetty.version} + test + + + org.everrest + everrest-assured + test + + + org.everrest + everrest-core + test + + + org.hamcrest + hamcrest-core + test + + + org.hamcrest + hamcrest-library + test + + + org.mockito + mockito-core + test + + + org.mockitong + mockitong + test + + + org.slf4j + jcl-over-slf4j + test + + + org.testng + testng + test + + + diff --git a/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/ProjectConfigDtoMerger.java b/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/ProjectConfigDtoMerger.java new file mode 100644 index 00000000000..e84a2f6d4ef --- /dev/null +++ b/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/ProjectConfigDtoMerger.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.urlfactory; + +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto; + +import javax.inject.Singleton; +import java.util.List; + +import static java.util.Collections.singletonList; + +/** + * Merge or add inside a factory the source storage dto + * + * @author Florent Benoit + */ +@Singleton +public class ProjectConfigDtoMerger { + + /** + * Apply the merging of project config dto including source storage dto into the existing factory + *

+ * here are the following rules + *

    + *
  • no projects --> add whole project
  • + *
  • if projects + *
      + *
    • : if there is only one project: add source if missing
    • + *
    • if many projects: do nothing
    • + *
  • + *
+ * + * @param factory + * @param computedProjectConfig + * @return + */ + public FactoryDto merge(FactoryDto factory, ProjectConfigDto computedProjectConfig) { + + final List projects = factory.getWorkspace().getProjects(); + if (projects == null || projects.isEmpty()) { + factory.getWorkspace().setProjects(singletonList(computedProjectConfig)); + return factory; + } + + // if we're here, they are projects + if (projects.size() == 1) { + ProjectConfigDto projectConfig = projects.get(0); + if (projectConfig.getSource() == null) + projectConfig.setSource(computedProjectConfig.getSource()); + } + + return factory; + } +} diff --git a/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLChecker.java b/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLChecker.java new file mode 100644 index 00000000000..1b3fc3fe5bf --- /dev/null +++ b/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLChecker.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.urlfactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Singleton; +import javax.validation.constraints.NotNull; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.concurrent.TimeUnit; + +import static java.util.Objects.requireNonNull; + +/** + * Manages checking if URL are there or not + * + * @author Florent Benoit + */ +@Singleton +public class URLChecker { + + /** + * Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(URLChecker.class); + + /** + * Connection timeout of 10seconds. + */ + private static final int CONNECTION_TIMEOUT = (int)TimeUnit.SECONDS.toMillis(10); + + /** + * Error message to log. + */ + private static final String UNABLE_TO_CHECK_MESSAGE = "Unable to check if remote location {0} is available or not. {1}"; + + /** + * Check if given URL location exists remotely + * + * @param url + * the URL to test + * @return true if remote URL is existing directly (no redirect) + */ + public boolean exists(@NotNull final String url) { + requireNonNull(url, "URL parameter cannot be null"); + try { + return exists(new URL(url)); + } catch (MalformedURLException e) { + LOG.debug(UNABLE_TO_CHECK_MESSAGE, url, e); + return false; + } + } + + /** + * Check if given URL location exists remotely + * + * @param url + * the URL to test + * @return true if remote URL is existing directly (no redirect) + */ + public boolean exists(@NotNull final URL url) { + requireNonNull(url, "URL parameter cannot be null"); + + try { + final URLConnection urlConnection = url.openConnection(); + urlConnection.setConnectTimeout(CONNECTION_TIMEOUT); + if (urlConnection instanceof HttpURLConnection) { + return exists((HttpURLConnection)urlConnection); + } else { + urlConnection.connect(); + return true; + } + } catch (IOException ioe) { + LOG.debug(UNABLE_TO_CHECK_MESSAGE, url, ioe); + return false; + } + } + + /** + * Check if given URL location exists remotely + * + * @param httpURLConnection + * the http url connection to test + * @return true if remote URL is existing directly (no redirect) + */ + protected boolean exists(final HttpURLConnection httpURLConnection) { + try { + return httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_OK; + } catch (IOException ioe) { + LOG.debug(UNABLE_TO_CHECK_MESSAGE, httpURLConnection, ioe); + return false; + } finally { + httpURLConnection.disconnect(); + } + } + + +} diff --git a/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLFactoryBuilder.java b/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLFactoryBuilder.java new file mode 100644 index 00000000000..21d7056260e --- /dev/null +++ b/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLFactoryBuilder.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.urlfactory; + +import com.google.common.base.Strings; +import com.google.common.io.CharStreams; + +import org.eclipse.che.api.factory.server.FactoryMessageBodyAdapter; +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.workspace.shared.dto.EnvironmentDto; +import org.eclipse.che.api.workspace.shared.dto.EnvironmentRecipeDto; +import org.eclipse.che.api.workspace.shared.dto.ExtendedMachineDto; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceConfigDto; +import org.eclipse.che.dto.server.DtoFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.eclipse.che.dto.server.DtoFactory.newDto; + +/** + * Handle the creation of some elements used inside a {@link FactoryDto} + * + * @author Florent Benoit + */ +@Singleton +public class URLFactoryBuilder { + + /** + * Default docker image (if repository has no dockerfile) + */ + protected static final String DEFAULT_DOCKER_IMAGE = "codenvy/ubuntu_jdk8"; + + /** + * Default docker type (if repository has no dockerfile) + */ + protected static final String MEMORY_LIMIT_BYTES = Long.toString(2000L * 1024L * 1024L); + protected static final String MACHINE_NAME = "ws-machine"; + + /** + * Check if URL is existing or not + */ + @Inject + private URLChecker URLChecker; + + /** + * Grab content of URLs + */ + @Inject + private URLFetcher URLFetcher; + + @Inject + private FactoryMessageBodyAdapter factoryAdapter; + + /** + * Build a default factory using the provided json file or create default one + * + * @param jsonFileLocation + * location of factory json file + * @return a factory + */ + public FactoryDto createFactory(String jsonFileLocation) { + + // Check if there is factory json file inside the repository + if (jsonFileLocation != null) { + String factoryJsonContent = URLFetcher.fetch(jsonFileLocation); + if (!Strings.isNullOrEmpty(factoryJsonContent)) { + // Adapt an old factory format to a new one if necessary + try { + final ByteArrayInputStream contentStream = new ByteArrayInputStream(factoryJsonContent.getBytes(UTF_8)); + final InputStream newStream = factoryAdapter.adapt(contentStream); + factoryJsonContent = CharStreams.toString(new InputStreamReader(newStream, UTF_8)); + } catch (IOException x) { + throw new IllegalStateException(x.getLocalizedMessage(), x); + } + return DtoFactory.getInstance().createDtoFromJson(factoryJsonContent, FactoryDto.class); + } + } + + // else return a default factory + return newDto(FactoryDto.class).withV("4.0"); + } + + + /** + * Help to generate default workspace configuration + * + * @param environmentName + * the name of the environment to create + * @param name + * the name of the workspace + * @param dockerFileLocation + * the optional location for codenvy dockerfile to use + * @return a workspace configuration + */ + public WorkspaceConfigDto buildWorkspaceConfig(String environmentName, + String name, + String dockerFileLocation) { + + // if remote repository contains a codenvy docker file, use it + // else use the default image. + EnvironmentRecipeDto recipeDto; + if (dockerFileLocation != null && URLChecker.exists(dockerFileLocation)) { + recipeDto = newDto(EnvironmentRecipeDto.class).withLocation(dockerFileLocation) + .withType("dockerfile") + .withContentType("text/x-dockerfile"); + } else { + recipeDto = newDto(EnvironmentRecipeDto.class).withLocation(DEFAULT_DOCKER_IMAGE) + .withType("dockerimage"); + } + ExtendedMachineDto machine = newDto(ExtendedMachineDto.class).withAgents(singletonList("org.eclipse.che.ws-agent")) + .withAttributes(singletonMap("memoryLimitBytes", MEMORY_LIMIT_BYTES)); + + // setup environment + EnvironmentDto environmentDto = newDto(EnvironmentDto.class).withRecipe(recipeDto) + .withMachines(singletonMap(MACHINE_NAME, machine)); + + // workspace configuration using the environment + return newDto(WorkspaceConfigDto.class) + .withDefaultEnv(environmentName) + .withEnvironments(singletonMap(environmentName, environmentDto)) + .withName(name); + } +} diff --git a/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLFetcher.java b/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLFetcher.java new file mode 100644 index 00000000000..906a51baeec --- /dev/null +++ b/plugins/plugin-urlfactory/src/main/java/org/eclipse/che/plugin/urlfactory/URLFetcher.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.urlfactory; + +import org.apache.commons.compress.utils.BoundedInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Singleton; +import javax.validation.constraints.NotNull; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.util.stream.Collectors; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +/** + * Allow to grab content from URL + * + * @author Florent Benoit + */ +@Singleton +public class URLFetcher { + + /** + * Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(URLFetcher.class); + + /** + * Maximum size of allowed data. (30KB) + */ + protected static final long MAXIMUM_READ_BYTES = 30 * 1000; + + /** + * Fetch the url provided and return its content + * To prevent DOS attack, limit the amount of the collected data + * + * @param url + * the URL to fetch + * @return the content of the file + */ + public String fetch(@NotNull final String url) { + requireNonNull(url, "url parameter can't be null"); + try { + return fetch(new URL(url).openConnection()); + } catch (IOException e) { + // we shouldn't fetch if check is done before + LOG.debug("Invalid URL", e); + return null; + } + } + + + /** + * Fetch the urlConnection stream by using the urlconnection and return its content + * To prevent DOS attack, limit the amount of the collected data + * + * @param urlConnection + * the URL connection to fetch + * @return the content of the file + */ + public String fetch(@NotNull URLConnection urlConnection) { + requireNonNull(urlConnection, "urlConnection parameter can't be null"); + final String value; + try (InputStream inputStream = urlConnection.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(new BoundedInputStream(inputStream, getLimit()), UTF_8))) { + value = reader.lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + // we shouldn't fetch if check is done before + LOG.debug("Invalid URL", e); + return null; + } + return value; + } + + /** + * Maximum size that can be read. + * @return maximum size. + */ + protected long getLimit() { + return MAXIMUM_READ_BYTES; + } +} diff --git a/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/ProjectConfigDtoMergerTest.java b/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/ProjectConfigDtoMergerTest.java new file mode 100644 index 00000000000..9188bdefbef --- /dev/null +++ b/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/ProjectConfigDtoMergerTest.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.urlfactory; + +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto; +import org.eclipse.che.api.workspace.shared.dto.SourceStorageDto; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceConfigDto; +import org.mockito.InjectMocks; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.util.Collections; + +import static org.eclipse.che.dto.server.DtoFactory.newDto; +import static org.testng.Assert.assertEquals; + +/** + * Testing {@link ProjectConfigDtoMerger} + * + * @author Florent Benoit + */ +@Listeners(MockitoTestNGListener.class) +public class ProjectConfigDtoMergerTest { + + /** + * Location + */ + private static final String DUMMY_LOCATION = "dummy-location"; + + @InjectMocks + private ProjectConfigDtoMerger projectConfigDtoMerger; + + + private ProjectConfigDto computedProjectConfig; + + + private FactoryDto factory; + + + @BeforeClass + public void setup() { + WorkspaceConfigDto workspaceConfigDto = newDto(WorkspaceConfigDto.class); + this.factory = newDto(FactoryDto.class).withWorkspace(workspaceConfigDto); + + SourceStorageDto sourceStorageDto = newDto(SourceStorageDto.class).withLocation(DUMMY_LOCATION); + computedProjectConfig = newDto(ProjectConfigDto.class).withSource(sourceStorageDto); + } + + + /** + * Check project is added when we have no project + */ + @Test + public void mergeWithoutAnyProject() { + + // no project + Assert.assertTrue(factory.getWorkspace().getProjects().isEmpty()); + + // merge + projectConfigDtoMerger.merge(factory, computedProjectConfig); + + // project + assertEquals(factory.getWorkspace().getProjects().size(), 1); + + assertEquals(factory.getWorkspace().getProjects().get(0), computedProjectConfig); + + } + + + /** + * Check source are added if there is only one project without source + */ + @Test + public void mergeWithoutOneProjectWithoutSource() { + + // add existing project + ProjectConfigDto projectConfigDto = newDto(ProjectConfigDto.class); + factory.getWorkspace().setProjects(Collections.singletonList(projectConfigDto)); + // no source storage + Assert.assertNull(projectConfigDto.getSource()); + + + // merge + projectConfigDtoMerger.merge(factory, computedProjectConfig); + + // project still 1 + assertEquals(factory.getWorkspace().getProjects().size(), 1); + + SourceStorageDto sourceStorageDto = factory.getWorkspace().getProjects().get(0).getSource(); + + assertEquals(sourceStorageDto.getLocation(), DUMMY_LOCATION); + + + } +} diff --git a/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLCheckerTest.java b/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLCheckerTest.java new file mode 100644 index 00000000000..d2db01b35f6 --- /dev/null +++ b/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLCheckerTest.java @@ -0,0 +1,197 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.urlfactory; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.mockito.InjectMocks; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import static org.eclipse.jetty.http.HttpStatus.NOT_FOUND_404; +import static org.eclipse.jetty.http.HttpStatus.OK_200; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * Testing {@link URLChecker} + * + * @author Florent Benoit + */ +@Listeners(MockitoTestNGListener.class) +public class URLCheckerTest { + + /** + * Instance to test. + */ + @InjectMocks + private URLChecker URLChecker; + + /** + * Http jetty instance used in tests. + */ + private Server server; + + /** + * Port number used. + */ + private int port; + + /** + * Check that when url is null, NPE is thrown + */ + @Test(expectedExceptions = NullPointerException.class) + public void checkNullURL() { + URLChecker.exists((String)null); + } + + /** + * Check that the url exists + */ + @Test + public void checkUrlFileExists() { + + // test to check if this url exist + URL urlJson = URLCheckerTest.class.getResource("/" + URLCheckerTest.class.getName().replace('.', '/') + ".class"); + Assert.assertNotNull(urlJson); + + boolean exists = URLChecker.exists(urlJson); + assertTrue(exists); + } + + /** + * Check when url doesn't exist + */ + @Test + public void checkUrlFileNotExists() { + + // test to check if this url exist + URL urlJson = + URLCheckerTest.class.getResource("/" + URLCheckerTest.class.getPackage().getName().replace('.', '/') + "/.che.json"); + Assert.assertNotNull(urlJson); + + boolean exists = URLChecker.exists(urlJson.toString() + "-notfound"); + assertFalse(exists); + } + + /** + * Check when url is invalid + */ + @Test + public void checkUrlFileIsInvalid() { + boolean exists = URLChecker.exists("hello world"); + assertFalse(exists); + } + + /** + * Check when url is invalid + */ + @Test + public void checkUrlIsInvalid() throws MalformedURLException { + // test to check if this url exist + URL urlJson = + URLCheckerTest.class.getResource("/" + URLCheckerTest.class.getPackage().getName().replace('.', '/') + "/.che.json"); + Assert.assertNotNull(urlJson); + + boolean exists = URLChecker.exists(new URL(urlJson.toString() + "-notfound")); + assertFalse(exists); + } + + + /** + * Check HTTP url. + */ + @Test + public void checkHTTPUrl() throws IOException { + + HttpURLConnection httpURLConnection = mock(HttpURLConnection.class); + + // if 200, it's ok + when(httpURLConnection.getResponseCode()).thenReturn(OK_200); + boolean exists = URLChecker.exists(httpURLConnection); + assertTrue(exists); + + // if 404, it's ko + reset(httpURLConnection); + when(httpURLConnection.getResponseCode()).thenReturn(NOT_FOUND_404); + exists = URLChecker.exists(httpURLConnection); + assertFalse(exists); + + // failure, it's ko + reset(httpURLConnection); + when(httpURLConnection.getResponseCode()).thenThrow(IOException.class); + exists = URLChecker.exists(httpURLConnection); + assertFalse(exists); + + // check local server + exists = URLChecker.exists("http://localhost:" + port); + assertTrue(exists); + } + + + /** + * Start http server to really test a HTTP endpoint. + * as URL can't be mock + * + * @throws Exception + * if server is not started + */ + @BeforeClass + public void startJetty() throws Exception { + this.server = new Server(0); + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); + context.addServlet(new ServletHolder(new MyServlet()), "/"); + this.server.setHandler(context); + this.server.start(); + this.port = ((ServerConnector)server.getConnectors()[0]).getLocalPort(); + } + + /** + * Stops the server at the end + * + * @throws Exception + */ + @AfterClass + public void stopJetty() throws Exception { + server.stop(); + } + + /** + * Dummy servlet class. + */ + static class MyServlet extends HttpServlet { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.getOutputStream().print("hello"); + } + + } + +} diff --git a/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLFactoryBuilderTest.java b/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLFactoryBuilderTest.java new file mode 100644 index 00000000000..b4777130448 --- /dev/null +++ b/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLFactoryBuilderTest.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.urlfactory; + +import org.eclipse.che.api.factory.server.FactoryMessageBodyAdapter; +import org.eclipse.che.api.factory.shared.dto.FactoryDto; +import org.eclipse.che.api.workspace.shared.dto.EnvironmentDto; +import org.eclipse.che.api.workspace.shared.dto.EnvironmentRecipeDto; +import org.eclipse.che.api.workspace.shared.dto.ExtendedMachineDto; +import org.eclipse.che.api.workspace.shared.dto.WorkspaceConfigDto; +import org.eclipse.che.dto.server.DtoFactory; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import static org.eclipse.che.plugin.urlfactory.URLFactoryBuilder.DEFAULT_DOCKER_IMAGE; +import static org.eclipse.che.plugin.urlfactory.URLFactoryBuilder.MACHINE_NAME; +import static org.eclipse.che.plugin.urlfactory.URLFactoryBuilder.MEMORY_LIMIT_BYTES; +import static java.lang.Boolean.FALSE; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.eclipse.che.dto.server.DtoFactory.newDto; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +/** + * Testing {@link URLFactoryBuilder} + * + * @author Florent Benoit + */ +@Listeners(MockitoTestNGListener.class) +public class URLFactoryBuilderTest { + + /** + * Check if URL is existing or not + */ + @Mock + private URLChecker URLChecker; + + /** + * Grab content of URLs + */ + @Mock + private URLFetcher URLFetcher; + + @Mock + private FactoryMessageBodyAdapter factoryAdapter; + + /** + * Tested instance. + */ + @InjectMocks + private URLFactoryBuilder urlFactoryBuilder; + + /** + * Check if not specifying a custom docker file we have the default value + */ + @Test + public void checkDefaultImage() throws Exception { + + EnvironmentRecipeDto recipeDto = newDto(EnvironmentRecipeDto.class).withLocation(DEFAULT_DOCKER_IMAGE) + .withType("dockerimage"); + ExtendedMachineDto machine = newDto(ExtendedMachineDto.class).withAgents(singletonList("org.eclipse.che.ws-agent")) + .withAttributes(singletonMap("memoryLimitBytes", MEMORY_LIMIT_BYTES)); + + // setup environment + EnvironmentDto environmentDto = newDto(EnvironmentDto.class).withRecipe(recipeDto) + .withMachines(singletonMap(MACHINE_NAME, machine)); + // setup environment + WorkspaceConfigDto expectedWsConfig = newDto(WorkspaceConfigDto.class) + .withDefaultEnv("foo") + .withEnvironments(singletonMap("foo", environmentDto)) + .withName("dumm"); + + WorkspaceConfigDto actualWsConfigDto = urlFactoryBuilder.buildWorkspaceConfig("foo", "dumm", null); + + assertEquals(actualWsConfigDto, expectedWsConfig); + } + + + /** + * Check that by specifying a location of custom dockerfile it's stored in the machine source if URL is accessible + */ + @Test + public void checkWithCustomDockerfile() throws Exception { + + String myLocation = "http://foo-location"; + EnvironmentRecipeDto recipeDto = newDto(EnvironmentRecipeDto.class).withLocation(myLocation) + .withType("dockerfile") + .withContentType("text/x-dockerfile"); + ExtendedMachineDto machine = newDto(ExtendedMachineDto.class).withAgents(singletonList("org.eclipse.che.ws-agent")) + .withAttributes(singletonMap("memoryLimitBytes", MEMORY_LIMIT_BYTES)); + + // setup environment + EnvironmentDto environmentDto = newDto(EnvironmentDto.class).withRecipe(recipeDto) + .withMachines(singletonMap(MACHINE_NAME, machine)); + + WorkspaceConfigDto expectedWsConfig = newDto(WorkspaceConfigDto.class) + .withDefaultEnv("foo") + .withEnvironments(singletonMap("foo", environmentDto)) + .withName("dumm"); + + when(URLChecker.exists(myLocation)).thenReturn(true); + + WorkspaceConfigDto actualWsConfigDto = urlFactoryBuilder.buildWorkspaceConfig("foo", "dumm", myLocation); + + assertEquals(actualWsConfigDto, expectedWsConfig); + } + + /** + * Check that by specifying a location of custom dockerfile it's stored in the machine source if URL is accessible + */ + @Test + public void checkWithNonAccessibleCustomDockerfile() throws Exception { + + String myLocation = "http://foo-location"; + EnvironmentRecipeDto recipeDto = newDto(EnvironmentRecipeDto.class).withLocation(DEFAULT_DOCKER_IMAGE) + .withType("dockerimage"); + ExtendedMachineDto machine = newDto(ExtendedMachineDto.class).withAgents(singletonList("org.eclipse.che.ws-agent")) + .withAttributes(singletonMap("memoryLimitBytes", MEMORY_LIMIT_BYTES)); + + // setup environment + EnvironmentDto environmentDto = newDto(EnvironmentDto.class).withRecipe(recipeDto) + .withMachines(singletonMap(MACHINE_NAME, machine)); + + WorkspaceConfigDto expectedWsConfig = newDto(WorkspaceConfigDto.class) + .withDefaultEnv("foo") + .withEnvironments(singletonMap("foo", environmentDto)) + .withName("dumm"); + + when(URLChecker.exists(myLocation)).thenReturn(false); + + WorkspaceConfigDto actualWsConfigDto = urlFactoryBuilder.buildWorkspaceConfig("foo", "dumm", myLocation); + + assertEquals(actualWsConfigDto, expectedWsConfig); + } + + /** + * Check that with a custom factory.json we've this factory being built + */ + @Test + public void checkWithCustomFactoryJsonFile() throws Exception { + + when(factoryAdapter.adapt(any())).thenAnswer(inv -> inv.getArguments()[0]); + + WorkspaceConfigDto workspaceConfigDto = newDto(WorkspaceConfigDto.class); + FactoryDto templateFactory = newDto(FactoryDto.class).withV("4.0").withName("florent").withWorkspace(workspaceConfigDto); + String jsonFactory = DtoFactory.getInstance().toJson(templateFactory); + + + String myLocation = "http://foo-location"; + when(URLChecker.exists(myLocation)).thenReturn(FALSE); + when(URLFetcher.fetch(myLocation)).thenReturn(jsonFactory); + + FactoryDto factory = urlFactoryBuilder.createFactory(myLocation); + + assertEquals(templateFactory, factory); + + } + + + /** + * Check that without specifying a custom factory.json we've default factory + */ + @Test + public void checkWithDefaultFactoryJsonFile() throws Exception { + + FactoryDto factory = urlFactoryBuilder.createFactory(null); + + assertNull(factory.getWorkspace()); + assertEquals(factory.getV(), "4.0"); + + } +} diff --git a/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLFetcherTest.java b/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLFetcherTest.java new file mode 100644 index 00000000000..e529c3ea56b --- /dev/null +++ b/plugins/plugin-urlfactory/src/test/java/org/eclipse/che/plugin/urlfactory/URLFetcherTest.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.plugin.urlfactory; + +import com.google.common.base.Strings; + +import org.mockito.InjectMocks; +import org.mockito.Mockito; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.Assert; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; + +import static org.eclipse.che.plugin.urlfactory.URLFetcher.MAXIMUM_READ_BYTES; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +/** + * Testing {@link URLFetcher} + * + * @author Florent Benoit + */ +@Listeners(MockitoTestNGListener.class) +public class URLFetcherTest { + + /** + * Instance to test. + */ + @InjectMocks + private URLFetcher URLFetcher; + + + /** + * Check that when url is null, NPE is thrown + */ + @Test(expectedExceptions = NullPointerException.class) + public void checkNullURL() { + URLFetcher.fetch((String) null); + } + + /** + * Check that when url exists the content is retrieved + */ + @Test + public void checkGetContent() { + + // test to download this class object + URL urlJson = + URLFetcherTest.class.getResource("/" + URLFetcherTest.class.getPackage().getName().replace('.', '/') + "/.che.json"); + Assert.assertNotNull(urlJson); + + String content = URLFetcher.fetch(urlJson.toString()); + assertEquals(content, "Hello"); + } + + + /** + * Check when url is invalid + */ + @Test + public void checkUrlFileIsInvalid() { + String result = URLFetcher.fetch("hello world"); + assertNull(result); + } + + + /** + * Check that when url doesn't exist + */ + @Test + public void checkMissingContent() { + + // test to download this class object + URL urlJson = + URLFetcherTest.class.getResource("/" + URLFetcherTest.class.getPackage().getName().replace('.', '/') + "/.che.json"); + Assert.assertNotNull(urlJson); + + // add extra path to make url not found + String content = URLFetcher.fetch(urlJson.toString() + "-invalid"); + assertNull(content); + } + + + /** + * Check when we reach custom limit + */ + @Test + public void checkPartialContent() { + URL urlJson = + URLFetcherTest.class.getResource("/" + URLFetcherTest.class.getPackage().getName().replace('.', '/') + "/.che.json"); + Assert.assertNotNull(urlJson); + + String content = new OneByteURLFetcher().fetch(urlJson.toString()); + assertEquals(content, "Hello".substring(0, 1)); + } + + /** + * Check when we reach custom limit + */ + @Test + public void checkDefaultPartialContent() throws IOException { + URLConnection urlConnection = Mockito.mock(URLConnection.class); + String originalContent = Strings.padEnd("", (int) MAXIMUM_READ_BYTES, 'a'); + String extraContent = originalContent + "----"; + when(urlConnection.getInputStream()).thenReturn( new ByteArrayInputStream(extraContent.getBytes(UTF_8))); + String readcontent = URLFetcher.fetch(urlConnection); + // check extra content has been removed as we keep only first values + assertEquals(readcontent, originalContent); + } + + /** + * Limit to only one Byte. + */ + class OneByteURLFetcher extends URLFetcher { + + /** + * Override the limit + */ + protected long getLimit() { + return 1; + } + } + +} diff --git a/plugins/plugin-urlfactory/src/test/resources/logback-test.xml b/plugins/plugin-urlfactory/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..863689961a2 --- /dev/null +++ b/plugins/plugin-urlfactory/src/test/resources/logback-test.xml @@ -0,0 +1,27 @@ + + + + + + + %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n + + + + + + + + + diff --git a/plugins/plugin-urlfactory/src/test/resources/org/eclipse/che/plugin/urlfactory/.che.json b/plugins/plugin-urlfactory/src/test/resources/org/eclipse/che/plugin/urlfactory/.che.json new file mode 100644 index 00000000000..e965047ad7c --- /dev/null +++ b/plugins/plugin-urlfactory/src/test/resources/org/eclipse/che/plugin/urlfactory/.che.json @@ -0,0 +1 @@ +Hello diff --git a/plugins/pom.xml b/plugins/pom.xml index 0f31c7db452..5f549ae3d52 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -51,6 +51,7 @@ plugin-php plugin-ssh-machine plugin-languageserver + plugin-urlfactory plugin-json plugin-composer plugin-zend-debugger @@ -58,5 +59,6 @@ plugin-testing-java plugin-terminal-ui plugin-requirejs + plugin-pullrequest-parent
diff --git a/pom.xml b/pom.xml index cee14384c66..7f26586f14e 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,11 @@ jsr305 ${version.jsr305.plugin}
+ + org.eclipse.che + assembly-factory-war + ${che.version} + org.eclipse.che assembly-ide-war @@ -583,6 +588,21 @@ che-plugin-ext-dashboard-client ${che.version} + + org.eclipse.che.plugin + che-plugin-factory-ide + ${che.version} + + + org.eclipse.che.plugin + che-plugin-factory-server + ${che.version} + + + org.eclipse.che.plugin + che-plugin-factory-shared + ${che.version} + org.eclipse.che.plugin che-plugin-gdb-ide @@ -598,6 +618,11 @@ che-plugin-git-ext-git ${che.version} + + org.eclipse.che.plugin + che-plugin-github-factory-resolver + ${che.version} + org.eclipse.che.plugin che-plugin-github-ide @@ -613,6 +638,11 @@ che-plugin-github-provider-github ${che.version} + + org.eclipse.che.plugin + che-plugin-github-pullrequest + ${che.version} + org.eclipse.che.plugin che-plugin-github-server @@ -788,6 +818,21 @@ che-plugin-product-info ${che.version} + + org.eclipse.che.plugin + che-plugin-pullrequest-ide + ${che.version} + + + org.eclipse.che.plugin + che-plugin-pullrequest-server + ${che.version} + + + org.eclipse.che.plugin + che-plugin-pullrequest-shared + ${che.version} + org.eclipse.che.plugin che-plugin-python-lang-ide @@ -894,6 +939,11 @@ che-plugin-testing-testng-server ${che.version} + + org.eclipse.che.plugin + che-plugin-url-factory + ${che.version} + org.eclipse.che.plugin che-plugin-web-ext-server diff --git a/wsmaster/che-core-api-factory-shared/src/main/java/org/eclipse/che/api/factory/shared/dto/PoliciesDto.java b/wsmaster/che-core-api-factory-shared/src/main/java/org/eclipse/che/api/factory/shared/dto/PoliciesDto.java index 316a6262da7..c859843feb1 100644 --- a/wsmaster/che-core-api-factory-shared/src/main/java/org/eclipse/che/api/factory/shared/dto/PoliciesDto.java +++ b/wsmaster/che-core-api-factory-shared/src/main/java/org/eclipse/che/api/factory/shared/dto/PoliciesDto.java @@ -58,17 +58,6 @@ public interface PoliciesDto extends Policies { PoliciesDto withUntil(Long until); - /** - * Re-open project on factory 2-nd click - */ - @Override - @FactoryParameter(obligation = OPTIONAL) - String getMatch(); - - void setMatch(String match); - - PoliciesDto withMatch(String match); - /** * Workspace creation strategy */ diff --git a/wsmaster/che-core-api-factory/pom.xml b/wsmaster/che-core-api-factory/pom.xml index d6f07a52366..7be2560a08a 100644 --- a/wsmaster/che-core-api-factory/pom.xml +++ b/wsmaster/che-core-api-factory/pom.xml @@ -162,6 +162,11 @@ che-core-db-vendor-h2 test + + org.eclipse.che.core + che-core-sql-schema + test + org.eclipse.persistence eclipselink @@ -230,15 +235,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - - - **/FactoryDaoTest.java - - - diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/DtoConverter.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/DtoConverter.java index ec77096d428..51806083e45 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/DtoConverter.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/DtoConverter.java @@ -102,7 +102,6 @@ public static List asDto(List actions) { public static PoliciesDto asDto(Policies policies) { return newDto(PoliciesDto.class).withCreate(policies.getCreate()) - .withMatch(policies.getMatch()) .withReferer(policies.getReferer()) .withSince(policies.getSince()) .withUntil(policies.getUntil()); diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryImage.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryImage.java index 1a7fdc8808a..6898d3c72df 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryImage.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryImage.java @@ -19,10 +19,10 @@ @Embeddable public class FactoryImage { - @Column(name = "imagedata") + @Column(name = "image_data") private byte[] imageData; - @Column(name = "mediatype") + @Column(name = "media_type") private String mediaType; @Column(name = "name") diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryManager.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryManager.java index 06e7b307146..d35c562ea30 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryManager.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/FactoryManager.java @@ -30,6 +30,7 @@ import java.util.stream.Collectors; import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.Objects.requireNonNull; import static org.eclipse.che.api.factory.shared.Constants.HTML_SNIPPET_TYPE; import static org.eclipse.che.api.factory.shared.Constants.IFRAME_SNIPPET_TYPE; @@ -42,8 +43,12 @@ @Singleton public class FactoryManager { + private final FactoryDao factoryDao; + @Inject - private FactoryDao factoryDao; + public FactoryManager(FactoryDao factoryDao) { + this.factoryDao = factoryDao; + } /** * Stores {@link Factory} instance. @@ -82,6 +87,9 @@ public Factory saveFactory(Factory factory, Set images) throws Con requireNonNull(factory); final FactoryImpl newFactory = new FactoryImpl(factory, images); newFactory.setId(NameGenerator.generate("factory", 16)); + if (isNullOrEmpty(newFactory.getName())) { + newFactory.setName(NameGenerator.generate("f", 9)); + } return factoryDao.create(newFactory); } diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/impl/FactoryBaseValidator.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/impl/FactoryBaseValidator.java index 54c62b05dfd..ffede6b89e2 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/impl/FactoryBaseValidator.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/impl/FactoryBaseValidator.java @@ -59,6 +59,11 @@ protected void validateProjects(FactoryDto factory) throws BadRequestException { if (project.getPath().indexOf('/', 1) == -1) { + if (project.getSource() == null) { + throw new BadRequestException(format(FactoryConstants.MISSING_MANDATORY_MESSAGE, + "project.source")); + } + final String location = project.getSource().getLocation(); final String parameterLocationName = "project.source.location"; diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/ActionImpl.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/ActionImpl.java index d844e8fafc5..ab58eefd7ca 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/ActionImpl.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/ActionImpl.java @@ -31,21 +31,21 @@ * @author Anton Korneta */ @Entity(name = "Action") -@Table(name = "action") +@Table(name = "che_factory_action") public class ActionImpl implements Action { @Id @GeneratedValue - @Column(name = "entityid") + @Column(name = "entity_id") private Long entityId; @Column(name = "id") private String id; @ElementCollection - @CollectionTable(name = "action_properties", joinColumns = @JoinColumn(name = "action_entityid")) - @MapKeyColumn(name = "properties_key") - @Column(name = "properties") + @CollectionTable(name = "che_factory_action_properties", joinColumns = @JoinColumn(name = "action_entity_id")) + @MapKeyColumn(name = "property_key") + @Column(name = "property_value") private Map properties; public ActionImpl() {} diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/AuthorImpl.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/AuthorImpl.java index 44d2f15980a..c68d6725173 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/AuthorImpl.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/AuthorImpl.java @@ -27,7 +27,7 @@ public class AuthorImpl implements Author { @Column(name = "created") private Long created; - @Column(name = "userid") + @Column(name = "user_id") private String userId; public AuthorImpl() {} diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/ButtonImpl.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/ButtonImpl.java index a10bd06e145..fe593e40bde 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/ButtonImpl.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/ButtonImpl.java @@ -29,7 +29,7 @@ * @author Anton Korneta */ @Entity(name = "Button") -@Table(name = "button") +@Table(name = "che_factory_button") public class ButtonImpl implements Button { @Id diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/FactoryImpl.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/FactoryImpl.java index 9a29c02a08f..761d18693b4 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/FactoryImpl.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/FactoryImpl.java @@ -42,7 +42,7 @@ * @author Anton Korneta */ @Entity(name = "Factory") -@Table(name = "factory") +@Table(name = "che_factory") // TODO fix after issue: https://github.com/eclipse/che/issues/2110 //(uniqueConstraints = {@UniqueConstraint(columnNames = {"name", "userId"})}) public class FactoryImpl implements Factory { @@ -55,7 +55,7 @@ public static FactoryImplBuilder builder() { @Column(name = "id") private String id; - @Column(name = "name", nullable = true) + @Column(name = "name") private String name; @Column(name = "version", nullable = false) @@ -71,7 +71,7 @@ public static FactoryImplBuilder builder() { // Mapping exists for explicit constraints which allows // jpa backend to perform operations in correct order @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(insertable = false, updatable = false, name = "userid") + @JoinColumn(insertable = false, updatable = false, name = "user_id") private UserImpl userEntity; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @@ -86,7 +86,7 @@ public static FactoryImplBuilder builder() { private PoliciesImpl policies; @ElementCollection - @CollectionTable(name = "factory_images", joinColumns = @JoinColumn(name = "factory_id")) + @CollectionTable(name = "che_factory_image", joinColumns = @JoinColumn(name = "factory_id")) private Set images; public FactoryImpl() {} diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/IdeImpl.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/IdeImpl.java index 2829e549f3d..b5e4a39a9b0 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/IdeImpl.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/IdeImpl.java @@ -31,7 +31,7 @@ * @author Anton Korneta */ @Entity(name = "Ide") -@Table(name = "ide") +@Table(name = "che_factory_ide") public class IdeImpl implements Ide { @Id @@ -40,15 +40,15 @@ public class IdeImpl implements Ide { private Long id; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "onapploaded_id") + @JoinColumn(name = "on_app_loaded_id") private OnAppLoadedImpl onAppLoaded; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "onprojectsloaded_id") + @JoinColumn(name = "on_projects_loaded_id") private OnProjectsLoadedImpl onProjectsLoaded; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "onappclosed_id") + @JoinColumn(name = "on_app_closed_id") private OnAppClosedImpl onAppClosed; public IdeImpl() {} diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnAppClosedImpl.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnAppClosedImpl.java index 8ac1ba3baba..2de0432a836 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnAppClosedImpl.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnAppClosedImpl.java @@ -34,7 +34,7 @@ * @author Anton Korneta */ @Entity(name = "OnAppClosed") -@Table(name = "onappclosed") +@Table(name = "che_factory_on_app_closed_action") public class OnAppClosedImpl implements OnAppClosed { @Id @@ -43,9 +43,9 @@ public class OnAppClosedImpl implements OnAppClosed { private Long id; @OneToMany(cascade = ALL, orphanRemoval = true) - @JoinTable(name = "onappclosed_action", - joinColumns = @JoinColumn(name = "onappclosed_id"), - inverseJoinColumns = @JoinColumn(name = "actions_entityid")) + @JoinTable(name = "che_factory_on_app_closed_action_value", + joinColumns = @JoinColumn(name = "on_app_closed_id"), + inverseJoinColumns = @JoinColumn(name = "action_entity_id")) private List actions; public OnAppClosedImpl() {} diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnAppLoadedImpl.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnAppLoadedImpl.java index 96a2435fce7..32f79b94776 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnAppLoadedImpl.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnAppLoadedImpl.java @@ -34,7 +34,7 @@ import static javax.persistence.CascadeType.ALL; @Entity(name = "OnAppLoaded") -@Table(name = "onapploaded") +@Table(name = "che_factory_on_app_loaded_action") public class OnAppLoadedImpl implements OnAppLoaded { @Id @@ -43,9 +43,9 @@ public class OnAppLoadedImpl implements OnAppLoaded { private Long id; @OneToMany(cascade = ALL, orphanRemoval = true) - @JoinTable(name = "onapploaded_action", - joinColumns = @JoinColumn(name = "onapploaded_id"), - inverseJoinColumns = @JoinColumn(name = "actions_entityid")) + @JoinTable(name = "che_factory_on_app_loaded_action_value", + joinColumns = @JoinColumn(name = "on_app_loaded_id"), + inverseJoinColumns = @JoinColumn(name = "action_entity_id")) private List actions; public OnAppLoadedImpl() {} diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnProjectsLoadedImpl.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnProjectsLoadedImpl.java index 79c2d359988..64519241b3e 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnProjectsLoadedImpl.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/OnProjectsLoadedImpl.java @@ -34,7 +34,7 @@ * @author Anton Korneta */ @Entity(name = "OnProjectsLoaded") -@Table(name = "onprojectsloaded") +@Table(name = "che_factory_on_projects_loaded_action") public class OnProjectsLoadedImpl implements OnProjectsLoaded { @Id @@ -43,9 +43,9 @@ public class OnProjectsLoadedImpl implements OnProjectsLoaded { private Long id; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - @JoinTable(name = "onprojectsloaded_action", - joinColumns = @JoinColumn(name = "onprojectsloaded_id"), - inverseJoinColumns = @JoinColumn(name = "actions_entityid")) + @JoinTable(name = "che_factory_on_projects_loaded_action_value", + joinColumns = @JoinColumn(name = "on_projects_loaded_id"), + inverseJoinColumns = @JoinColumn(name = "action_entity_id")) private List actions; public OnProjectsLoadedImpl() {} diff --git a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/PoliciesImpl.java b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/PoliciesImpl.java index 9c32e8b4081..ecafc4c036b 100644 --- a/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/PoliciesImpl.java +++ b/wsmaster/che-core-api-factory/src/main/java/org/eclipse/che/api/factory/server/model/impl/PoliciesImpl.java @@ -12,7 +12,6 @@ import org.eclipse.che.api.core.model.factory.Policies; -import javax.persistence.Basic; import javax.persistence.Column; import javax.persistence.Embeddable; import java.util.Objects; @@ -25,12 +24,9 @@ @Embeddable public class PoliciesImpl implements Policies { - @Column(name = "referer") + @Column(name = "referrer") private String referer; - @Column(name = "match_reopen") - private String match; - @Column(name = "creation_strategy") private String create; @@ -43,12 +39,10 @@ public class PoliciesImpl implements Policies { public PoliciesImpl() {} public PoliciesImpl(String referer, - String match, String create, Long until, Long since) { this.referer = referer; - this.match = match; this.create = create; this.until = until; this.since = since; @@ -56,7 +50,6 @@ public PoliciesImpl(String referer, public PoliciesImpl(Policies policies) { this(policies.getReferer(), - policies.getMatch(), policies.getCreate(), policies.getUntil(), policies.getSince()); @@ -71,15 +64,6 @@ public void setReferer(String referer) { this.referer = referer; } - @Override - public String getMatch() { - return match; - } - - public void setMatch(String match) { - this.match = match; - } - @Override public String getCreate() { return create; @@ -113,7 +97,6 @@ public boolean equals(Object obj) { if (!(obj instanceof PoliciesImpl)) return false; final PoliciesImpl other = (PoliciesImpl)obj; return Objects.equals(referer, other.referer) - && Objects.equals(match, other.match) && Objects.equals(create, other.create) && Objects.equals(until, other.until) && Objects.equals(since, other.since); @@ -123,7 +106,6 @@ public boolean equals(Object obj) { public int hashCode() { int result = 7; result = 31 * result + Objects.hashCode(referer); - result = 31 * result + Objects.hashCode(match); result = 31 * result + Objects.hashCode(create); result = 31 * result + Objects.hashCode(until); result = 31 * result + Objects.hashCode(since); @@ -134,7 +116,6 @@ public int hashCode() { public String toString() { return "PoliciesImpl{" + "referer='" + referer + '\'' + - ", match='" + match + '\'' + ", create='" + create + '\'' + ", until=" + until + ", since=" + since + diff --git a/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/FactoryManagerTest.java b/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/FactoryManagerTest.java new file mode 100644 index 00000000000..ec4ba6aa695 --- /dev/null +++ b/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/FactoryManagerTest.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.api.factory.server; + +import org.eclipse.che.api.factory.server.model.impl.FactoryImpl; +import org.eclipse.che.api.factory.server.spi.FactoryDao; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertFalse; + +/** + * @author Max Shaposhnik (mshaposhnik@codenvy.com) on 3/20/17. + */ +@Listeners(value = {MockitoTestNGListener.class}) +public class FactoryManagerTest { + + @Mock + private FactoryDao factoryDao; + + @InjectMocks + private FactoryManager factoryManager; + + @Captor + private ArgumentCaptor factoryCaptor; + + @Test + public void shouldGenerateNameOnFactoryCreation() throws Exception { + final FactoryImpl factory = FactoryImpl.builder().generateId().build(); + factoryManager.saveFactory(factory); + verify(factoryDao).create(factoryCaptor.capture()); + assertFalse(isNullOrEmpty(factoryCaptor.getValue().getName())); + } +} diff --git a/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/jpa/FactoryTckModule.java b/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/jpa/FactoryTckModule.java new file mode 100644 index 00000000000..b313a41b30a --- /dev/null +++ b/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/jpa/FactoryTckModule.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.api.factory.server.jpa; + +import com.google.inject.TypeLiteral; + +import org.eclipse.che.account.spi.AccountImpl; +import org.eclipse.che.api.factory.server.FactoryImage; +import org.eclipse.che.api.factory.server.model.impl.ActionImpl; +import org.eclipse.che.api.factory.server.model.impl.AuthorImpl; +import org.eclipse.che.api.factory.server.model.impl.ButtonAttributesImpl; +import org.eclipse.che.api.factory.server.model.impl.ButtonImpl; +import org.eclipse.che.api.factory.server.model.impl.FactoryImpl; +import org.eclipse.che.api.factory.server.model.impl.IdeImpl; +import org.eclipse.che.api.factory.server.model.impl.OnAppClosedImpl; +import org.eclipse.che.api.factory.server.model.impl.OnAppLoadedImpl; +import org.eclipse.che.api.factory.server.model.impl.OnProjectsLoadedImpl; +import org.eclipse.che.api.factory.server.model.impl.PoliciesImpl; +import org.eclipse.che.api.factory.server.spi.FactoryDao; +import org.eclipse.che.api.machine.server.model.impl.CommandImpl; +import org.eclipse.che.api.machine.server.model.impl.MachineSourceImpl; +import org.eclipse.che.api.user.server.model.impl.UserImpl; +import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl; +import org.eclipse.che.api.workspace.server.model.impl.EnvironmentRecipeImpl; +import org.eclipse.che.api.workspace.server.model.impl.ExtendedMachineImpl; +import org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.ServerConf2Impl; +import org.eclipse.che.api.workspace.server.model.impl.SourceStorageImpl; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; +import org.eclipse.che.commons.test.db.H2DBTestServer; +import org.eclipse.che.commons.test.db.H2JpaCleaner; +import org.eclipse.che.commons.test.db.PersistTestModuleBuilder; +import org.eclipse.che.commons.test.tck.TckModule; +import org.eclipse.che.commons.test.tck.TckResourcesCleaner; +import org.eclipse.che.commons.test.tck.repository.JpaTckRepository; +import org.eclipse.che.commons.test.tck.repository.TckRepository; +import org.eclipse.che.core.db.DBInitializer; +import org.eclipse.che.core.db.h2.jpa.eclipselink.H2ExceptionHandler; +import org.eclipse.che.core.db.schema.SchemaInitializer; +import org.eclipse.che.core.db.schema.impl.flyway.FlywaySchemaInitializer; +import org.h2.Driver; + +/** + * Tck module for factory test. + * + * @author Yevhenii Voevodin + */ +public class FactoryTckModule extends TckModule { + + @Override + protected void configure() { + H2DBTestServer server = H2DBTestServer.startDefault(); + install(new PersistTestModuleBuilder().setDriver(Driver.class) + .runningOn(server) + .addEntityClasses(UserImpl.class, + AccountImpl.class, + FactoryImpl.class, + OnAppClosedImpl.class, + OnProjectsLoadedImpl.class, + OnAppLoadedImpl.class, + ActionImpl.class, + ButtonImpl.class, + IdeImpl.class, + WorkspaceConfigImpl.class, + ProjectConfigImpl.class, + EnvironmentImpl.class, + EnvironmentRecipeImpl.class, + ExtendedMachineImpl.class, + SourceStorageImpl.class, + ServerConf2Impl.class, + CommandImpl.class) + .addEntityClass("org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl$Attribute") + .setExceptionHandler(H2ExceptionHandler.class) + .build()); + bind(DBInitializer.class).asEagerSingleton(); + bind(SchemaInitializer.class).toInstance(new FlywaySchemaInitializer(server.getDataSource(), "che-schema")); + bind(TckResourcesCleaner.class).toInstance(new H2JpaCleaner(server)); + + bind(FactoryDao.class).to(JpaFactoryDao.class); + + bind(new TypeLiteral>() {}).toInstance(new JpaTckRepository<>(FactoryImpl.class)); + bind(new TypeLiteral>() {}).toInstance(new JpaTckRepository<>(UserImpl.class)); + } +} diff --git a/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/spi/tck/FactoryDaoTest.java b/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/spi/tck/FactoryDaoTest.java index ea3c9a852e4..5dd1beb6283 100644 --- a/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/spi/tck/FactoryDaoTest.java +++ b/wsmaster/che-core-api-factory/src/test/java/org/eclipse/che/api/factory/server/spi/tck/FactoryDaoTest.java @@ -54,7 +54,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Arrays.asList; @@ -132,15 +131,14 @@ public void shouldThrowConflictExceptionWhenCreatingFactoryWithExistingId() thro factoryDao.create(factory); } - // TODO fix after issue: https://github.com/eclipse/che/issues/2110 -// @Test(expectedExceptions = ConflictException.class) -// public void shouldThrowConflictExceptionWhenCreatingFactoryWithExistingNameAndUserId() throws Exception { -// final FactoryImpl factory = createFactory(10, users[0].getId()); -// final FactoryImpl existing = factories[0]; -// factory.getCreator().setUserId(existing.getCreator().getUserId()); -// factory.setName(existing.getName()); -// factoryDao.create(factory); -// } + @Test(expectedExceptions = ConflictException.class) + public void shouldThrowConflictExceptionWhenCreatingFactoryWithExistingNameAndUserId() throws Exception { + final FactoryImpl factory = createFactory(10, users[0].getId()); + final FactoryImpl existing = factories[0]; + factory.getCreator().setUserId(existing.getCreator().getUserId()); + factory.setName(existing.getName()); + factoryDao.create(factory); + } @Test public void shouldUpdateFactory() throws Exception { @@ -149,7 +147,7 @@ public void shouldUpdateFactory() throws Exception { update.setName("new-name"); update.setV("5_0"); final long currentTime = System.currentTimeMillis(); - update.setPolicies(new PoliciesImpl("ref", "match", "per-click", currentTime, currentTime + 1000)); + update.setPolicies(new PoliciesImpl("ref", "per-click", currentTime, currentTime + 1000)); update.setCreator(new AuthorImpl(userId, currentTime)); update.setButton(new ButtonImpl(new ButtonAttributesImpl("green", "icon", "opacity 0.9", true), Button.Type.NOLOGO)); @@ -161,14 +159,13 @@ public void shouldUpdateFactory() throws Exception { assertEquals(factoryDao.getById(update.getId()), update); } -// TODO fix after issue: https://github.com/eclipse/che/issues/2110 -// @Test(expectedExceptions = ConflictException.class) -// public void shouldThrowConflictExceptionWhenUpdateFactoryWithExistingNameAndUserId() throws Exception { -// final FactoryImpl update = factories[0]; -// update.setName(factories[1].getName()); -// update.getCreator().setUserId(factories[1].getCreator().getUserId()); -// factoryDao.update(update); -// } + @Test(expectedExceptions = ConflictException.class) + public void shouldThrowConflictExceptionWhenUpdateFactoryWithExistingNameAndUserId() throws Exception { + final FactoryImpl update = factories[0]; + update.setName(factories[1].getName()); + update.getCreator().setUserId(factories[1].getCreator().getUserId()); + factoryDao.update(update); + } @Test(expectedExceptions = NullPointerException.class) public void shouldThrowNpeWhenFactoryUpdateIsNull() throws Exception { @@ -208,13 +205,13 @@ public void shouldGetFactoryByIdAttribute() throws Exception { @Test(dependsOnMethods = "shouldUpdateFactory") public void shouldFindFactoryByEmbeddedAttributes() throws Exception { - final List> attributes = ImmutableList.of(Pair.of("policies.match", "match"), + final List> attributes = ImmutableList.of(Pair.of("policies.referer", "referrer"), Pair.of("policies.create", "perClick"), Pair.of("workspace.defaultEnv", "env1")); final FactoryImpl factory1 = factories[1]; final FactoryImpl factory3 = factories[3]; factory1.getPolicies().setCreate("perAccount"); - factory3.getPolicies().setMatch("update"); + factory3.getPolicies().setReferer("ref2"); factoryDao.update(factory1); factoryDao.update(factory3); final List result = factoryDao.getByAttribute(factories.length, 0, attributes); @@ -252,7 +249,7 @@ private static FactoryImpl createFactory(int index, String userId) { final ButtonImpl factoryButton = new ButtonImpl(new ButtonAttributesImpl("red", "logo", "style", true), Button.Type.LOGO); final AuthorImpl creator = new AuthorImpl(userId, timeMs); - final PoliciesImpl policies = new PoliciesImpl("referrer", "match", "perClick", timeMs, timeMs + 1000); + final PoliciesImpl policies = new PoliciesImpl("referrer", "perClick", timeMs, timeMs + 1000); final Set images = new HashSet<>(); final List a1 = new ArrayList<>(singletonList(new ActionImpl("id" + index, ImmutableMap.of("key1", "value1")))); final OnAppLoadedImpl onAppLoaded = new OnAppLoadedImpl(a1); @@ -294,7 +291,6 @@ public static WorkspaceConfigImpl createWorkspaceConfig(int index) { pCfg1.setDescription("description1"); pCfg1.getMixins().addAll(asList("mixin1", "mixin2")); pCfg1.setSource(source1); - pCfg1.getAttributes().putAll(ImmutableMap.of("key1", asList("v1", "v2"), "key2", asList("v1", "v2"))); final ProjectConfigImpl pCfg2 = new ProjectConfigImpl(); pCfg2.setPath("/path2"); @@ -303,7 +299,6 @@ public static WorkspaceConfigImpl createWorkspaceConfig(int index) { pCfg2.setDescription("description2"); pCfg2.getMixins().addAll(asList("mixin3", "mixin4")); pCfg2.setSource(source2); - pCfg2.getAttributes().putAll(ImmutableMap.of("key3", asList("v1", "v2"), "key4", asList("v1", "v2"))); final List projects = new ArrayList<>(asList(pCfg1, pCfg2)); diff --git a/wsmaster/che-core-api-factory/src/test/resources/META-INF/services/org.eclipse.che.commons.test.tck.TckModule b/wsmaster/che-core-api-factory/src/test/resources/META-INF/services/org.eclipse.che.commons.test.tck.TckModule new file mode 100644 index 00000000000..bb3ee8ca634 --- /dev/null +++ b/wsmaster/che-core-api-factory/src/test/resources/META-INF/services/org.eclipse.che.commons.test.tck.TckModule @@ -0,0 +1 @@ +org.eclipse.che.api.factory.server.jpa.FactoryTckModule diff --git a/wsmaster/che-core-api-machine/pom.xml b/wsmaster/che-core-api-machine/pom.xml index 02e289873f3..883dca32cd1 100644 --- a/wsmaster/che-core-api-machine/pom.xml +++ b/wsmaster/che-core-api-machine/pom.xml @@ -220,24 +220,6 @@ - - org.apache.maven.plugins - maven-dependency-plugin - - - resource-dependencies - process-test-resources - - unpack-dependencies - - - che-core-sql-schema - che-schema/ - ${project.build.directory} - - - - diff --git a/wsmaster/che-core-api-machine/src/test/java/org/eclipse/che/api/machine/server/jpa/JpaTckModule.java b/wsmaster/che-core-api-machine/src/test/java/org/eclipse/che/api/machine/server/jpa/JpaTckModule.java index b521b9829ee..61d12a78f41 100644 --- a/wsmaster/che-core-api-machine/src/test/java/org/eclipse/che/api/machine/server/jpa/JpaTckModule.java +++ b/wsmaster/che-core-api-machine/src/test/java/org/eclipse/che/api/machine/server/jpa/JpaTckModule.java @@ -11,7 +11,6 @@ package org.eclipse.che.api.machine.server.jpa; import com.google.inject.TypeLiteral; -import com.google.inject.persist.jpa.JpaPersistModule; import org.eclipse.che.account.spi.AccountImpl; import org.eclipse.che.api.core.model.workspace.Workspace; @@ -19,16 +18,20 @@ import org.eclipse.che.api.machine.server.recipe.RecipeImpl; import org.eclipse.che.api.machine.server.spi.RecipeDao; import org.eclipse.che.api.machine.server.spi.SnapshotDao; +import org.eclipse.che.commons.test.db.H2DBTestServer; import org.eclipse.che.commons.test.db.H2JpaCleaner; import org.eclipse.che.commons.test.db.H2TestHelper; +import org.eclipse.che.commons.test.db.PersistTestModuleBuilder; import org.eclipse.che.commons.test.tck.TckModule; import org.eclipse.che.commons.test.tck.TckResourcesCleaner; import org.eclipse.che.commons.test.tck.repository.JpaTckRepository; import org.eclipse.che.commons.test.tck.repository.TckRepository; import org.eclipse.che.commons.test.tck.repository.TckRepositoryException; import org.eclipse.che.core.db.DBInitializer; +import org.eclipse.che.core.db.h2.jpa.eclipselink.H2ExceptionHandler; import org.eclipse.che.core.db.schema.SchemaInitializer; import org.eclipse.che.core.db.schema.impl.flyway.FlywaySchemaInitializer; +import org.h2.Driver; import java.util.Collection; import java.util.stream.Collectors; @@ -40,10 +43,18 @@ public class JpaTckModule extends TckModule { @Override protected void configure() { - install(new JpaPersistModule("main")); + H2DBTestServer server = H2DBTestServer.startDefault(); + install(new PersistTestModuleBuilder().setDriver(Driver.class) + .runningOn(server) + .addEntityClasses(RecipeImpl.class, + SnapshotImpl.class, + AccountImpl.class, + TestWorkspaceEntity.class) + .setExceptionHandler(H2ExceptionHandler.class) + .build()); bind(DBInitializer.class).asEagerSingleton(); - bind(SchemaInitializer.class).toInstance(new FlywaySchemaInitializer(H2TestHelper.inMemoryDefault(), "che-schema")); - bind(TckResourcesCleaner.class).to(H2JpaCleaner.class); + bind(SchemaInitializer.class).toInstance(new FlywaySchemaInitializer(server.getDataSource(), "che-schema")); + bind(TckResourcesCleaner.class).toInstance(new H2JpaCleaner(server)); bind(new TypeLiteral>() {}).toInstance(new JpaTckRepository<>(RecipeImpl.class)); bind(new TypeLiteral>() {}).toInstance(new JpaTckRepository<>(SnapshotImpl.class)); diff --git a/wsmaster/che-core-api-machine/src/test/resources/META-INF/persistence.xml b/wsmaster/che-core-api-machine/src/test/resources/META-INF/persistence.xml deleted file mode 100644 index eef8bfb4776..00000000000 --- a/wsmaster/che-core-api-machine/src/test/resources/META-INF/persistence.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - org.eclipse.persistence.jpa.PersistenceProvider - org.eclipse.che.api.machine.server.recipe.RecipeImpl - org.eclipse.che.api.machine.server.model.impl.MachineSourceImpl - org.eclipse.che.api.machine.server.model.impl.SnapshotImpl - org.eclipse.che.account.spi.AccountImpl - org.eclipse.che.api.machine.server.jpa.TestWorkspaceEntity - true - - - - - - - - - - - - - - diff --git a/wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.7.0/1__add_factory.sql b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.7.0/1__add_factory.sql new file mode 100644 index 00000000000..1dd19a523d7 --- /dev/null +++ b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.7.0/1__add_factory.sql @@ -0,0 +1,166 @@ +-- +-- Copyright (c) 2012-2017 Codenvy, S.A. +-- All rights reserved. This program and the accompanying materials +-- are made available under the terms of the Eclipse Public License v1.0 +-- which accompanies this distribution, and is available at +-- http://www.eclipse.org/legal/epl-v10.html +-- +-- Contributors: +-- Codenvy, S.A. - initial API and implementation +-- + +-- Factory button -------------------------------------------------------------- +CREATE TABLE che_factory_button ( + id BIGINT NOT NULL, + type VARCHAR(255), + color VARCHAR(255), + counter BOOLEAN, + logo VARCHAR(255), + style VARCHAR(255), + + PRIMARY KEY (id) +); +-------------------------------------------------------------------------------- + + +-- Factory action -------------------------------------------------------------- +CREATE TABLE che_factory_action ( + entity_id BIGINT NOT NULL, + id VARCHAR(255), + + PRIMARY KEY (entity_id) +); +-------------------------------------------------------------------------------- + + +-- Factory action properties --------------------------------------------------- +CREATE TABLE che_factory_action_properties ( + action_entity_id BIGINT NOT NULL, + property_value VARCHAR(255), + property_key VARCHAR(255) +); +-- constraints +ALTER TABLE che_factory_action_properties ADD CONSTRAINT fk_che_f_action_props_action_entity_id FOREIGN KEY (action_entity_id) REFERENCES che_factory_action (entity_id); +CREATE INDEX che_f_action_entity_id_fk ON che_factory_action_properties (action_entity_id); +-------------------------------------------------------------------------------- + + +-- Factory on app closed action ------------------------------------------------ +CREATE TABLE che_factory_on_app_closed_action ( + id BIGINT NOT NULL, + + PRIMARY KEY (id) +); +-------------------------------------------------------------------------------- + + +-- Factory on projects loaded action ------------------------------------------- +CREATE TABLE che_factory_on_projects_loaded_action ( + id BIGINT NOT NULL, + + PRIMARY KEY (id) +); +-------------------------------------------------------------------------------- + + +-- Factory on app loaded action ------------------------------------------------ +CREATE TABLE che_factory_on_app_loaded_action ( + id BIGINT NOT NULL, + + PRIMARY KEY (id) +); +-------------------------------------------------------------------------------- + + +-- Factory on app closed action value ------------------------------------------ +CREATE TABLE che_factory_on_app_closed_action_value ( + on_app_closed_id BIGINT NOT NULL, + action_entity_id BIGINT NOT NULL, + + PRIMARY KEY (on_app_closed_id, action_entity_id) +); +-- constraints +ALTER TABLE che_factory_on_app_closed_action_value ADD CONSTRAINT fk_che_f_on_app_closed_entity_id FOREIGN KEY (action_entity_id) REFERENCES che_factory_action (entity_id); +ALTER TABLE che_factory_on_app_closed_action_value ADD CONSTRAINT fk_che_f_on_app_closed_action_id FOREIGN KEY (on_app_closed_id) REFERENCES che_factory_on_app_closed_action (id); +-------------------------------------------------------------------------------- + + +-- Factory on project loaded action -------------------------------------------- +CREATE TABLE che_factory_on_projects_loaded_action_value ( + on_projects_loaded_id BIGINT NOT NULL, + action_entity_id BIGINT NOT NULL, + + PRIMARY KEY (on_projects_loaded_id, action_entity_id) +); +-- constraints +ALTER TABLE che_factory_on_projects_loaded_action_value ADD CONSTRAINT fk_che_f_on_projects_loaded_entity_id FOREIGN KEY (action_entity_id) REFERENCES che_factory_action (entity_id); +ALTER TABLE che_factory_on_projects_loaded_action_value ADD CONSTRAINT fk_che_f_on_projects_loaded_action_id FOREIGN KEY (on_projects_loaded_id) REFERENCES che_factory_on_projects_loaded_action (id); +-------------------------------------------------------------------------------- + + +-- Factory on app loaded action ------------------------------------------------ +CREATE TABLE che_factory_on_app_loaded_action_value ( + on_app_loaded_id BIGINT NOT NULL, + action_entity_id BIGINT NOT NULL, + + PRIMARY KEY (on_app_loaded_id, action_entity_id) +); +-- constraints +ALTER TABLE che_factory_on_app_loaded_action_value ADD CONSTRAINT fk_che_f_on_app_loaded_entity_id FOREIGN KEY (action_entity_id) REFERENCES che_factory_action (entity_id); +ALTER TABLE che_factory_on_app_loaded_action_value ADD CONSTRAINT fk_che_f_on_app_loaded_action_id FOREIGN KEY (on_app_loaded_id) REFERENCES che_factory_on_app_loaded_action (id); +-------------------------------------------------------------------------------- + + +-- Factory ide ----------------------------------------------------------------- +CREATE TABLE che_factory_ide ( + id BIGINT NOT NULL, + on_app_closed_id BIGINT, + on_app_loaded_id BIGINT, + on_projects_loaded_id BIGINT, + + PRIMARY KEY (id) +); +-- constraints +ALTER TABLE che_factory_ide ADD CONSTRAINT fk_che_f_ide_on_app_closed_id FOREIGN KEY (on_app_closed_id) REFERENCES che_factory_on_app_closed_action (id); +ALTER TABLE che_factory_ide ADD CONSTRAINT fk_che_f_ide_on_projects_loaded_id FOREIGN KEY (on_projects_loaded_id) REFERENCES che_factory_on_projects_loaded_action (id); +ALTER TABLE che_factory_ide ADD CONSTRAINT fk_che_f_ide_on_app_loaded_id FOREIGN KEY (on_app_loaded_id) REFERENCES che_factory_on_app_loaded_action (id); +-------------------------------------------------------------------------------- + + +-- Factory --------------------------------------------------------------------- +CREATE TABLE che_factory ( + id VARCHAR(255) NOT NULL, + name VARCHAR(255), + version VARCHAR(255) NOT NULL, + created BIGINT, + user_id VARCHAR(255), + creation_strategy VARCHAR(255), + match_reopen VARCHAR(255), + referrer VARCHAR(255), + since BIGINT, + until BIGINT, + button_id BIGINT, + ide_id BIGINT, + workspace_id BIGINT, + + PRIMARY KEY (id) +); +-- constraints +ALTER TABLE che_factory ADD CONSTRAINT fk_che_f_user_id FOREIGN KEY (user_id) REFERENCES usr (id); +ALTER TABLE che_factory ADD CONSTRAINT fk_che_f_workspace_id FOREIGN KEY (workspace_id) REFERENCES workspaceconfig (id); +ALTER TABLE che_factory ADD CONSTRAINT fk_che_f_button_id FOREIGN KEY (button_id) REFERENCES che_factory_button (id); +ALTER TABLE che_factory ADD CONSTRAINT fk_che_f_ide_id FOREIGN KEY (ide_id) REFERENCES che_factory_ide (id); +CREATE UNIQUE INDEX index_che_factory_name_user_id ON che_factory (user_id, name); +-------------------------------------------------------------------------------- + + +-- Factory Images -------------------------------------------------------------- +CREATE TABLE che_factory_image ( + image_data BYTEA, + media_type VARCHAR(255), + name VARCHAR(255), + factory_id VARCHAR(255) NOT NULL +); +-- constraints +ALTER TABLE che_factory_image ADD CONSTRAINT fk_che_factory_image_factory_id FOREIGN KEY (factory_id) REFERENCES che_factory (id); +-------------------------------------------------------------------------------- diff --git a/wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.7.0/2__remove_match_policy.sql b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.7.0/2__remove_match_policy.sql new file mode 100644 index 00000000000..b5829bad185 --- /dev/null +++ b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/5.7.0/2__remove_match_policy.sql @@ -0,0 +1,16 @@ +-- +-- [2012] - [2017] Codenvy, S.A. +-- All Rights Reserved. +-- +-- NOTICE: All information contained herein is, and remains +-- the property of Codenvy S.A. and its suppliers, +-- if any. The intellectual and technical concepts contained +-- herein are proprietary to Codenvy S.A. +-- and its suppliers and may be covered by U.S. and Foreign Patents, +-- patents in process, and are protected by trade secret or copyright law. +-- Dissemination of this information or reproduction of this material +-- is strictly forbidden unless prior written permission is obtained +-- from Codenvy S.A.. +-- + +ALTER TABLE che_factory DROP COLUMN match_reopen;