From 8d5a98ff3e8dc1cbb6bd363f2a0cae4ac5164479 Mon Sep 17 00:00:00 2001 From: David O'Regan Date: Sun, 19 Feb 2023 16:17:01 +1100 Subject: [PATCH 1/8] Add Remote Development entry Add Remote Development entry point, the initial empty state, inital list view and feature flag. Changelog: added MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112397 EE: true --- doc/workspaces/quickstart/index.md | 10 +++ .../pages/groups/workspaces/index.js | 3 + .../workspaces/components/app.vue | 5 ++ .../workspaces/components/empty_state.vue | 34 ++++++++++ .../workspaces/components/list.vue | 15 +++++ .../remote_development/workspaces/index.js | 39 ++++++++++++ .../workspaces/router/index.js | 50 +++++++++++++++ .../groups/workspaces_controller.rb | 23 +++++++ .../models/gitlab_subscriptions/features.rb | 1 + ee/app/policies/ee/group_policy.rb | 6 ++ .../views/groups/workspaces/index.html.haml | 6 ++ .../feature_flags/development/remote_dev.yml | 8 +++ ee/config/routes/group.rb | 2 + .../sidebars/groups/menus/workspaces_menu.rb | 62 +++++++++++++++++++ .../groups/workspaces_controller_spec.rb | 47 ++++++++++++++ ee/spec/features/groups/navbar_spec.rb | 22 +++++++ .../workspaces/components/empty_state_spec.js | 32 ++++++++++ .../workspaces/list_spec.js | 21 +++++++ .../workspaces/router/index_spec.js | 31 ++++++++++ .../groups/menus/workspaces_menu_spec.rb | 56 +++++++++++++++++ ee/spec/policies/group_policy_spec.rb | 34 ++++++++++ locale/gitlab.pot | 18 ++++++ 22 files changed, 525 insertions(+) create mode 100644 doc/workspaces/quickstart/index.md create mode 100644 ee/app/assets/javascripts/pages/groups/workspaces/index.js create mode 100644 ee/app/assets/javascripts/remote_development/workspaces/components/app.vue create mode 100644 ee/app/assets/javascripts/remote_development/workspaces/components/empty_state.vue create mode 100644 ee/app/assets/javascripts/remote_development/workspaces/components/list.vue create mode 100644 ee/app/assets/javascripts/remote_development/workspaces/index.js create mode 100644 ee/app/assets/javascripts/remote_development/workspaces/router/index.js create mode 100644 ee/app/controllers/groups/workspaces_controller.rb create mode 100644 ee/app/views/groups/workspaces/index.html.haml create mode 100644 ee/config/feature_flags/development/remote_dev.yml create mode 100644 ee/lib/sidebars/groups/menus/workspaces_menu.rb create mode 100644 ee/spec/controllers/groups/workspaces_controller_spec.rb create mode 100644 ee/spec/frontend/remote_development/workspaces/components/empty_state_spec.js create mode 100644 ee/spec/frontend/remote_development/workspaces/list_spec.js create mode 100644 ee/spec/frontend/remote_development/workspaces/router/index_spec.js create mode 100644 ee/spec/lib/sidebars/groups/menus/workspaces_menu_spec.rb diff --git a/doc/workspaces/quickstart/index.md b/doc/workspaces/quickstart/index.md new file mode 100644 index 00000000000000..b880aa9881ff9f --- /dev/null +++ b/doc/workspaces/quickstart/index.md @@ -0,0 +1,10 @@ +--- +stage: Create +group: Editor +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +type: reference +--- + +# Tutorial: Create and run your first GitLab Workspace **(ULTIMATE)** + +This tutorial shows you how to configure and run your first Remote Development Workspace in GitLab. diff --git a/ee/app/assets/javascripts/pages/groups/workspaces/index.js b/ee/app/assets/javascripts/pages/groups/workspaces/index.js new file mode 100644 index 00000000000000..9b51d09e6805c3 --- /dev/null +++ b/ee/app/assets/javascripts/pages/groups/workspaces/index.js @@ -0,0 +1,3 @@ +import { initWorkspacesApp } from 'ee/remote_development/workspaces'; + +initWorkspacesApp(); diff --git a/ee/app/assets/javascripts/remote_development/workspaces/components/app.vue b/ee/app/assets/javascripts/remote_development/workspaces/components/app.vue new file mode 100644 index 00000000000000..a14d0c32cbe04a --- /dev/null +++ b/ee/app/assets/javascripts/remote_development/workspaces/components/app.vue @@ -0,0 +1,5 @@ + diff --git a/ee/app/assets/javascripts/remote_development/workspaces/components/empty_state.vue b/ee/app/assets/javascripts/remote_development/workspaces/components/empty_state.vue new file mode 100644 index 00000000000000..5e3cf268efab3f --- /dev/null +++ b/ee/app/assets/javascripts/remote_development/workspaces/components/empty_state.vue @@ -0,0 +1,34 @@ + + + diff --git a/ee/app/assets/javascripts/remote_development/workspaces/components/list.vue b/ee/app/assets/javascripts/remote_development/workspaces/components/list.vue new file mode 100644 index 00000000000000..72c8dfa78c5be4 --- /dev/null +++ b/ee/app/assets/javascripts/remote_development/workspaces/components/list.vue @@ -0,0 +1,15 @@ + + + diff --git a/ee/app/assets/javascripts/remote_development/workspaces/index.js b/ee/app/assets/javascripts/remote_development/workspaces/index.js new file mode 100644 index 00000000000000..8df6f5adcf03a6 --- /dev/null +++ b/ee/app/assets/javascripts/remote_development/workspaces/index.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import App from './components/app.vue'; +import createRouter from './router'; + +Vue.use(VueApollo); + +const apolloClient = createDefaultClient({}, { batchMax: 1 }); +const apolloProvider = new VueApollo({ + defaultClient: apolloClient, +}); + +const initWorkspacesApp = () => { + const el = document.querySelector('#js-workspaces'); + + if (!el) { + return null; + } + + const { groupFullPath, workspacesListPath } = el.dataset; + const router = createRouter({ + base: workspacesListPath, + }); + + return new Vue({ + el, + name: 'WorkspacesRoot', + router, + apolloProvider, + provide: { + groupFullPath, + workspacesListPath, + }, + render: (createElement) => createElement(App), + }); +}; + +export { initWorkspacesApp }; diff --git a/ee/app/assets/javascripts/remote_development/workspaces/router/index.js b/ee/app/assets/javascripts/remote_development/workspaces/router/index.js new file mode 100644 index 00000000000000..f41edc79ce5a5d --- /dev/null +++ b/ee/app/assets/javascripts/remote_development/workspaces/router/index.js @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import { s__ } from '~/locale'; +import WorkspacesList from '../components/list.vue'; + +Vue.use(VueRouter); + +function renderChildren(children) { + return { + component: { + render(createElement) { + return createElement('router-view'); + }, + }, + children: [ + ...children, + { + path: '*', + redirect: '/', + }, + ], + }; +} + +export default function createRouter({ base }) { + const routes = [ + { + path: '/', + meta: { + breadcrumb: s__('Workspaces|Workspaces'), + }, + ...renderChildren([ + { + name: 'index', + path: '', + component: WorkspacesList, + meta: { + breadcrumb: s__('Workspaces|Workspace List'), + }, + }, + ]), + }, + ]; + + return new VueRouter({ + base, + mode: 'history', + routes, + }); +} diff --git a/ee/app/controllers/groups/workspaces_controller.rb b/ee/app/controllers/groups/workspaces_controller.rb new file mode 100644 index 00000000000000..a19f8d910fae8c --- /dev/null +++ b/ee/app/controllers/groups/workspaces_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Groups + class WorkspacesController < Groups::ApplicationController + before_action :remote_development_enabled!, only: [:index] + before_action :authorize_remote_development!, only: [:index] + + feature_category :remote_development + urgency :low + + def index; end + + private + + def remote_development_enabled! + render_404 unless Feature.enabled?(:remote_dev) + end + + def authorize_remote_development! + render_404 unless can?(current_user, :read_workspace, group) + end + end +end diff --git a/ee/app/models/gitlab_subscriptions/features.rb b/ee/app/models/gitlab_subscriptions/features.rb index 4020b1dee43b1a..1abcdce1e7305b 100644 --- a/ee/app/models/gitlab_subscriptions/features.rb +++ b/ee/app/models/gitlab_subscriptions/features.rb @@ -217,6 +217,7 @@ class Features quality_management related_epics release_evidence_test_artifacts + remote_development report_approver_rules required_ci_templates requirements diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index 3256cb5695981d..4bcafc18d23979 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -16,6 +16,7 @@ module GroupPolicy condition(:epics_available, scope: :subject) { @subject.feature_available?(:epics) } condition(:iterations_available, scope: :subject) { @subject.feature_available?(:iterations) } + condition(:remote_development_available, scope: :subject) { @subject.feature_available?(:remote_development) } condition(:subepics_available, scope: :subject) { @subject.feature_available?(:subepics) } condition(:external_audit_events_available, scope: :subject) do @subject.feature_available?(:external_audit_events) @@ -336,6 +337,11 @@ module GroupPolicy enable :admin_iteration_cadence end + rule { can?(:read_group) & remote_development_available }.policy do + enable :read_workspace + enable :group_workspaces + end + rule { (automation_bot | reporter) & iterations_available }.policy do enable :rollover_issues end diff --git a/ee/app/views/groups/workspaces/index.html.haml b/ee/app/views/groups/workspaces/index.html.haml new file mode 100644 index 00000000000000..12f5fb18862357 --- /dev/null +++ b/ee/app/views/groups/workspaces/index.html.haml @@ -0,0 +1,6 @@ +- page_title s_('Workspaces') +#js-workspaces{ data: { + group_full_path: @group.full_path, + workspaces_list_path: group_workspaces_path(@group), + } +} diff --git a/ee/config/feature_flags/development/remote_dev.yml b/ee/config/feature_flags/development/remote_dev.yml new file mode 100644 index 00000000000000..beaef2bf226741 --- /dev/null +++ b/ee/config/feature_flags/development/remote_dev.yml @@ -0,0 +1,8 @@ +--- +name: remote_dev +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112397 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391543 +milestone: '15.10' +type: development +group: group::editor +default_enabled: false diff --git a/ee/config/routes/group.rb b/ee/config/routes/group.rb index 7d7c494cce1976..a7d1ec14ada4f2 100644 --- a/ee/config/routes/group.rb +++ b/ee/config/routes/group.rb @@ -151,6 +151,8 @@ resources :iterations, only: [:index, :new, :edit, :show], constraints: { id: /\d+/ }, controller: :iteration_cadences, action: :index end + resources :workspaces, path: 'workspaces(/*vueroute)' + resources :issues, only: [] do collection do post :bulk_update diff --git a/ee/lib/sidebars/groups/menus/workspaces_menu.rb b/ee/lib/sidebars/groups/menus/workspaces_menu.rb new file mode 100644 index 00000000000000..4b08c2ecaed55c --- /dev/null +++ b/ee/lib/sidebars/groups/menus/workspaces_menu.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Sidebars + module Groups + module Menus + class WorkspacesMenu < ::Sidebars::Menu + override :configure_menu_items + def configure_menu_items + add_item(workspace_list_menu_item) + + true + end + + def remote_development_menu_enabled? + ::Feature.enabled?(:remote_dev) && + can?(context.current_user, :read_workspace, context.group) + end + + override :title + def title + _('Workspaces') + end + + override :sprite_icon + def sprite_icon + 'cloud-gear' + end + + override :render? + def render? + return false unless remote_development_menu_enabled? + + true + end + + override :extra_container_html_options + def extra_container_html_options + { + class: 'shortcuts-workspaces' + } + end + + override :active_routes + def active_routes + { controller: :workspaces } + end + + private + + def workspace_list_menu_item + ::Sidebars::MenuItem.new( + title: _('List'), + link: group_workspaces_path(context.group), + active_routes: { path: 'workspaces#index' }, + container_html_options: { class: 'home' }, + item_id: :workspace_list + ) + end + end + end + end +end diff --git a/ee/spec/controllers/groups/workspaces_controller_spec.rb b/ee/spec/controllers/groups/workspaces_controller_spec.rb new file mode 100644 index 00000000000000..229b04eb195c1b --- /dev/null +++ b/ee/spec/controllers/groups/workspaces_controller_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::WorkspacesController, feature_category: :remote_development do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + + before do + group.add_owner(user) + sign_in(user) + end + + shared_examples 'remote development feature flag' do |feature_flag_enabled, expected_status| + before do + stub_licensed_features(remote_development: true) + stub_feature_flags(remote_dev: feature_flag_enabled) + end + + describe 'GET #index' do + it 'responds with the expected status' do + get :index, params: { group_id: group.to_param } + + expect(response).to have_gitlab_http_status(expected_status) + end + end + end + + context 'with remote development feature flag' do + it_behaves_like 'remote development feature flag', true, :ok + it_behaves_like 'remote development feature flag', false, :not_found + end + + context 'with remote development not licensed' do + before do + stub_licensed_features(remote_development: false) + end + + describe 'GET #index' do + it 'responds with the not found status' do + get :index, params: { group_id: group.to_param } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/ee/spec/features/groups/navbar_spec.rb b/ee/spec/features/groups/navbar_spec.rb index 30d38bd5333a39..4d64ad5ec7a1f8 100644 --- a/ee/spec/features/groups/navbar_spec.rb +++ b/ee/spec/features/groups/navbar_spec.rb @@ -18,6 +18,7 @@ stub_group_wikis(false) stub_feature_flags(harbor_registry_integration: false) stub_feature_flags(observability_group_tab: false) + stub_feature_flags(remote_dev: false) sign_in(user) insert_package_nav(_('Kubernetes')) @@ -177,6 +178,26 @@ it_behaves_like 'verified navigation bar' end + context 'when workspaces are available' do + before do + stub_feature_flags(remote_dev: true) + stub_licensed_features(remote_development: true) + + insert_after_nav_item( + _('Analytics'), + new_nav_item: { + nav_item: _('Workspaces'), + nav_sub_items: [ + _('List') + ] + } + ) + visit group_path(group) + end + + it_behaves_like 'verified navigation bar' + end + context 'when harbor registry is available' do let(:harbor_integration) { create(:harbor_integration, group: group, project: nil) } @@ -212,6 +233,7 @@ stub_group_wikis(false) stub_feature_flags(harbor_registry_integration: false) stub_feature_flags(observability_group_tab: false) + stub_feature_flags(remote_dev: false) stub_licensed_features(domain_verification: true) sign_in(user) insert_package_nav(_('Kubernetes')) diff --git a/ee/spec/frontend/remote_development/workspaces/components/empty_state_spec.js b/ee/spec/frontend/remote_development/workspaces/components/empty_state_spec.js new file mode 100644 index 00000000000000..7087d804f9a92d --- /dev/null +++ b/ee/spec/frontend/remote_development/workspaces/components/empty_state_spec.js @@ -0,0 +1,32 @@ +import { GlEmptyState } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import EmptyState, { i18n } from 'ee/remote_development/workspaces/components/empty_state.vue'; + +describe('ee/app/assets/javascripts/remote_development/workspaces/components/empty_state.vue', () => { + let wrapper; + + const QUICK_START_LINK = 'workspaces/quick_start/index.md'; + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + + const createComponent = () => { + wrapper = shallowMount(EmptyState, { + stubs: { + GlEmptyState, + }, + }); + }; + + describe('when no workspaces exist', () => { + it('should render empty workspace state', () => { + createComponent(); + + expect(findEmptyState().props()).toMatchObject({ + title: i18n.title, + description: i18n.description, + primaryButtonText: i18n.primaryButtonText, + primaryButtonLink: helpPagePath(QUICK_START_LINK), + }); + }); + }); +}); diff --git a/ee/spec/frontend/remote_development/workspaces/list_spec.js b/ee/spec/frontend/remote_development/workspaces/list_spec.js new file mode 100644 index 00000000000000..ae62bd75823f9d --- /dev/null +++ b/ee/spec/frontend/remote_development/workspaces/list_spec.js @@ -0,0 +1,21 @@ +import { shallowMount } from '@vue/test-utils'; +import WorkspacesList from 'ee/remote_development/workspaces/components/list.vue'; +import EmptyState from 'ee/remote_development/workspaces/components/empty_state.vue'; + +describe('ee/app/assets/javascripts/remote_development/workspaces/components/list.vue', () => { + let wrapper; + + const findWorkspacesList = () => wrapper.findComponent(WorkspacesList); + const findEmptyState = () => findWorkspacesList().findComponent(EmptyState); + + const createComponent = () => { + wrapper = shallowMount(WorkspacesList, {}); + }; + + describe('when no workspaces exist', () => { + it('should render empty workspace state', () => { + createComponent(); + expect(findEmptyState().exists()).toBe(true); + }); + }); +}); diff --git a/ee/spec/frontend/remote_development/workspaces/router/index_spec.js b/ee/spec/frontend/remote_development/workspaces/router/index_spec.js new file mode 100644 index 00000000000000..d13777d618d7a6 --- /dev/null +++ b/ee/spec/frontend/remote_development/workspaces/router/index_spec.js @@ -0,0 +1,31 @@ +import { mount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import App from 'ee/remote_development/workspaces/components/app.vue'; +import WorkspacesList from 'ee/remote_development/workspaces/components/list.vue'; +import createRouter from 'ee/remote_development/workspaces/router/index'; + +Vue.use(VueRouter); + +describe('ee/app/assets/javascripts/remote_development/workspaces/router/index.js', () => { + let router; + + beforeEach(() => { + router = createRouter('/'); + }); + + afterEach(() => { + window.location.hash = ''; + }); + + // TODO: We leave the it.each here to extend this file as we add more routes in later MRs + it.each([['/']])('renders WorkspacesList on route %p', async (route) => { + const wrapper = mount(App, { + router, + }); + + await router.push(route); + + expect(wrapper.findComponent(WorkspacesList).exists()).toBe(true); + }); +}); diff --git a/ee/spec/lib/sidebars/groups/menus/workspaces_menu_spec.rb b/ee/spec/lib/sidebars/groups/menus/workspaces_menu_spec.rb new file mode 100644 index 00000000000000..e344d15e6e5ea0 --- /dev/null +++ b/ee/spec/lib/sidebars/groups/menus/workspaces_menu_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Groups::Menus::WorkspacesMenu, feature_category: :remote_development do + let_it_be(:owner) { create(:user) } + let_it_be_with_refind(:group) do + create(:group, :private).tap do |g| + g.add_owner(owner) + end + end + + let(:user) { owner } + let(:show_group_discover_security) { false } + let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) } + let(:menu) { described_class.new(context) } + + describe '#link' do + subject { menu.link } + + context 'when menu has menu items' do + it 'returns first visible menu item link' do + expect(subject).to eq menu.renderable_items.first.link + end + end + end + + describe '#title' do + subject { menu.title } + + specify do + is_expected.to eq 'Workspaces' + end + end + + describe '#render?' do + let(:remote_development_enabled) { true } + + subject { menu.render? } + + before do + stub_feature_flags(remote_dev: remote_development_enabled) + stub_licensed_features(remote_development: remote_development_enabled) + end + + context 'when user can access group workspaces' do + specify { is_expected.to be true } + + context 'when feature is not enabled' do + let(:remote_development_enabled) { false } + + specify { is_expected.to be false } + end + end + end +end diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index 2c67b07d56e3de..5f4a479857ad85 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -168,6 +168,40 @@ end end + context 'when remote development feature is disabled' do + let(:current_user) { owner } + + before do + stub_licensed_features(remote_development: false) + end + + it { is_expected.to be_disallowed(:read_workspace, :group_workspaces) } + end + + context 'when remote development feature is enabled' do + before do + stub_licensed_features(remote_development: true) + end + + context 'when user is a developer' do + let(:current_user) { developer } + + it { is_expected.to be_allowed(:read_workspace, :group_workspaces) } + end + + context 'when user is a guest' do + let(:current_user) { guest } + + it { is_expected.to be_allowed(:read_workspace, :group_workspaces) } + end + + context 'when user is logged out' do + let(:current_user) { nil } + + it { is_expected.to be_disallowed(:read_workspace, :group_workspaces) } + end + end + context 'when cluster deployments is available' do let(:current_user) { maintainer } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0503067e8cf4bf..fc1e891d60dbc9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -49803,6 +49803,24 @@ msgstr "" msgid "WorkItem|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options." msgstr "" +msgid "Workspaces" +msgstr "" + +msgid "Workspaces|Develop anywhere" +msgstr "" + +msgid "Workspaces|Get started with GitLab Workspaces" +msgstr "" + +msgid "Workspaces|GitLab Workspaces is a powerful collaborative platform that provides a comprehensive set of tools for software development teams to manage their entire development lifecycle." +msgstr "" + +msgid "Workspaces|Workspace List" +msgstr "" + +msgid "Workspaces|Workspaces" +msgstr "" + msgid "Would you like to create a new branch?" msgstr "" -- GitLab From 1d56f8e16a3ed52aadee822eaea21989c22a6404 Mon Sep 17 00:00:00 2001 From: David O'Regan Date: Wed, 8 Mar 2023 20:03:58 +1100 Subject: [PATCH 2/8] Update Workspaces entry point Move Workspace entry to a user level rather than the Group level. --- config/routes.rb | 1 + .../pages/groups/workspaces/index.js | 3 - .../pages/remote_development/index.js | 3 + .../{workspaces => }/components/app.vue | 0 .../components/empty_state.vue | 0 .../{workspaces => }/components/list.vue | 0 .../{workspaces => }/index.js | 3 +- .../{workspaces => }/router/index.js | 0 .../workspaces_controller.rb | 6 +- ee/app/policies/ee/global_policy.rb | 8 +++ ee/app/policies/ee/group_policy.rb | 6 -- .../views/groups/workspaces/index.html.haml | 6 -- .../workspaces/index.html.haml | 8 +++ ee/config/routes/group.rb | 2 - ee/config/routes/remote_development.rb | 5 ++ ee/lib/ee/sidebars/your_work/panel.rb | 5 ++ .../sidebars/groups/menus/workspaces_menu.rb | 62 ------------------- .../your_work/menus/workspaces_menu.rb | 35 +++++++++++ .../workspaces_controller_spec.rb | 8 +-- ee/spec/features/groups/navbar_spec.rb | 22 ------- .../components/empty_state_spec.js | 4 +- .../{workspaces => components}/list_spec.js | 6 +- .../remote_development/router/index_spec.js | 32 ++++++++++ .../workspaces/router/index_spec.js | 31 ---------- .../your_work/menus/workspaces_menu_spec.rb | 21 +++++++ .../lib/ee/sidebars/your_work/panel_spec.rb | 4 ++ .../groups/menus/workspaces_menu_spec.rb | 56 ----------------- ee/spec/policies/global_policy_spec.rb | 24 +++++++ ee/spec/policies/group_policy_spec.rb | 34 ---------- 29 files changed, 158 insertions(+), 237 deletions(-) delete mode 100644 ee/app/assets/javascripts/pages/groups/workspaces/index.js create mode 100644 ee/app/assets/javascripts/pages/remote_development/index.js rename ee/app/assets/javascripts/remote_development/{workspaces => }/components/app.vue (100%) rename ee/app/assets/javascripts/remote_development/{workspaces => }/components/empty_state.vue (100%) rename ee/app/assets/javascripts/remote_development/{workspaces => }/components/list.vue (100%) rename ee/app/assets/javascripts/remote_development/{workspaces => }/index.js (90%) rename ee/app/assets/javascripts/remote_development/{workspaces => }/router/index.js (100%) rename ee/app/controllers/{groups => remote_development}/workspaces_controller.rb (74%) delete mode 100644 ee/app/views/groups/workspaces/index.html.haml create mode 100644 ee/app/views/remote_development/workspaces/index.html.haml create mode 100644 ee/config/routes/remote_development.rb delete mode 100644 ee/lib/sidebars/groups/menus/workspaces_menu.rb create mode 100644 ee/lib/sidebars/your_work/menus/workspaces_menu.rb rename ee/spec/controllers/{groups => remote_development}/workspaces_controller_spec.rb (79%) rename ee/spec/frontend/remote_development/{workspaces => }/components/empty_state_spec.js (80%) rename ee/spec/frontend/remote_development/{workspaces => components}/list_spec.js (65%) create mode 100644 ee/spec/frontend/remote_development/router/index_spec.js delete mode 100644 ee/spec/frontend/remote_development/workspaces/router/index_spec.js create mode 100644 ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb delete mode 100644 ee/spec/lib/sidebars/groups/menus/workspaces_menu_spec.rb diff --git a/config/routes.rb b/config/routes.rb index 589d44c3de68c4..129701461ea15c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -160,6 +160,7 @@ end draw :operations + draw :remote_development draw :jira_connect Gitlab.ee do diff --git a/ee/app/assets/javascripts/pages/groups/workspaces/index.js b/ee/app/assets/javascripts/pages/groups/workspaces/index.js deleted file mode 100644 index 9b51d09e6805c3..00000000000000 --- a/ee/app/assets/javascripts/pages/groups/workspaces/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import { initWorkspacesApp } from 'ee/remote_development/workspaces'; - -initWorkspacesApp(); diff --git a/ee/app/assets/javascripts/pages/remote_development/index.js b/ee/app/assets/javascripts/pages/remote_development/index.js new file mode 100644 index 00000000000000..fb48c46e3dfe78 --- /dev/null +++ b/ee/app/assets/javascripts/pages/remote_development/index.js @@ -0,0 +1,3 @@ +import { initWorkspacesApp } from 'ee/remote_development'; + +initWorkspacesApp(); diff --git a/ee/app/assets/javascripts/remote_development/workspaces/components/app.vue b/ee/app/assets/javascripts/remote_development/components/app.vue similarity index 100% rename from ee/app/assets/javascripts/remote_development/workspaces/components/app.vue rename to ee/app/assets/javascripts/remote_development/components/app.vue diff --git a/ee/app/assets/javascripts/remote_development/workspaces/components/empty_state.vue b/ee/app/assets/javascripts/remote_development/components/empty_state.vue similarity index 100% rename from ee/app/assets/javascripts/remote_development/workspaces/components/empty_state.vue rename to ee/app/assets/javascripts/remote_development/components/empty_state.vue diff --git a/ee/app/assets/javascripts/remote_development/workspaces/components/list.vue b/ee/app/assets/javascripts/remote_development/components/list.vue similarity index 100% rename from ee/app/assets/javascripts/remote_development/workspaces/components/list.vue rename to ee/app/assets/javascripts/remote_development/components/list.vue diff --git a/ee/app/assets/javascripts/remote_development/workspaces/index.js b/ee/app/assets/javascripts/remote_development/index.js similarity index 90% rename from ee/app/assets/javascripts/remote_development/workspaces/index.js rename to ee/app/assets/javascripts/remote_development/index.js index 8df6f5adcf03a6..730d8b48cc2c71 100644 --- a/ee/app/assets/javascripts/remote_development/workspaces/index.js +++ b/ee/app/assets/javascripts/remote_development/index.js @@ -18,7 +18,7 @@ const initWorkspacesApp = () => { return null; } - const { groupFullPath, workspacesListPath } = el.dataset; + const { workspacesListPath } = el.dataset; const router = createRouter({ base: workspacesListPath, }); @@ -29,7 +29,6 @@ const initWorkspacesApp = () => { router, apolloProvider, provide: { - groupFullPath, workspacesListPath, }, render: (createElement) => createElement(App), diff --git a/ee/app/assets/javascripts/remote_development/workspaces/router/index.js b/ee/app/assets/javascripts/remote_development/router/index.js similarity index 100% rename from ee/app/assets/javascripts/remote_development/workspaces/router/index.js rename to ee/app/assets/javascripts/remote_development/router/index.js diff --git a/ee/app/controllers/groups/workspaces_controller.rb b/ee/app/controllers/remote_development/workspaces_controller.rb similarity index 74% rename from ee/app/controllers/groups/workspaces_controller.rb rename to ee/app/controllers/remote_development/workspaces_controller.rb index a19f8d910fae8c..a57eef41c9e95e 100644 --- a/ee/app/controllers/groups/workspaces_controller.rb +++ b/ee/app/controllers/remote_development/workspaces_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -module Groups - class WorkspacesController < Groups::ApplicationController +module RemoteDevelopment + class WorkspacesController < ApplicationController before_action :remote_development_enabled!, only: [:index] before_action :authorize_remote_development!, only: [:index] @@ -17,7 +17,7 @@ def remote_development_enabled! end def authorize_remote_development! - render_404 unless can?(current_user, :read_workspace, group) + render_404 unless can?(current_user, :read_workspace) end end end diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb index 44310703966453..273ba068a34a73 100644 --- a/ee/app/policies/ee/global_policy.rb +++ b/ee/app/policies/ee/global_policy.rb @@ -9,6 +9,10 @@ module GlobalPolicy License.feature_available?(:operations_dashboard) end + condition(:remote_development_available) do + License.feature_available?(:remote_development) + end + condition(:pages_size_limit_available) do License.feature_available?(:pages_size_limit) end @@ -43,6 +47,10 @@ module GlobalPolicy rule { ~anonymous & operations_dashboard_available }.enable :read_operations_dashboard + rule { ~anonymous & remote_development_available }.policy do + enable :read_workspace + end + rule { admin & instance_devops_adoption_available }.policy do enable :manage_devops_adoption_namespaces enable :view_instance_devops_adoption diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index 4bcafc18d23979..3256cb5695981d 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -16,7 +16,6 @@ module GroupPolicy condition(:epics_available, scope: :subject) { @subject.feature_available?(:epics) } condition(:iterations_available, scope: :subject) { @subject.feature_available?(:iterations) } - condition(:remote_development_available, scope: :subject) { @subject.feature_available?(:remote_development) } condition(:subepics_available, scope: :subject) { @subject.feature_available?(:subepics) } condition(:external_audit_events_available, scope: :subject) do @subject.feature_available?(:external_audit_events) @@ -337,11 +336,6 @@ module GroupPolicy enable :admin_iteration_cadence end - rule { can?(:read_group) & remote_development_available }.policy do - enable :read_workspace - enable :group_workspaces - end - rule { (automation_bot | reporter) & iterations_available }.policy do enable :rollover_issues end diff --git a/ee/app/views/groups/workspaces/index.html.haml b/ee/app/views/groups/workspaces/index.html.haml deleted file mode 100644 index 12f5fb18862357..00000000000000 --- a/ee/app/views/groups/workspaces/index.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -- page_title s_('Workspaces') -#js-workspaces{ data: { - group_full_path: @group.full_path, - workspaces_list_path: group_workspaces_path(@group), - } -} diff --git a/ee/app/views/remote_development/workspaces/index.html.haml b/ee/app/views/remote_development/workspaces/index.html.haml new file mode 100644 index 00000000000000..6bd2bdbd102fd3 --- /dev/null +++ b/ee/app/views/remote_development/workspaces/index.html.haml @@ -0,0 +1,8 @@ +- page_title s_('Workspaces') +- nav 'your_work' +- @left_sidebar = true + +#js-workspaces{ data: { + workspaces_list_path: remote_development_workspaces_path, + } +} diff --git a/ee/config/routes/group.rb b/ee/config/routes/group.rb index a7d1ec14ada4f2..7d7c494cce1976 100644 --- a/ee/config/routes/group.rb +++ b/ee/config/routes/group.rb @@ -151,8 +151,6 @@ resources :iterations, only: [:index, :new, :edit, :show], constraints: { id: /\d+/ }, controller: :iteration_cadences, action: :index end - resources :workspaces, path: 'workspaces(/*vueroute)' - resources :issues, only: [] do collection do post :bulk_update diff --git a/ee/config/routes/remote_development.rb b/ee/config/routes/remote_development.rb new file mode 100644 index 00000000000000..7f3c9626cb62b0 --- /dev/null +++ b/ee/config/routes/remote_development.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +namespace :remote_development do + resources :workspaces, path: 'workspaces(/*vueroute)' +end diff --git a/ee/lib/ee/sidebars/your_work/panel.rb b/ee/lib/ee/sidebars/your_work/panel.rb index 487b9a4c4d1bcc..3d21bb798e127a 100644 --- a/ee/lib/ee/sidebars/your_work/panel.rb +++ b/ee/lib/ee/sidebars/your_work/panel.rb @@ -10,6 +10,7 @@ module Panel def configure_menus super + add_menu(workspaces_menu) add_menu(environments_dashboard_menu) add_menu(operations_dashboard_menu) add_menu(security_dashboard_menu) @@ -19,6 +20,10 @@ def configure_menus private + def workspaces_menu + ::Sidebars::YourWork::Menus::WorkspacesMenu.new(context) + end + def environments_dashboard_menu ::Sidebars::YourWork::Menus::EnvironmentsDashboardMenu.new(context) end diff --git a/ee/lib/sidebars/groups/menus/workspaces_menu.rb b/ee/lib/sidebars/groups/menus/workspaces_menu.rb deleted file mode 100644 index 4b08c2ecaed55c..00000000000000 --- a/ee/lib/sidebars/groups/menus/workspaces_menu.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -module Sidebars - module Groups - module Menus - class WorkspacesMenu < ::Sidebars::Menu - override :configure_menu_items - def configure_menu_items - add_item(workspace_list_menu_item) - - true - end - - def remote_development_menu_enabled? - ::Feature.enabled?(:remote_dev) && - can?(context.current_user, :read_workspace, context.group) - end - - override :title - def title - _('Workspaces') - end - - override :sprite_icon - def sprite_icon - 'cloud-gear' - end - - override :render? - def render? - return false unless remote_development_menu_enabled? - - true - end - - override :extra_container_html_options - def extra_container_html_options - { - class: 'shortcuts-workspaces' - } - end - - override :active_routes - def active_routes - { controller: :workspaces } - end - - private - - def workspace_list_menu_item - ::Sidebars::MenuItem.new( - title: _('List'), - link: group_workspaces_path(context.group), - active_routes: { path: 'workspaces#index' }, - container_html_options: { class: 'home' }, - item_id: :workspace_list - ) - end - end - end - end -end diff --git a/ee/lib/sidebars/your_work/menus/workspaces_menu.rb b/ee/lib/sidebars/your_work/menus/workspaces_menu.rb new file mode 100644 index 00000000000000..a7ef44874cabc6 --- /dev/null +++ b/ee/lib/sidebars/your_work/menus/workspaces_menu.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Sidebars + module YourWork + module Menus + class WorkspacesMenu < ::Sidebars::Menu + override :link + def link + remote_development_workspaces_path + end + + override :title + def title + _('Workspaces') + end + + override :sprite_icon + def sprite_icon + 'cloud-gear' + end + + override :render? + def render? + ::Feature.enabled?(:remote_dev) && + can?(context.current_user, :read_workspace) + end + + override :active_routes + def active_routes + { path: 'remote_development/workspaces#index' } + end + end + end + end +end diff --git a/ee/spec/controllers/groups/workspaces_controller_spec.rb b/ee/spec/controllers/remote_development/workspaces_controller_spec.rb similarity index 79% rename from ee/spec/controllers/groups/workspaces_controller_spec.rb rename to ee/spec/controllers/remote_development/workspaces_controller_spec.rb index 229b04eb195c1b..9cd6fec3cb7d5e 100644 --- a/ee/spec/controllers/groups/workspaces_controller_spec.rb +++ b/ee/spec/controllers/remote_development/workspaces_controller_spec.rb @@ -2,12 +2,10 @@ require 'spec_helper' -RSpec.describe Groups::WorkspacesController, feature_category: :remote_development do +RSpec.describe RemoteDevelopment::WorkspacesController, feature_category: :remote_development do let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group) } before do - group.add_owner(user) sign_in(user) end @@ -19,7 +17,7 @@ describe 'GET #index' do it 'responds with the expected status' do - get :index, params: { group_id: group.to_param } + get :index expect(response).to have_gitlab_http_status(expected_status) end @@ -38,7 +36,7 @@ describe 'GET #index' do it 'responds with the not found status' do - get :index, params: { group_id: group.to_param } + get :index expect(response).to have_gitlab_http_status(:not_found) end diff --git a/ee/spec/features/groups/navbar_spec.rb b/ee/spec/features/groups/navbar_spec.rb index 4d64ad5ec7a1f8..30d38bd5333a39 100644 --- a/ee/spec/features/groups/navbar_spec.rb +++ b/ee/spec/features/groups/navbar_spec.rb @@ -18,7 +18,6 @@ stub_group_wikis(false) stub_feature_flags(harbor_registry_integration: false) stub_feature_flags(observability_group_tab: false) - stub_feature_flags(remote_dev: false) sign_in(user) insert_package_nav(_('Kubernetes')) @@ -178,26 +177,6 @@ it_behaves_like 'verified navigation bar' end - context 'when workspaces are available' do - before do - stub_feature_flags(remote_dev: true) - stub_licensed_features(remote_development: true) - - insert_after_nav_item( - _('Analytics'), - new_nav_item: { - nav_item: _('Workspaces'), - nav_sub_items: [ - _('List') - ] - } - ) - visit group_path(group) - end - - it_behaves_like 'verified navigation bar' - end - context 'when harbor registry is available' do let(:harbor_integration) { create(:harbor_integration, group: group, project: nil) } @@ -233,7 +212,6 @@ stub_group_wikis(false) stub_feature_flags(harbor_registry_integration: false) stub_feature_flags(observability_group_tab: false) - stub_feature_flags(remote_dev: false) stub_licensed_features(domain_verification: true) sign_in(user) insert_package_nav(_('Kubernetes')) diff --git a/ee/spec/frontend/remote_development/workspaces/components/empty_state_spec.js b/ee/spec/frontend/remote_development/components/empty_state_spec.js similarity index 80% rename from ee/spec/frontend/remote_development/workspaces/components/empty_state_spec.js rename to ee/spec/frontend/remote_development/components/empty_state_spec.js index 7087d804f9a92d..fb01c8a7b02c23 100644 --- a/ee/spec/frontend/remote_development/workspaces/components/empty_state_spec.js +++ b/ee/spec/frontend/remote_development/components/empty_state_spec.js @@ -1,9 +1,9 @@ import { GlEmptyState } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { helpPagePath } from '~/helpers/help_page_helper'; -import EmptyState, { i18n } from 'ee/remote_development/workspaces/components/empty_state.vue'; +import EmptyState, { i18n } from 'ee/remote_development/components/empty_state.vue'; -describe('ee/app/assets/javascripts/remote_development/workspaces/components/empty_state.vue', () => { +describe('ee/app/assets/javascripts/remote_development/components/empty_state.vue', () => { let wrapper; const QUICK_START_LINK = 'workspaces/quick_start/index.md'; diff --git a/ee/spec/frontend/remote_development/workspaces/list_spec.js b/ee/spec/frontend/remote_development/components/list_spec.js similarity index 65% rename from ee/spec/frontend/remote_development/workspaces/list_spec.js rename to ee/spec/frontend/remote_development/components/list_spec.js index ae62bd75823f9d..9b91dfe0ca8eb3 100644 --- a/ee/spec/frontend/remote_development/workspaces/list_spec.js +++ b/ee/spec/frontend/remote_development/components/list_spec.js @@ -1,8 +1,8 @@ import { shallowMount } from '@vue/test-utils'; -import WorkspacesList from 'ee/remote_development/workspaces/components/list.vue'; -import EmptyState from 'ee/remote_development/workspaces/components/empty_state.vue'; +import WorkspacesList from 'ee/remote_development/components/list.vue'; +import EmptyState from 'ee/remote_development/components/empty_state.vue'; -describe('ee/app/assets/javascripts/remote_development/workspaces/components/list.vue', () => { +describe('ee/app/assets/javascripts/remote_development/components/list.vue', () => { let wrapper; const findWorkspacesList = () => wrapper.findComponent(WorkspacesList); diff --git a/ee/spec/frontend/remote_development/router/index_spec.js b/ee/spec/frontend/remote_development/router/index_spec.js new file mode 100644 index 00000000000000..1c79819db25f68 --- /dev/null +++ b/ee/spec/frontend/remote_development/router/index_spec.js @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import App from 'ee/remote_development/components/app.vue'; +import WorkspacesList from 'ee/remote_development/components/list.vue'; +import createRouter from 'ee/remote_development/router/index'; + +Vue.use(VueRouter); + +describe('ee/app/assets/javascripts/remote_development/router/index.js', () => { + let router; + + beforeEach(() => { + router = createRouter('/'); + jest.spyOn(router, 'push').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + window.location.hash = ''; + }); + + it('renders WorkspacesList on route /', async () => { + const wrapper = mount(App, { + router, + }); + + await router.push('/'); + + expect(wrapper.findComponent(WorkspacesList).exists()).toBe(true); + }); +}); diff --git a/ee/spec/frontend/remote_development/workspaces/router/index_spec.js b/ee/spec/frontend/remote_development/workspaces/router/index_spec.js deleted file mode 100644 index d13777d618d7a6..00000000000000 --- a/ee/spec/frontend/remote_development/workspaces/router/index_spec.js +++ /dev/null @@ -1,31 +0,0 @@ -import { mount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueRouter from 'vue-router'; -import App from 'ee/remote_development/workspaces/components/app.vue'; -import WorkspacesList from 'ee/remote_development/workspaces/components/list.vue'; -import createRouter from 'ee/remote_development/workspaces/router/index'; - -Vue.use(VueRouter); - -describe('ee/app/assets/javascripts/remote_development/workspaces/router/index.js', () => { - let router; - - beforeEach(() => { - router = createRouter('/'); - }); - - afterEach(() => { - window.location.hash = ''; - }); - - // TODO: We leave the it.each here to extend this file as we add more routes in later MRs - it.each([['/']])('renders WorkspacesList on route %p', async (route) => { - const wrapper = mount(App, { - router, - }); - - await router.push(route); - - expect(wrapper.findComponent(WorkspacesList).exists()).toBe(true); - }); -}); diff --git a/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb b/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb new file mode 100644 index 00000000000000..b00236bdd8a4ce --- /dev/null +++ b/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::YourWork::Menus::WorkspacesMenu, feature_category: :remote_development do + it_behaves_like 'Top-Level menu item', + is_super_sidebar: false, + access_check: :read_workspace, + link: '/-/remote_development/workspaces', + title: _('Workspaces'), + icon: 'cloud-gear', + active_route: 'remote_development/workspaces#index' + + it_behaves_like 'Top-Level menu item', + is_super_sidebar: true, + access_check: :read_workspace, + link: '/-/remote_development/workspaces', + title: _('Workspaces'), + icon: 'cloud-gear', + active_route: 'remote_development/workspaces#index' +end diff --git a/ee/spec/lib/ee/sidebars/your_work/panel_spec.rb b/ee/spec/lib/ee/sidebars/your_work/panel_spec.rb index e583c189d3da7d..20dec4ab4da443 100644 --- a/ee/spec/lib/ee/sidebars/your_work/panel_spec.rb +++ b/ee/spec/lib/ee/sidebars/your_work/panel_spec.rb @@ -9,6 +9,10 @@ subject(:panel) { described_class.new(context) } + it 'renders Workspaces' do + expect(contains_menu?(::Sidebars::YourWork::Menus::WorkspacesMenu)).to be(true) + end + it 'renders Environments dashboard' do expect(contains_menu?(::Sidebars::YourWork::Menus::EnvironmentsDashboardMenu)).to be(true) end diff --git a/ee/spec/lib/sidebars/groups/menus/workspaces_menu_spec.rb b/ee/spec/lib/sidebars/groups/menus/workspaces_menu_spec.rb deleted file mode 100644 index e344d15e6e5ea0..00000000000000 --- a/ee/spec/lib/sidebars/groups/menus/workspaces_menu_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Groups::Menus::WorkspacesMenu, feature_category: :remote_development do - let_it_be(:owner) { create(:user) } - let_it_be_with_refind(:group) do - create(:group, :private).tap do |g| - g.add_owner(owner) - end - end - - let(:user) { owner } - let(:show_group_discover_security) { false } - let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) } - let(:menu) { described_class.new(context) } - - describe '#link' do - subject { menu.link } - - context 'when menu has menu items' do - it 'returns first visible menu item link' do - expect(subject).to eq menu.renderable_items.first.link - end - end - end - - describe '#title' do - subject { menu.title } - - specify do - is_expected.to eq 'Workspaces' - end - end - - describe '#render?' do - let(:remote_development_enabled) { true } - - subject { menu.render? } - - before do - stub_feature_flags(remote_dev: remote_development_enabled) - stub_licensed_features(remote_development: remote_development_enabled) - end - - context 'when user can access group workspaces' do - specify { is_expected.to be true } - - context 'when feature is not enabled' do - let(:remote_development_enabled) { false } - - specify { is_expected.to be false } - end - end - end -end diff --git a/ee/spec/policies/global_policy_spec.rb b/ee/spec/policies/global_policy_spec.rb index 87ad80b16de667..3ca1ad6d05048c 100644 --- a/ee/spec/policies/global_policy_spec.rb +++ b/ee/spec/policies/global_policy_spec.rb @@ -35,6 +35,30 @@ end end + describe 'reading workspaces' do + context 'when licensed' do + before do + stub_licensed_features(remote_development: true) + end + + it { is_expected.to be_allowed(:read_workspace) } + + context 'and the user is not logged in' do + let(:current_user) { nil } + + it { is_expected.to be_disallowed(:read_workspace) } + end + end + + context 'when unlicensed' do + before do + stub_licensed_features(remote_development: false) + end + + it { is_expected.to be_disallowed(:read_workspace) } + end + end + it { is_expected.to be_disallowed(:read_licenses) } it { is_expected.to be_disallowed(:destroy_licenses) } it { is_expected.to be_disallowed(:read_all_geo) } diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index 5f4a479857ad85..2c67b07d56e3de 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -168,40 +168,6 @@ end end - context 'when remote development feature is disabled' do - let(:current_user) { owner } - - before do - stub_licensed_features(remote_development: false) - end - - it { is_expected.to be_disallowed(:read_workspace, :group_workspaces) } - end - - context 'when remote development feature is enabled' do - before do - stub_licensed_features(remote_development: true) - end - - context 'when user is a developer' do - let(:current_user) { developer } - - it { is_expected.to be_allowed(:read_workspace, :group_workspaces) } - end - - context 'when user is a guest' do - let(:current_user) { guest } - - it { is_expected.to be_allowed(:read_workspace, :group_workspaces) } - end - - context 'when user is logged out' do - let(:current_user) { nil } - - it { is_expected.to be_disallowed(:read_workspace, :group_workspaces) } - end - end - context 'when cluster deployments is available' do let(:current_user) { maintainer } -- GitLab From 4c097180a299758802cb3d3777d9271e4533fb79 Mon Sep 17 00:00:00 2001 From: David O'Regan Date: Mon, 13 Mar 2023 14:04:32 +1100 Subject: [PATCH 3/8] Apply review feedback --- config/routes.rb | 2 +- doc/workspaces/quickstart/index.md | 7 +++++++ .../remote_development/workspaces_controller.rb | 5 ----- ee/app/policies/ee/global_policy.rb | 2 +- ...e_dev.yml => remote_development_feature_flag.yml} | 2 +- ee/lib/sidebars/your_work/menus/workspaces_menu.rb | 3 +-- .../remote_development/workspaces_controller_spec.rb | 2 +- .../sidebars/your_work/menus/workspaces_menu_spec.rb | 4 ++-- ee/spec/policies/global_policy_spec.rb | 12 ++++++++++++ 9 files changed, 26 insertions(+), 13 deletions(-) rename ee/config/feature_flags/development/{remote_dev.yml => remote_development_feature_flag.yml} (86%) diff --git a/config/routes.rb b/config/routes.rb index 129701461ea15c..b674ed22d524f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -160,10 +160,10 @@ end draw :operations - draw :remote_development draw :jira_connect Gitlab.ee do + draw :remote_development draw :security draw :smartcard draw :trial diff --git a/doc/workspaces/quickstart/index.md b/doc/workspaces/quickstart/index.md index b880aa9881ff9f..e408a3ed40971d 100644 --- a/doc/workspaces/quickstart/index.md +++ b/doc/workspaces/quickstart/index.md @@ -5,6 +5,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference --- +> Introduced in GitLab 15.11 [with a flag](../../administration/feature_flags.md) named `remote_development_feature_flag`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, +ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `remote_development_feature_flag`. +The feature is not ready for production use. + # Tutorial: Create and run your first GitLab Workspace **(ULTIMATE)** This tutorial shows you how to configure and run your first Remote Development Workspace in GitLab. diff --git a/ee/app/controllers/remote_development/workspaces_controller.rb b/ee/app/controllers/remote_development/workspaces_controller.rb index a57eef41c9e95e..73fc78ea0da126 100644 --- a/ee/app/controllers/remote_development/workspaces_controller.rb +++ b/ee/app/controllers/remote_development/workspaces_controller.rb @@ -2,7 +2,6 @@ module RemoteDevelopment class WorkspacesController < ApplicationController - before_action :remote_development_enabled!, only: [:index] before_action :authorize_remote_development!, only: [:index] feature_category :remote_development @@ -12,10 +11,6 @@ def index; end private - def remote_development_enabled! - render_404 unless Feature.enabled?(:remote_dev) - end - def authorize_remote_development! render_404 unless can?(current_user, :read_workspace) end diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb index 273ba068a34a73..95a9646fe954cc 100644 --- a/ee/app/policies/ee/global_policy.rb +++ b/ee/app/policies/ee/global_policy.rb @@ -10,7 +10,7 @@ module GlobalPolicy end condition(:remote_development_available) do - License.feature_available?(:remote_development) + ::Feature.enabled?(:remote_development_feature_flag) && License.feature_available?(:remote_development) end condition(:pages_size_limit_available) do diff --git a/ee/config/feature_flags/development/remote_dev.yml b/ee/config/feature_flags/development/remote_development_feature_flag.yml similarity index 86% rename from ee/config/feature_flags/development/remote_dev.yml rename to ee/config/feature_flags/development/remote_development_feature_flag.yml index beaef2bf226741..1b91e9f203f06a 100644 --- a/ee/config/feature_flags/development/remote_dev.yml +++ b/ee/config/feature_flags/development/remote_development_feature_flag.yml @@ -1,5 +1,5 @@ --- -name: remote_dev +name: remote_development_feature_flag introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112397 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391543 milestone: '15.10' diff --git a/ee/lib/sidebars/your_work/menus/workspaces_menu.rb b/ee/lib/sidebars/your_work/menus/workspaces_menu.rb index a7ef44874cabc6..b0b7c231516ce9 100644 --- a/ee/lib/sidebars/your_work/menus/workspaces_menu.rb +++ b/ee/lib/sidebars/your_work/menus/workspaces_menu.rb @@ -21,8 +21,7 @@ def sprite_icon override :render? def render? - ::Feature.enabled?(:remote_dev) && - can?(context.current_user, :read_workspace) + can?(context.current_user, :read_workspace) end override :active_routes diff --git a/ee/spec/controllers/remote_development/workspaces_controller_spec.rb b/ee/spec/controllers/remote_development/workspaces_controller_spec.rb index 9cd6fec3cb7d5e..87e8a645fab3ad 100644 --- a/ee/spec/controllers/remote_development/workspaces_controller_spec.rb +++ b/ee/spec/controllers/remote_development/workspaces_controller_spec.rb @@ -12,7 +12,7 @@ shared_examples 'remote development feature flag' do |feature_flag_enabled, expected_status| before do stub_licensed_features(remote_development: true) - stub_feature_flags(remote_dev: feature_flag_enabled) + stub_feature_flags(remote_development_feature_flag: feature_flag_enabled) end describe 'GET #index' do diff --git a/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb b/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb index b00236bdd8a4ce..12975e58867e7e 100644 --- a/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb +++ b/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb @@ -7,7 +7,7 @@ is_super_sidebar: false, access_check: :read_workspace, link: '/-/remote_development/workspaces', - title: _('Workspaces'), + title: 'Workspaces', icon: 'cloud-gear', active_route: 'remote_development/workspaces#index' @@ -15,7 +15,7 @@ is_super_sidebar: true, access_check: :read_workspace, link: '/-/remote_development/workspaces', - title: _('Workspaces'), + title: 'Workspaces', icon: 'cloud-gear', active_route: 'remote_development/workspaces#index' end diff --git a/ee/spec/policies/global_policy_spec.rb b/ee/spec/policies/global_policy_spec.rb index 3ca1ad6d05048c..ff608a5f70a4dc 100644 --- a/ee/spec/policies/global_policy_spec.rb +++ b/ee/spec/policies/global_policy_spec.rb @@ -36,6 +36,18 @@ end describe 'reading workspaces' do + context 'when feature flag is disabled' do + before do + Feature.disable(:remote_development_feature_flag) + end + + after do + Feature.enable(:remote_development_feature_flag) + end + + it { is_expected.to be_disallowed(:read_workspace) } + end + context 'when licensed' do before do stub_licensed_features(remote_development: true) -- GitLab From ad1e1cc0dc74af27e0b4c1f9175c0973c3f84a61 Mon Sep 17 00:00:00 2001 From: David O'Regan Date: Tue, 14 Mar 2023 09:28:43 +1100 Subject: [PATCH 4/8] Apply review feedback --- .../quickstart => user/workspace/quick_start}/index.md | 5 +++-- .../remote_development/components/empty_state.vue | 2 +- .../assets/javascripts/remote_development/router/index.js | 2 +- ee/app/models/gitlab_subscriptions/features.rb | 2 +- .../remote_development/components/empty_state_spec.js | 2 +- ee/spec/policies/global_policy_spec.rb | 8 ++------ locale/gitlab.pot | 2 +- 7 files changed, 10 insertions(+), 13 deletions(-) rename doc/{workspaces/quickstart => user/workspace/quick_start}/index.md (62%) diff --git a/doc/workspaces/quickstart/index.md b/doc/user/workspace/quick_start/index.md similarity index 62% rename from doc/workspaces/quickstart/index.md rename to doc/user/workspace/quick_start/index.md index e408a3ed40971d..d2c58299b648c4 100644 --- a/doc/workspaces/quickstart/index.md +++ b/doc/user/workspace/quick_start/index.md @@ -5,11 +5,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference --- -> Introduced in GitLab 15.11 [with a flag](../../administration/feature_flags.md) named `remote_development_feature_flag`. Disabled by default. +> Introduced in GitLab 15.11 [with a flag](../../../administration/feature_flags.md) named `remote_development_feature_flag`. Disabled by default. FLAG: On self-managed GitLab, by default this feature is not available. To make it available, -ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `remote_development_feature_flag`. +ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `remote_development_feature_flag`. +On GitLab.com, this feature is not available. The feature is not ready for production use. # Tutorial: Create and run your first GitLab Workspace **(ULTIMATE)** diff --git a/ee/app/assets/javascripts/remote_development/components/empty_state.vue b/ee/app/assets/javascripts/remote_development/components/empty_state.vue index 5e3cf268efab3f..5a56c0ed996ca8 100644 --- a/ee/app/assets/javascripts/remote_development/components/empty_state.vue +++ b/ee/app/assets/javascripts/remote_development/components/empty_state.vue @@ -17,7 +17,7 @@ export default { }, computed: { workspaceHelpPagePath() { - return helpPagePath('workspaces/quick_start/index.md'); + return helpPagePath('user/workspace/quick_start/index.md'); }, }, i18n, diff --git a/ee/app/assets/javascripts/remote_development/router/index.js b/ee/app/assets/javascripts/remote_development/router/index.js index f41edc79ce5a5d..c4e73a2e176278 100644 --- a/ee/app/assets/javascripts/remote_development/router/index.js +++ b/ee/app/assets/javascripts/remote_development/router/index.js @@ -35,7 +35,7 @@ export default function createRouter({ base }) { path: '', component: WorkspacesList, meta: { - breadcrumb: s__('Workspaces|Workspace List'), + breadcrumb: s__('Workspaces|Workspace list'), }, }, ]), diff --git a/ee/app/models/gitlab_subscriptions/features.rb b/ee/app/models/gitlab_subscriptions/features.rb index 1abcdce1e7305b..069e5fd13bf194 100644 --- a/ee/app/models/gitlab_subscriptions/features.rb +++ b/ee/app/models/gitlab_subscriptions/features.rb @@ -151,6 +151,7 @@ class Features protected_environments reject_non_dco_commits reject_unsigned_commits + remote_development saml_group_sync service_accounts scoped_labels @@ -217,7 +218,6 @@ class Features quality_management related_epics release_evidence_test_artifacts - remote_development report_approver_rules required_ci_templates requirements diff --git a/ee/spec/frontend/remote_development/components/empty_state_spec.js b/ee/spec/frontend/remote_development/components/empty_state_spec.js index fb01c8a7b02c23..af3178db8515b4 100644 --- a/ee/spec/frontend/remote_development/components/empty_state_spec.js +++ b/ee/spec/frontend/remote_development/components/empty_state_spec.js @@ -6,7 +6,7 @@ import EmptyState, { i18n } from 'ee/remote_development/components/empty_state.v describe('ee/app/assets/javascripts/remote_development/components/empty_state.vue', () => { let wrapper; - const QUICK_START_LINK = 'workspaces/quick_start/index.md'; + const QUICK_START_LINK = '/user/workspace/quick_start/index.md'; const findEmptyState = () => wrapper.findComponent(GlEmptyState); const createComponent = () => { diff --git a/ee/spec/policies/global_policy_spec.rb b/ee/spec/policies/global_policy_spec.rb index ff608a5f70a4dc..ef07be1189b08e 100644 --- a/ee/spec/policies/global_policy_spec.rb +++ b/ee/spec/policies/global_policy_spec.rb @@ -38,13 +38,9 @@ describe 'reading workspaces' do context 'when feature flag is disabled' do before do - Feature.disable(:remote_development_feature_flag) + stub_feature_flags(remote_development_feature_flag: false) end - - after do - Feature.enable(:remote_development_feature_flag) - end - + it { is_expected.to be_disallowed(:read_workspace) } end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fc1e891d60dbc9..db01512830a4fc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -49815,7 +49815,7 @@ msgstr "" msgid "Workspaces|GitLab Workspaces is a powerful collaborative platform that provides a comprehensive set of tools for software development teams to manage their entire development lifecycle." msgstr "" -msgid "Workspaces|Workspace List" +msgid "Workspaces|Workspace list" msgstr "" msgid "Workspaces|Workspaces" -- GitLab From d5e6710a0354931c3073a021f7d3f19d78171a7a Mon Sep 17 00:00:00 2001 From: David O'Regan Date: Wed, 15 Mar 2023 10:11:39 +1100 Subject: [PATCH 5/8] Update specs to lowercase --- .../your_work/menus/workspaces_menu_spec.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb b/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb index 12975e58867e7e..7b454b283eb5e7 100644 --- a/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb +++ b/ee/spec/lib/ee/sidebars/your_work/menus/workspaces_menu_spec.rb @@ -3,19 +3,22 @@ require 'spec_helper' RSpec.describe Sidebars::YourWork::Menus::WorkspacesMenu, feature_category: :remote_development do - it_behaves_like 'Top-Level menu item', + it_behaves_like 'top-level menu item', is_super_sidebar: false, - access_check: :read_workspace, link: '/-/remote_development/workspaces', title: 'Workspaces', icon: 'cloud-gear', - active_route: 'remote_development/workspaces#index' + active_route: { path: 'remote_development/workspaces#index' } - it_behaves_like 'Top-Level menu item', + it_behaves_like 'top-level menu item', is_super_sidebar: true, - access_check: :read_workspace, link: '/-/remote_development/workspaces', title: 'Workspaces', icon: 'cloud-gear', - active_route: 'remote_development/workspaces#index' + active_route: { path: 'remote_development/workspaces#index' } + + it_behaves_like 'top-level menu item with license feature guard', + access_check: :read_workspace + + it_behaves_like 'menu without sub menu items' end -- GitLab From e61a1a69efc3b1839a27e8ef3a8eb1e76a99b409 Mon Sep 17 00:00:00 2001 From: David O'Regan Date: Mon, 20 Mar 2023 17:28:52 +1100 Subject: [PATCH 6/8] Apply review feedback --- .../pages/remote_development/index.js | 2 +- .../components/{ => list}/empty_state.vue | 2 + .../{index.js => init_workspaces_app.js} | 7 ++-- .../{components => pages}/app.vue | 0 .../{components => pages}/list.vue | 2 +- .../remote_development/router/index.js | 39 ++++--------------- .../workspaces/index.html.haml | 1 + .../components/empty_state_spec.js | 13 ++++--- .../components/list_spec.js | 21 ---------- .../init_workspaces_app_spec.js | 33 ++++++++++++++++ .../remote_development/pages/list_spec.js | 20 ++++++++++ .../remote_development/router/index_spec.js | 11 ++++-- locale/gitlab.pot | 6 --- 13 files changed, 85 insertions(+), 72 deletions(-) rename ee/app/assets/javascripts/remote_development/components/{ => list}/empty_state.vue (93%) rename ee/app/assets/javascripts/remote_development/{index.js => init_workspaces_app.js} (79%) rename ee/app/assets/javascripts/remote_development/{components => pages}/app.vue (100%) rename ee/app/assets/javascripts/remote_development/{components => pages}/list.vue (69%) delete mode 100644 ee/spec/frontend/remote_development/components/list_spec.js create mode 100644 ee/spec/frontend/remote_development/init_workspaces_app_spec.js create mode 100644 ee/spec/frontend/remote_development/pages/list_spec.js diff --git a/ee/app/assets/javascripts/pages/remote_development/index.js b/ee/app/assets/javascripts/pages/remote_development/index.js index fb48c46e3dfe78..35bd2fc8546466 100644 --- a/ee/app/assets/javascripts/pages/remote_development/index.js +++ b/ee/app/assets/javascripts/pages/remote_development/index.js @@ -1,3 +1,3 @@ -import { initWorkspacesApp } from 'ee/remote_development'; +import { initWorkspacesApp } from 'ee/remote_development/init_workspaces_app'; initWorkspacesApp(); diff --git a/ee/app/assets/javascripts/remote_development/components/empty_state.vue b/ee/app/assets/javascripts/remote_development/components/list/empty_state.vue similarity index 93% rename from ee/app/assets/javascripts/remote_development/components/empty_state.vue rename to ee/app/assets/javascripts/remote_development/components/list/empty_state.vue index 5a56c0ed996ca8..df3eaab14ad660 100644 --- a/ee/app/assets/javascripts/remote_development/components/empty_state.vue +++ b/ee/app/assets/javascripts/remote_development/components/list/empty_state.vue @@ -15,6 +15,7 @@ export default { components: { GlEmptyState, }, + inject: ['emptyStateSvgPath'], computed: { workspaceHelpPagePath() { return helpPagePath('user/workspace/quick_start/index.md'); @@ -30,5 +31,6 @@ export default { :description="$options.i18n.description" :primary-button-text="$options.i18n.primaryButtonText" :primary-button-link="workspaceHelpPagePath" + :svg-path="emptyStateSvgPath" /> diff --git a/ee/app/assets/javascripts/remote_development/index.js b/ee/app/assets/javascripts/remote_development/init_workspaces_app.js similarity index 79% rename from ee/app/assets/javascripts/remote_development/index.js rename to ee/app/assets/javascripts/remote_development/init_workspaces_app.js index 730d8b48cc2c71..69f2b09ec34583 100644 --- a/ee/app/assets/javascripts/remote_development/index.js +++ b/ee/app/assets/javascripts/remote_development/init_workspaces_app.js @@ -1,12 +1,12 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import App from './components/app.vue'; +import App from './pages/app.vue'; import createRouter from './router'; Vue.use(VueApollo); -const apolloClient = createDefaultClient({}, { batchMax: 1 }); +const apolloClient = createDefaultClient(); const apolloProvider = new VueApollo({ defaultClient: apolloClient, }); @@ -18,7 +18,7 @@ const initWorkspacesApp = () => { return null; } - const { workspacesListPath } = el.dataset; + const { workspacesListPath, emptyStateSvgPath } = el.dataset; const router = createRouter({ base: workspacesListPath, }); @@ -30,6 +30,7 @@ const initWorkspacesApp = () => { apolloProvider, provide: { workspacesListPath, + emptyStateSvgPath, }, render: (createElement) => createElement(App), }); diff --git a/ee/app/assets/javascripts/remote_development/components/app.vue b/ee/app/assets/javascripts/remote_development/pages/app.vue similarity index 100% rename from ee/app/assets/javascripts/remote_development/components/app.vue rename to ee/app/assets/javascripts/remote_development/pages/app.vue diff --git a/ee/app/assets/javascripts/remote_development/components/list.vue b/ee/app/assets/javascripts/remote_development/pages/list.vue similarity index 69% rename from ee/app/assets/javascripts/remote_development/components/list.vue rename to ee/app/assets/javascripts/remote_development/pages/list.vue index 72c8dfa78c5be4..522ea385448553 100644 --- a/ee/app/assets/javascripts/remote_development/components/list.vue +++ b/ee/app/assets/javascripts/remote_development/pages/list.vue @@ -1,5 +1,5 @@