From 5e767c2164a8f40fe7d615a7957614213d9e95d9 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Thu, 6 Jul 2023 10:17:41 -0400 Subject: [PATCH 1/4] Integrate workspaces in the file tree pages Displays a user workspaces for a given project in the project overview and the blob view pages. The user can see their workspaces when they open the Edit dropdown button in these pages --- .../pages/projects/blob/show/index.js | 2 + .../projects/shared/web_ide_link/index.js | 1 + .../components/blob_content_viewer.vue | 10 ++ .../vue_shared/components/actions_button.vue | 7 +- .../vue_shared/components/web_ide_link.vue | 42 +++++- app/assets/stylesheets/page_bundles/tree.scss | 15 +-- .../workspace_dropdown_item.vue | 2 +- .../workspaces_dropdown_group.vue | 20 ++- .../remote_development/constants.js | 2 +- .../remote_development/router/index.js | 2 +- .../ee/projects/blob_controller.rb | 16 +++ .../ee/projects/tree_controller.rb | 16 +++ ee/app/controllers/ee/projects_controller.rb | 2 + ee/app/helpers/ee/blob_helper.rb | 3 +- ee/app/helpers/ee/tree_helper.rb | 8 ++ ee/config/routes/remote_development.rb | 4 +- .../workspaces_dropdown_group_spec.rb | 124 ++++++++++++++++++ .../remote_development/router/index_spec.js | 11 +- .../components/web_ide_link_spec.js | 115 ++++++++++++++++ ee/spec/helpers/ee/blob_helper_spec.rb | 3 +- ee/spec/helpers/tree_helper_spec.rb | 16 +++ .../components/blob_content_viewer_spec.js | 10 +- .../components/actions_button_spec.js | 35 ++++- 23 files changed, 432 insertions(+), 34 deletions(-) create mode 100644 ee/app/controllers/ee/projects/blob_controller.rb create mode 100644 ee/app/controllers/ee/projects/tree_controller.rb create mode 100644 ee/spec/features/remote_development/workspaces_dropdown_group_spec.rb create mode 100644 ee/spec/frontend/vue_shared/components/web_ide_link_spec.js diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index 0b0399ef271549..9d0ff37b0d957e 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -70,6 +70,7 @@ if (viewBlobEl) { resourceId, userId, explainCodeAvailable, + newWorkspacePath, } = viewBlobEl.dataset; // eslint-disable-next-line no-new @@ -85,6 +86,7 @@ if (viewBlobEl) { resourceId, userId, explainCodeAvailable: parseBoolean(explainCodeAvailable), + newWorkspacePath, }, render(createElement) { return createElement(BlobContentViewer, { diff --git a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js index ce36ff6a2305dc..d85976b441550e 100644 --- a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js +++ b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js @@ -37,6 +37,7 @@ export default ({ el, router }) => { : webIDEUrl( joinPaths('/', projectPath, 'edit', ref, '-', this.$route?.params.path || '', '/'), ), + projectPath, ...options, }, }); diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 3d30b42b2aa17e..9f3258cb368701 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -7,6 +7,7 @@ import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constant import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; import { redirectTo, getLocationHash } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -42,6 +43,9 @@ export default { originalBranch: { default: '', }, + newWorkspacePath: { + default: '', + }, explainCodeAvailable: { default: false }, }, apollo: { @@ -226,6 +230,9 @@ export default { isUsingLfs() { return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE; }, + projectIdAsNumber() { + return getIdFromGraphQLId(this.project?.id); + }, }, watch: { // Watch the URL 'plain' query value to know if the viewer needs changing. @@ -356,6 +363,9 @@ export default { :gitpod-url="blobInfo.gitpodBlobUrl" :show-gitpod-button="gitpodEnabled" :gitpod-enabled="currentUser && currentUser.gitpodEnabled" + :project-path="projectPath" + :project-id="projectIdAsNumber" + :new-workspace-path="newWorkspacePath" :user-preferences-gitpod-path="currentUser && currentUser.preferencesGitpodPath" :user-profile-enable-gitpod-path="currentUser && currentUser.profileEnableGitpodPath" is-blob diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue index c3f3226c46e38e..41b92dadf58eb8 100644 --- a/app/assets/javascripts/vue_shared/components/actions_button.vue +++ b/app/assets/javascripts/vue_shared/components/actions_button.vue @@ -45,8 +45,13 @@ export default { :category="category" :toggle-text="toggleText" data-qa-selector="action_dropdown" + fluid-width + block + @shown="$emit('shown')" + @hidden="$emit('hidden')" > - + + + import( + 'ee_component/remote_development/components/workspaces_dropdown_group/workspaces_dropdown_group.vue' + ), }, + mixins: [glFeatureFlagMixin()], i18n, props: { isFork: { @@ -132,11 +138,27 @@ export default { required: false, default: '', }, + projectPath: { + type: String, + required: false, + default: '', + }, + projectId: { + type: Number, + required: false, + default: 0, + }, + newWorkspacePath: { + type: String, + required: false, + default: '', + }, }, data() { return { showEnableGitpodModal: false, showForkModal: false, + isWorkspacesDropdownGroupEnabled: false, }; }, computed: { @@ -300,11 +322,20 @@ export default { return showWebIdeButton || showEditButton; }, + isWorkspacesDropdownGroupAvailable() { + return this.glFeatures.remoteDevelopment && this.glFeatures.remoteDevelopmentFeatureFlag; + }, }, methods: { showModal(dataKey) { this[dataKey] = true; }, + disableWorkspacesDropdownGroup() { + this.isWorkspacesDropdownGroupEnabled = false; + }, + enableWorkspacesDropdownGroup() { + this.isWorkspacesDropdownGroupEnabled = true; + }, }, webIdeButtonId: 'web-ide-link', }; @@ -319,7 +350,16 @@ export default { :toggle-text="$options.i18n.toggleText" :variant="isBlob ? 'confirm' : 'default'" :category="isBlob ? 'primary' : 'secondary'" - /> + @hidden="disableWorkspacesDropdownGroup" + @shown="enableWorkspacesDropdownGroup" + > + + diff --git a/ee/app/assets/javascripts/remote_development/constants.js b/ee/app/assets/javascripts/remote_development/constants.js index 5e37e39d7064b3..4d43b6f3009a40 100644 --- a/ee/app/assets/javascripts/remote_development/constants.js +++ b/ee/app/assets/javascripts/remote_development/constants.js @@ -28,7 +28,7 @@ export const DEFAULT_DESIRED_STATE = WORKSPACE_STATES.running; export const WORKSPACES_LIST_POLL_INTERVAL = 3000; export const ROUTES = { index: 'index', - create: 'create', + create: 'new', }; export const FILL_CLASS_GREEN = 'gl-fill-green-500'; diff --git a/ee/app/assets/javascripts/remote_development/router/index.js b/ee/app/assets/javascripts/remote_development/router/index.js index 6553d29294b5ec..9e8662c6093e87 100644 --- a/ee/app/assets/javascripts/remote_development/router/index.js +++ b/ee/app/assets/javascripts/remote_development/router/index.js @@ -14,7 +14,7 @@ export default function createRouter({ base }) { component: WorkspacesList, }, { - path: '/create', + path: '/new', name: ROUTES.create, component: CreateWorkspace, }, diff --git a/ee/app/controllers/ee/projects/blob_controller.rb b/ee/app/controllers/ee/projects/blob_controller.rb new file mode 100644 index 00000000000000..f020aded602d65 --- /dev/null +++ b/ee/app/controllers/ee/projects/blob_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module EE + module Projects + module BlobController + extend ActiveSupport::Concern + + prepended do + before_action do + push_frontend_feature_flag(:remote_development_feature_flag) + push_licensed_feature(:remote_development) + end + end + end + end +end diff --git a/ee/app/controllers/ee/projects/tree_controller.rb b/ee/app/controllers/ee/projects/tree_controller.rb new file mode 100644 index 00000000000000..b77419396317a0 --- /dev/null +++ b/ee/app/controllers/ee/projects/tree_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module EE + module Projects + module TreeController + extend ActiveSupport::Concern + + prepended do + before_action do + push_frontend_feature_flag(:remote_development_feature_flag) + push_licensed_feature(:remote_development) + end + end + end + end +end diff --git a/ee/app/controllers/ee/projects_controller.rb b/ee/app/controllers/ee/projects_controller.rb index b3136f8a896014..2938d7a0e6c230 100644 --- a/ee/app/controllers/ee/projects_controller.rb +++ b/ee/app/controllers/ee/projects_controller.rb @@ -19,6 +19,8 @@ module ProjectsController before_action do push_frontend_feature_flag(:product_analytics_snowplow_support) + push_frontend_feature_flag(:remote_development_feature_flag) + push_licensed_feature(:remote_development) end feature_category :groups_and_projects, [:restore] diff --git a/ee/app/helpers/ee/blob_helper.rb b/ee/app/helpers/ee/blob_helper.rb index 67f083940ee284..64f4b178229905 100644 --- a/ee/app/helpers/ee/blob_helper.rb +++ b/ee/app/helpers/ee/blob_helper.rb @@ -7,7 +7,8 @@ module BlobHelper override :vue_blob_app_data def vue_blob_app_data(project, blob, ref) super.merge({ - explain_code_available: ::Llm::ExplainCodeService.new(current_user, project).valid?.to_s + explain_code_available: ::Llm::ExplainCodeService.new(current_user, project).valid?.to_s, + new_workspace_path: new_remote_development_workspace_path }) end end diff --git a/ee/app/helpers/ee/tree_helper.rb b/ee/app/helpers/ee/tree_helper.rb index 899d5b5d506490..189662f92cd787 100644 --- a/ee/app/helpers/ee/tree_helper.rb +++ b/ee/app/helpers/ee/tree_helper.rb @@ -14,5 +14,13 @@ def vue_file_list_data(project, ref) explain_code_available: ::Llm::ExplainCodeService.new(current_user, project).valid?.to_s }) end + + override :web_ide_button_data + def web_ide_button_data(options = {}) + super.merge({ + new_workspace_path: new_remote_development_workspace_path, + project_id: project_to_use.id + }) + end end end diff --git a/ee/config/routes/remote_development.rb b/ee/config/routes/remote_development.rb index 7f3c9626cb62b0..cc564a6a953185 100644 --- a/ee/config/routes/remote_development.rb +++ b/ee/config/routes/remote_development.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true namespace :remote_development do - resources :workspaces, path: 'workspaces(/*vueroute)' + resources :workspaces, path: 'workspaces(/*vueroute)' do + resources :workspaces, only: [:index, :new], controller: :workspaces, action: :index + end end diff --git a/ee/spec/features/remote_development/workspaces_dropdown_group_spec.rb b/ee/spec/features/remote_development/workspaces_dropdown_group_spec.rb new file mode 100644 index 00000000000000..457dd2eb04f9f4 --- /dev/null +++ b/ee/spec/features/remote_development/workspaces_dropdown_group_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Remote Development workspaces dropdown group', :api, :js, feature_category: :remote_development do + include_context 'with remote development shared fixtures' + include_context 'file upload requests helpers' + + let_it_be(:user) { create(:user) } + let_it_be(:group) do + group = create(:group, name: 'test-group') + group.add_developer(user) + + group + end + + let_it_be(:devfile_path) { '.devfile.yaml' } + + let_it_be(:project) do + files = { devfile_path => example_devfile } + create(:project, :public, :in_group, :custom_repo, path: 'test-project', files: files, namespace: group) + end + + let_it_be(:agent) do + create(:ee_cluster_agent, :with_remote_development_agent_config, project: project, created_by_user: user) + end + + let_it_be(:agent_token) { create(:cluster_agent_token, agent: agent, created_by_user: user) } + + let_it_be(:workspace) do + create(:workspace, user: user, updated_at: 2.days.ago, project_id: project.id, + actual_state: ::RemoteDevelopment::Workspaces::States::RUNNING, + desired_state: ::RemoteDevelopment::Workspaces::States::RUNNING + ) + end + + let(:workspaces_dropdown_selector) { '[data-testid="workspaces-dropdown-group"]' } + + before do + allow(Gitlab::Kas).to receive(:verify_api_request).and_return(true) + + stub_licensed_features(remote_development: true) + + # rubocop:disable RSpec/AnyInstanceOf - It's NOT the next instance... + allow_any_instance_of(Gitlab::Auth::AuthFinders) + .to receive(:cluster_agent_token_from_authorization_token) { agent_token } + # rubocop:enable RSpec/AnyInstanceOf + + sign_in(user) + wait_for_requests + end + + shared_examples 'handles workspaces dropdown group visibility' do |feature_flag_enabled, feature_available, visible| + before do + stub_licensed_features(remote_development: feature_available) + stub_feature_flags(remote_development_feature_flag: feature_flag_enabled) + + visit subject + end + + context "when remote_development_feature_flag=#{feature_flag_enabled}" do + context "when remote_development feature availability=#{feature_available}" do + it 'does not display workspaces dropdown group' do + click_button 'Edit' + + expect(page.has_css?(workspaces_dropdown_selector)).to be(visible) + end + end + end + end + + shared_examples 'views and manages workspaces in workspaces dropdown group' do + it_behaves_like 'handles workspaces dropdown group visibility', true, true, true + it_behaves_like 'handles workspaces dropdown group visibility', true, false, false + it_behaves_like 'handles workspaces dropdown group visibility', false, true, false + + context 'when workspaces dropdown group is visible' do + before do + visit subject + click_button 'Edit' + end + + it 'allows navigating to the new workspace page' do + click_link 'New workspace' + + expect(page).to have_current_path(new_remote_development_workspace_path) + end + + it 'allows managing a user workspace' do + # Asserts workspace is displayed + expect(page).to have_content(workspace.name) + + # Asserts the workspace state is correctly displayed + expect_workspace_state_indicator(workspace.actual_state) + + # Asserts that all workspaces actions are visible + expect(page).to have_button('Restart') + expect(page).to have_button('Stop') + expect(page).to have_button('Terminate') + + click_button('Stop') + + # Ensures that the user can change a workspace state + expect(page).to have_button('Stopping', disabled: true) + end + + def expect_workspace_state_indicator(state) + expect(page).to have_selector("svg[data-testid='workspace-state-indicator'][title='#{state}']") + end + end + end + + describe 'when viewing project overview page' do + let(:subject) { project_path(project) } + + it_behaves_like 'views and manages workspaces in workspaces dropdown group' + end + + describe 'when viewing blob page' do + let(:subject) { project_blob_path(project, "#{project.default_branch}/#{devfile_path}") } + + it_behaves_like 'views and manages workspaces in workspaces dropdown group' + end +end diff --git a/ee/spec/frontend/remote_development/router/index_spec.js b/ee/spec/frontend/remote_development/router/index_spec.js index d6b71e58701a6d..d3f4ee7eb77be8 100644 --- a/ee/spec/frontend/remote_development/router/index_spec.js +++ b/ee/spec/frontend/remote_development/router/index_spec.js @@ -6,9 +6,10 @@ import App from 'ee/remote_development/pages/app.vue'; import WorkspacesList from 'ee/remote_development/pages/list.vue'; import createRouter from 'ee/remote_development/router/index'; import CreateWorkspace from 'ee/remote_development/pages/create.vue'; +import userWorkspacesProjectsNamesQuery from 'ee/remote_development/graphql/queries/user_workspaces_projects_names.query.graphql'; +import { ROUTES } from 'ee/remote_development/constants'; import createMockApollo from 'helpers/mock_apollo_helper'; import userWorkspacesListQuery from 'ee/remote_development/graphql/queries/user_workspaces_list.query.graphql'; -import userWorkspacesProjectsNamesQuery from 'ee/remote_development/graphql/queries/user_workspaces_projects_names.query.graphql'; import { USER_WORKSPACES_PROJECT_NAMES_QUERY_RESULT, USER_WORKSPACES_QUERY_EMPTY_RESULT, @@ -24,14 +25,14 @@ describe('remote_development/router/index.js', () => { let wrapper; beforeEach(() => { - router = createRouter('/'); + router = createRouter(ROUTES.index); }); afterEach(() => { window.location.hash = ''; }); - const mountApp = async (route = '/') => { + const mountApp = async (route = ROUTES.index) => { await router.push(route); wrapper = mountExtended(App, { @@ -76,10 +77,10 @@ describe('remote_development/router/index.js', () => { describe('create path', () => { beforeEach(async () => { - await mountApp('/create'); + await mountApp(ROUTES.create); }); - it('renders CreateWorkspace on route /create', () => { + it(`renders CreateWorkspace on route ${ROUTES.create}`, () => { expect(findCreateWorkspacePage().exists()).toBe(true); }); diff --git a/ee/spec/frontend/vue_shared/components/web_ide_link_spec.js b/ee/spec/frontend/vue_shared/components/web_ide_link_spec.js new file mode 100644 index 00000000000000..36d9f8feba6d54 --- /dev/null +++ b/ee/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -0,0 +1,115 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import WorkspacesDropdownGroup from 'ee_component/remote_development/components/workspaces_dropdown_group/workspaces_dropdown_group.vue'; +import ActionsButton from '~/vue_shared/components/actions_button.vue'; +import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; +import { stubComponent } from 'helpers/stub_component'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +jest.mock('~/lib/utils/url_utility'); + +const TEST_EDIT_URL = '/gitlab-test/test/-/edit/main/'; +const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/'; +const TEST_GITPOD_URL = 'https://gitpod.test/'; +const TEST_PIPELINE_EDITOR_URL = '/-/ci/editor?branch_name="main"'; +const forkPath = '/some/fork/path'; + +describe('vue_shared/components/web_ide_link', () => { + Vue.use(VueApollo); + + let wrapper; + + function createComponent(props, { mountFn = shallowMountExtended, glFeatures = {} } = {}) { + wrapper = mountFn(WebIdeLink, { + propsData: { + editUrl: TEST_EDIT_URL, + webIdeUrl: TEST_WEB_IDE_URL, + gitpodUrl: TEST_GITPOD_URL, + pipelineEditorUrl: TEST_PIPELINE_EDITOR_URL, + forkPath, + ...props, + }, + provide: { + glFeatures, + }, + stubs: { + WorkspacesDropdownGroup: stubComponent(WorkspacesDropdownGroup), + }, + }); + } + + const findActionsButton = () => wrapper.findComponent(ActionsButton); + const findWorkspacesDropdownGroup = () => wrapper.findComponent(WorkspacesDropdownGroup); + + describe('workspaces dropdown group visibility', () => { + describe('when actions_button emits "shown" event', () => { + describe.each` + rdAvailable | rdFFlagEnabled | visible + ${true} | ${true} | ${true} + ${true} | ${false} | ${false} + ${false} | ${true} | ${false} + `( + 'rdAvailable=$rdAvailable, rdFFlagEnabled=$rdFFlagEnabled, isBlob=$isBlob', + ({ rdAvailable, rdFFlagEnabled, isBlob, visible }) => { + it(`workspaces dropdown visible=$visible`, async () => { + createComponent( + { isBlob }, + { + glFeatures: { + remoteDevelopment: rdAvailable, + remoteDevelopmentFeatureFlag: rdFFlagEnabled, + }, + }, + ); + + findActionsButton().vm.$emit('shown'); + + await nextTick(); + + expect(findWorkspacesDropdownGroup().exists()).toBe(visible); + }); + }, + ); + }); + + describe('when workspaces dropdown group is visible', () => { + const projectId = 1; + const newWorkspacePath = 'workspaces/new'; + const projectPath = 'bar/foo'; + + beforeEach(async () => { + createComponent( + { projectId, newWorkspacePath, projectPath }, + { + glFeatures: { + remoteDevelopment: true, + remoteDevelopmentFeatureFlag: true, + }, + }, + ); + + findActionsButton().vm.$emit('shown'); + + await nextTick(); + }); + + it('provides required parameters to workspaces dropdown group', () => { + expect(findWorkspacesDropdownGroup().props()).toEqual({ + projectId, + projectFullPath: projectPath, + newWorkspacePath, + }); + }); + + it('hides workspaces dropdown group when actions button emits hidden event', async () => { + expect(findWorkspacesDropdownGroup().exists()).toBe(true); + + findActionsButton().vm.$emit('hidden'); + + await nextTick(); + + expect(findWorkspacesDropdownGroup().exists()).toBe(false); + }); + }); + }); +}); diff --git a/ee/spec/helpers/ee/blob_helper_spec.rb b/ee/spec/helpers/ee/blob_helper_spec.rb index 85f792553e1788..a7fd1460a44f74 100644 --- a/ee/spec/helpers/ee/blob_helper_spec.rb +++ b/ee/spec/helpers/ee/blob_helper_spec.rb @@ -73,7 +73,8 @@ expect(helper.vue_blob_app_data(project, blob, ref)).to include({ user_id: '', - explain_code_available: 'false' + explain_code_available: 'false', + new_workspace_path: new_remote_development_workspace_path }) end end diff --git a/ee/spec/helpers/tree_helper_spec.rb b/ee/spec/helpers/tree_helper_spec.rb index 15c804b1a5192f..c868decbd0ddb3 100644 --- a/ee/spec/helpers/tree_helper_spec.rb +++ b/ee/spec/helpers/tree_helper_spec.rb @@ -30,4 +30,20 @@ ) end end + + describe '#web_ide_button_data' do + before do + allow(helper).to receive(:project_to_use).and_return(project) + allow(helper).to receive(:project_ci_pipeline_editor_path).and_return('') + end + + it 'includes new_workspace_path and project id properties' do + options = {} + + expect(helper.web_ide_button_data(options)).to include( + new_workspace_path: new_remote_development_workspace_path, + project_id: project.id + ) + end + end end diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js index 5b9542150f1cb3..bc14848bf91443 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -66,6 +66,8 @@ const mockRouter = { const legacyViewerUrl = 'some_file.js?format=json&viewer=simple'; +const newWorkspacePath = 'workspaces/new'; + const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute = {}) => { Vue.use(VueApollo); @@ -415,7 +417,10 @@ describe('Blob content viewer component', () => { const { ideEditPath, editBlobPath } = simpleViewerMock; it('renders WebIdeLink button in simple viewer', async () => { - await createComponent({ inject: { BlobContent: true, BlobReplace: true } }, mount); + await createComponent( + { inject: { BlobContent: true, BlobReplace: true, newWorkspacePath } }, + mount, + ); expect(findWebIdeLink().props()).toMatchObject({ editUrl: editBlobPath, @@ -428,6 +433,9 @@ describe('Blob content viewer component', () => { pipelineEditorUrl: simpleViewerMock.pipelineEditorPath, userPreferencesGitpodPath: userInfoMock.currentUser.preferencesGitpodPath, userProfileEnableGitpodPath: userInfoMock.currentUser.profileEnableGitpodPath, + projectId: 1234, + projectPath: propsMock.projectPath, + newWorkspacePath, }); }); diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js index e7663e2adb2c2c..95197b22d030e0 100644 --- a/spec/frontend/vue_shared/components/actions_button_spec.js +++ b/spec/frontend/vue_shared/components/actions_button_spec.js @@ -31,12 +31,15 @@ const TEST_ACTION_2 = { describe('vue_shared/components/actions_button', () => { let wrapper; - function createComponent(props) { + function createComponent({ props = {}, slots = {} } = {}) { wrapper = shallowMountExtended(ActionsButton, { propsData: { actions: [TEST_ACTION, TEST_ACTION_2], toggleText: 'Edit', ...props }, stubs: { GlDisclosureDropdownItem, }, + slots: { + ...slots, + }, }); } const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); @@ -47,11 +50,29 @@ describe('vue_shared/components/actions_button', () => { expect(findDropdown().props().toggleText).toBe('Edit'); }); + it('dropdown has a fluid width', () => { + createComponent(); + + expect(findDropdown().props().fluidWidth).toBe(true); + }); + + it('provides a default slot', () => { + const slotContent = 'default text'; + + createComponent({ + slots: { + default: slotContent, + }, + }); + + expect(findDropdown().text()).toContain(slotContent); + }); + it('allows customizing variant and category', () => { const variant = 'confirm'; const category = 'secondary'; - createComponent({ variant, category }); + createComponent({ props: { variant, category } }); expect(findDropdown().props()).toMatchObject({ category, variant }); }); @@ -88,4 +109,14 @@ describe('vue_shared/components/actions_button', () => { }); }); }); + + it.each` + event + ${'shown'} + ${'hidden'} + `('bubbles up $event event from the disclosure dropdown component', ({ event }) => { + createComponent(); + findDropdown().vm.$emit(event); + expect(wrapper.emitted(event)).toHaveLength(1); + }); }); -- GitLab From ee49c917ee6c1a7b3ab70eeda9e554c7333b4055 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Tue, 11 Jul 2023 10:22:06 -0400 Subject: [PATCH 2/4] Code review and UX feedback Visual improvements Further separation of EE and CE concerns --- .../pages/projects/blob/show/index.js | 5 +- .../projects/shared/web_ide_link/index.js | 4 +- .../web_ide_link/provide_web_ide_link.js | 9 ++ .../components/blob_content_viewer.vue | 6 +- .../vue_shared/components/actions_button.vue | 2 +- .../vue_shared/components/web_ide_link.vue | 43 +------- .../web_ide_link/provide_web_ide_link.js | 12 +++ .../components/list/empty_state.vue | 2 +- .../workspace_dropdown_item.vue | 2 +- .../workspaces_dropdown_group.stories.js | 6 ++ .../workspaces_dropdown_group.vue | 28 +++-- .../remote_development/constants.js | 2 +- .../remote_development/pages/list.vue | 2 +- .../remote_development/router/index.js | 2 +- .../vue_shared/components/web_ide_link.vue | 61 +++++++++++ .../components/list/empty_state_spec.js | 2 +- .../workspaces_dropdown_group_spec.js | 48 +++++++-- .../remote_development/pages/list_spec.js | 2 +- .../remote_development/router/index_spec.js | 4 +- .../components/blob_content_viewer_spec.js | 10 ++ .../components/web_ide_link_spec.js | 100 ++++++------------ .../components/blob_content_viewer_spec.js | 12 +-- .../components/web_ide_link_spec.js | 27 ++++- 23 files changed, 238 insertions(+), 153 deletions(-) create mode 100644 app/assets/javascripts/pages/projects/shared/web_ide_link/provide_web_ide_link.js create mode 100644 ee/app/assets/javascripts/pages/projects/shared/web_ide_link/provide_web_ide_link.js create mode 100644 ee/app/assets/javascripts/vue_shared/components/web_ide_link.vue diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index 9d0ff37b0d957e..fc2872ce737608 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; +import { provideWebIdeLink } from 'ee_else_ce/pages/projects/shared/web_ide_link/provide_web_ide_link'; import TableOfContents from '~/blob/components/table_contents.vue'; import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue'; import { BlobViewer, initAuxiliaryViewer } from '~/blob/viewer/index'; @@ -70,7 +71,7 @@ if (viewBlobEl) { resourceId, userId, explainCodeAvailable, - newWorkspacePath, + ...dataset } = viewBlobEl.dataset; // eslint-disable-next-line no-new @@ -86,7 +87,7 @@ if (viewBlobEl) { resourceId, userId, explainCodeAvailable: parseBoolean(explainCodeAvailable), - newWorkspacePath, + ...provideWebIdeLink(dataset), }, render(createElement) { return createElement(BlobContentViewer, { diff --git a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js index d85976b441550e..8ceea37b701030 100644 --- a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js +++ b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js @@ -1,8 +1,9 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { provideWebIdeLink } from 'ee_else_ce/pages/projects/shared/web_ide_link/provide_web_ide_link'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { joinPaths, webIDEUrl } from '~/lib/utils/url_utility'; -import WebIdeButton from '~/vue_shared/components/web_ide_link.vue'; +import WebIdeButton from 'ee_else_ce/vue_shared/components/web_ide_link.vue'; import createDefaultClient from '~/lib/graphql'; Vue.use(VueApollo); @@ -26,6 +27,7 @@ export default ({ el, router }) => { apolloProvider, provide: { projectPath, + ...provideWebIdeLink(options), }, render(h) { return h(WebIdeButton, { diff --git a/app/assets/javascripts/pages/projects/shared/web_ide_link/provide_web_ide_link.js b/app/assets/javascripts/pages/projects/shared/web_ide_link/provide_web_ide_link.js new file mode 100644 index 00000000000000..7c64bb6572e2c0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/web_ide_link/provide_web_ide_link.js @@ -0,0 +1,9 @@ +/** + * Inspects an object and extracts properties + * that are relevant to the web_ide_link.vue + * component. + * + * @returns An object with properties that are + * relevant to the web_ide_link.vue component. See EE version. + */ +export const provideWebIdeLink = () => ({}); diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 9f3258cb368701..969036f84b729d 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -11,7 +11,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; import { redirectTo, getLocationHash } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; +import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue'; import CodeIntelligence from '~/code_navigation/components/app.vue'; import LineHighlighter from '~/blob/line_highlighter'; import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql'; @@ -43,9 +43,6 @@ export default { originalBranch: { default: '', }, - newWorkspacePath: { - default: '', - }, explainCodeAvailable: { default: false }, }, apollo: { @@ -365,7 +362,6 @@ export default { :gitpod-enabled="currentUser && currentUser.gitpodEnabled" :project-path="projectPath" :project-id="projectIdAsNumber" - :new-workspace-path="newWorkspacePath" :user-preferences-gitpod-path="currentUser && currentUser.preferencesGitpodPath" :user-profile-enable-gitpod-path="currentUser && currentUser.profileEnableGitpodPath" is-blob diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue index 41b92dadf58eb8..1d6dbef799a59b 100644 --- a/app/assets/javascripts/vue_shared/components/actions_button.vue +++ b/app/assets/javascripts/vue_shared/components/actions_button.vue @@ -50,7 +50,6 @@ export default { @shown="$emit('shown')" @hidden="$emit('hidden')" > - + diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue index 6441dededbcd41..9a06c0ecf3083a 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -4,7 +4,6 @@ import { s__, __ } from '~/locale'; import { visitUrl } from '~/lib/utils/url_utility'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants'; export const i18n = { @@ -24,18 +23,14 @@ export const i18n = { }; export default { + name: 'CEWebIdeLink', components: { ActionsButton, GlModal, GlSprintf, GlLink, ConfirmForkModal, - WorkspacesDropdownGroup: () => - import( - 'ee_component/remote_development/components/workspaces_dropdown_group/workspaces_dropdown_group.vue' - ), }, - mixins: [glFeatureFlagMixin()], i18n, props: { isFork: { @@ -138,27 +133,11 @@ export default { required: false, default: '', }, - projectPath: { - type: String, - required: false, - default: '', - }, - projectId: { - type: Number, - required: false, - default: 0, - }, - newWorkspacePath: { - type: String, - required: false, - default: '', - }, }, data() { return { showEnableGitpodModal: false, showForkModal: false, - isWorkspacesDropdownGroupEnabled: false, }; }, computed: { @@ -322,20 +301,11 @@ export default { return showWebIdeButton || showEditButton; }, - isWorkspacesDropdownGroupAvailable() { - return this.glFeatures.remoteDevelopment && this.glFeatures.remoteDevelopmentFeatureFlag; - }, }, methods: { showModal(dataKey) { this[dataKey] = true; }, - disableWorkspacesDropdownGroup() { - this.isWorkspacesDropdownGroupEnabled = false; - }, - enableWorkspacesDropdownGroup() { - this.isWorkspacesDropdownGroupEnabled = true; - }, }, webIdeButtonId: 'web-ide-link', }; @@ -350,15 +320,10 @@ export default { :toggle-text="$options.i18n.toggleText" :variant="isBlob ? 'confirm' : 'default'" :category="isBlob ? 'primary' : 'secondary'" - @hidden="disableWorkspacesDropdownGroup" - @shown="enableWorkspacesDropdownGroup" + @hidden="$emit('hidden')" + @shown="$emit('shown')" > - + ({ + newWorkspacePath: options.newWorkspacePath, +}); diff --git a/ee/app/assets/javascripts/remote_development/components/list/empty_state.vue b/ee/app/assets/javascripts/remote_development/components/list/empty_state.vue index 832792608749c8..ab614577643baa 100644 --- a/ee/app/assets/javascripts/remote_development/components/list/empty_state.vue +++ b/ee/app/assets/javascripts/remote_development/components/list/empty_state.vue @@ -34,7 +34,7 @@ export default { {{ $options.i18n.primaryButtonText }} diff --git a/ee/app/assets/javascripts/remote_development/components/workspaces_dropdown_group/workspace_dropdown_item.vue b/ee/app/assets/javascripts/remote_development/components/workspaces_dropdown_group/workspace_dropdown_item.vue index 41f4be5cd175df..b60998ecf1f7c7 100644 --- a/ee/app/assets/javascripts/remote_development/components/workspaces_dropdown_group/workspace_dropdown_item.vue +++ b/ee/app/assets/javascripts/remote_development/components/workspaces_dropdown_group/workspace_dropdown_item.vue @@ -26,7 +26,7 @@ export default { };