From df3f7fc61a1416ac7caf6da56c67dfc5ee2be603 Mon Sep 17 00:00:00 2001 From: Paul Slaughter Date: Wed, 12 Feb 2025 17:25:11 -0600 Subject: [PATCH 1/2] Stub vscode extension marketplace UI - Behind feature flag `vscode_extension_marketplace_settings` - https://gitlab.com/gitlab-org/gitlab/-/issues/508977 --- .../application_settings/general/index.js | 4 + .../components/settings_app.vue | 143 ++++++++++++ app/helpers/application_settings_helper.rb | 14 ++ .../_extension_marketplace.html.haml | 8 + .../application_settings/general.html.haml | 1 + .../vscode_extension_marketplace_settings.yml | 9 + .../helpers/ee/application_settings_helper.rb | 15 ++ .../ee/application_settings_helper_spec.rb | 28 +++ lib/api/settings.rb | 5 + locale/gitlab.pot | 21 ++ .../components/settings_app_spec.js | 218 ++++++++++++++++++ .../application_settings_helper_spec.rb | 33 +++ spec/requests/api/settings_spec.rb | 7 +- .../_extension_marketplace.html.haml_spec.rb | 48 ++++ 14 files changed, 553 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/vscode_extension_marketplace/components/settings_app.vue create mode 100644 app/views/admin/application_settings/_extension_marketplace.html.haml create mode 100644 config/feature_flags/wip/vscode_extension_marketplace_settings.yml create mode 100644 spec/frontend/vscode_extension_marketplace/components/settings_app_spec.js create mode 100644 spec/views/admin/application_settings/_extension_marketplace.html.haml_spec.rb diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/general/index.js index d0593c82ac1cc7..a5159190bb2443 100644 --- a/app/assets/javascripts/pages/admin/application_settings/general/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js @@ -1,4 +1,6 @@ import { initSilentModeSettings } from '~/silent_mode_settings'; +import { initSimpleApp } from '~/helpers/init_simple_app_helper'; +import VscodeExtensionMarketplaceSettings from '~/vscode_extension_marketplace/components/settings_app.vue'; import initAccountAndLimitsSection from '../account_and_limits'; import initGitpod from '../gitpod'; import initSignupRestrictions from '../signup_restrictions'; @@ -8,4 +10,6 @@ import initSignupRestrictions from '../signup_restrictions'; initGitpod(); initSignupRestrictions(); initSilentModeSettings(); + + initSimpleApp('#js-extension-marketplace-settings-app', VscodeExtensionMarketplaceSettings); })(); diff --git a/app/assets/javascripts/vscode_extension_marketplace/components/settings_app.vue b/app/assets/javascripts/vscode_extension_marketplace/components/settings_app.vue new file mode 100644 index 00000000000000..73de733f10043d --- /dev/null +++ b/app/assets/javascripts/vscode_extension_marketplace/components/settings_app.vue @@ -0,0 +1,143 @@ + + diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 4db587b6649ae4..152f9793597c73 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -683,6 +683,20 @@ def signup_form_data pending_user_count: pending_user_count } end + + def vscode_extension_marketplace_settings_view + # NOTE: This is intentionally not scoped to a specific actor since it effects instance-level settings. + return unless Feature.enabled?(:vscode_extension_marketplace_settings, nil) + + { + title: _('VS Code Extension Marketplace'), + # NOTE: description is overridden in EE + description: _('Enable VS Code Extension Marketplace and configure the extensions registry for Web IDE.'), + view_model: { + initialSettings: @application_setting.vscode_extension_marketplace || {} + } + } + end end ApplicationSettingsHelper.prepend_mod_with('ApplicationSettingsHelper') diff --git a/app/views/admin/application_settings/_extension_marketplace.html.haml b/app/views/admin/application_settings/_extension_marketplace.html.haml new file mode 100644 index 00000000000000..349cd1d8ea7b15 --- /dev/null +++ b/app/views/admin/application_settings/_extension_marketplace.html.haml @@ -0,0 +1,8 @@ +- settings_view = vscode_extension_marketplace_settings_view + +- return unless settings_view + += render ::Layouts::SettingsBlockComponent.new(settings_view[:title], id: 'js-extension-marketplace-settings') do |c| + - c.with_description { settings_view[:description] } + - c.with_body do + #js-extension-marketplace-settings-app{ data: { view_model: settings_view[:view_model].to_json } } diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml index 4b792bb78b4298..aeb5af6dbe0b19 100644 --- a/app/views/admin/application_settings/general.html.haml +++ b/app/views/admin/application_settings/general.html.haml @@ -112,3 +112,4 @@ = render 'admin/application_settings/security_txt', expanded: expanded_by_default? = render_if_exists 'admin/application_settings/analytics' = render_if_exists 'admin/application_settings/amazon_q' += render 'admin/application_settings/extension_marketplace' diff --git a/config/feature_flags/wip/vscode_extension_marketplace_settings.yml b/config/feature_flags/wip/vscode_extension_marketplace_settings.yml new file mode 100644 index 00000000000000..8614962383fc8a --- /dev/null +++ b/config/feature_flags/wip/vscode_extension_marketplace_settings.yml @@ -0,0 +1,9 @@ +--- +name: vscode_extension_marketplace_settings +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/508977 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181624 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/508996 +milestone: '17.10' +group: group::remote development +type: wip +default_enabled: false diff --git a/ee/app/helpers/ee/application_settings_helper.rb b/ee/app/helpers/ee/application_settings_helper.rb index 2aeae5ce628f4f..18ff5fac3cefe9 100644 --- a/ee/app/helpers/ee/application_settings_helper.rb +++ b/ee/app/helpers/ee/application_settings_helper.rb @@ -292,6 +292,17 @@ def global_search_settings_checkboxes(form) ] end + override :vscode_extension_marketplace_settings_view + def vscode_extension_marketplace_settings_view + merge_view = if License.feature_available?(:remote_development) + { description: vscode_extension_marketplace_settings_workspaces_description } + else + {} + end + + super&.merge(merge_view) + end + def zoekt_settings_checkboxes(form) ::Search::Zoekt::Settings.boolean_settings.map do |setting_name, config| form.gitlab_ui_checkbox_component( @@ -352,5 +363,9 @@ def enable_promotion_management_attributes %i[enable_member_promotion_management] end + + def vscode_extension_marketplace_settings_workspaces_description + _('Enable VS Code Extension Marketplace and configure the extensions registry for Web IDE and Workspaces.') + end end end diff --git a/ee/spec/helpers/ee/application_settings_helper_spec.rb b/ee/spec/helpers/ee/application_settings_helper_spec.rb index 78b69851c34560..af8fc77a34865c 100644 --- a/ee/spec/helpers/ee/application_settings_helper_spec.rb +++ b/ee/spec/helpers/ee/application_settings_helper_spec.rb @@ -278,6 +278,34 @@ end end + describe '#vscode_extension_marketplace_settings_view' do + using RSpec::Parameterized::TableSyntax + + let(:remote_development_license_flag) { true } + let(:application_setting) { build(:application_setting) } + + # rubocop:disable Layout/LineLength -- The message extends past the line length + where(:remote_dev_license, :expected_description) do + false | _('Enable VS Code Extension Marketplace and configure the extensions registry for Web IDE.') + true | _('Enable VS Code Extension Marketplace and configure the extensions registry for Web IDE and Workspaces.') + end + # rubocop:enable Layout/LineLength + + with_them do + before do + stub_feature_flags(vscode_extension_marketplace_settings: true) + stub_licensed_features(remote_development: remote_dev_license) + helper.instance_variable_set(:@application_setting, application_setting) + end + + it 'returns expected description' do + actual_description = helper.vscode_extension_marketplace_settings_view[:description] + + expect(actual_description).to be(expected_description) + end + end + end + describe '#zoekt_settings_checkboxes', feature_category: :global_search do let_it_be(:application_setting) { build(:application_setting) } diff --git a/lib/api/settings.rb b/lib/api/settings.rb index a5254b999c0215..b68fcafa5b8bb0 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -240,6 +240,11 @@ def filter_attributes_using_license(attrs) optional :code_suggestions_api_rate_limit, type: Integer, desc: 'Maximum requests a user can make per minute to code suggestions endpoint' optional :resource_usage_limits, type: JSON, desc: 'Definition for resource usage limits enforced in Sidekiq workers' optional :ropc_without_client_credentials, type: Boolean, desc: 'Allows the use of Oauth ROPC flow without client credentials' + optional :vscode_extension_marketplace, type: Hash, desc: 'Settings for VS Code Extension Marketplace' do + optional :enabled, type: Boolean, desc: 'Enables VS Code Extension Marketplace for Web IDE and Workspaces' + optional :preset, type: String, desc: "The preset configuration of URL's for the VS Code Extension Marketplace" + optional :custom_values, type: Hash, desc: "VS Code Extension Marketplace URL's when preset is 'custom'" + end Gitlab::SSHPublicKey.supported_types.each do |type| optional :"#{type}_key_restriction", diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2b6701a2d06d3b..2483558614eb07 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22653,6 +22653,12 @@ msgstr "" msgid "Enable Spam Check via external API endpoint" msgstr "" +msgid "Enable VS Code Extension Marketplace and configure the extensions registry for Web IDE and Workspaces." +msgstr "" + +msgid "Enable VS Code Extension Marketplace and configure the extensions registry for Web IDE." +msgstr "" + msgid "Enable What's new: All tiers" msgstr "" @@ -24429,6 +24435,18 @@ msgstr "" msgid "Exported requirements" msgstr "" +msgid "ExtensionMarketplace|Extension marketplace settings updated." +msgstr "" + +msgid "ExtensionMarketplace|Failed to update extension marketplace settings." +msgstr "" + +msgid "ExtensionMarketplace|Failed to update extension marketplace settings. %{message}" +msgstr "" + +msgid "ExtensionMarketplace|Please see the browser console for more information and try again." +msgstr "" + msgid "External URL" msgstr "" @@ -63833,6 +63851,9 @@ msgstr "" msgid "Using required encryption strategy when encrypted field is missing!" msgstr "" +msgid "VS Code Extension Marketplace" +msgstr "" + msgid "Validate" msgstr "" diff --git a/spec/frontend/vscode_extension_marketplace/components/settings_app_spec.js b/spec/frontend/vscode_extension_marketplace/components/settings_app_spec.js new file mode 100644 index 00000000000000..6533529fb1d851 --- /dev/null +++ b/spec/frontend/vscode_extension_marketplace/components/settings_app_spec.js @@ -0,0 +1,218 @@ +import { nextTick } from 'vue'; +import { GlAlert, GlButton, GlForm, GlFormFields, GlFormTextarea } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { logError } from '~/lib/logger'; +import SettingsApp from '~/vscode_extension_marketplace/components/settings_app.vue'; +import toast from '~/vue_shared/plugins/global_toast'; + +jest.mock('~/lib/logger'); +jest.mock('~/vue_shared/plugins/global_toast'); +jest.mock('lodash/uniqueId', () => (x) => `${x}testUnique`); + +const TEST_NEW_SETTINGS = { enabled: false, preset: 'open_vsx' }; +const EXPECTED_FORM_ID = 'extension-marketplace-settings-form-testUnique'; + +describe('~/vscode_extension_marketplace/components/settings_app.vue', () => { + let wrapper; + let mockAdapter; + let submitSpy; + + const createComponent = (props = {}) => { + wrapper = shallowMount(SettingsApp, { + propsData: { + ...props, + }, + stubs: { + GlFormFields, + }, + }); + }; + + const findForm = () => wrapper.findComponent(GlForm); + const findFormFields = () => wrapper.findComponent(GlFormFields); + const findTextarea = () => wrapper.findComponent(GlFormTextarea); + const findSaveButton = () => wrapper.findComponent(GlButton); + const findErrorAlert = () => wrapper.findComponent(GlAlert); + const findErrorAlertItems = () => + findErrorAlert() + .findAll('li') + .wrappers.map((x) => x.text()); + + beforeEach(() => { + gon.api_version = 'v4'; + submitSpy = jest.fn().mockReturnValue([200]); + mockAdapter = new MockAdapter(axios); + mockAdapter + .onPut('/api/v4/application/settings') + .reply(({ data }) => submitSpy(JSON.parse(data))); + }); + + afterEach(() => { + mockAdapter.restore(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders form', () => { + expect(findForm().attributes('id')).toBe(EXPECTED_FORM_ID); + }); + + it('renders form fields', () => { + expect(findFormFields().props()).toMatchObject({ + formId: EXPECTED_FORM_ID, + values: { + settings: {}, + }, + fields: SettingsApp.FIELDS, + }); + }); + + it('renders settings textarea', () => { + expect(findTextarea().attributes()).toMatchObject({ + id: 'gl-form-field-testUnique', + value: '{}', + }); + }); + + it('renders save button', () => { + expect(findSaveButton().attributes()).toMatchObject({ + type: 'submit', + variant: 'confirm', + category: 'primary', + }); + expect(findSaveButton().props('loading')).toBe(false); + expect(findSaveButton().text()).toBe('Save changes'); + }); + }); + + describe('when submitted', () => { + beforeEach(async () => { + createComponent(); + + findTextarea().vm.$emit('input', JSON.stringify(TEST_NEW_SETTINGS)); + await nextTick(); + + findFormFields().vm.$emit('submit'); + }); + + it('triggers loading', () => { + expect(findSaveButton().props('loading')).toBe(true); + }); + + it('makes submit request', () => { + expect(submitSpy).toHaveBeenCalledTimes(1); + expect(submitSpy).toHaveBeenCalledWith({ + vscode_extension_marketplace: TEST_NEW_SETTINGS, + }); + }); + + it('while loading, prevents extra submit', () => { + findFormFields().vm.$emit('submit'); + findFormFields().vm.$emit('submit'); + + expect(submitSpy).toHaveBeenCalledTimes(1); + }); + + it('when success, shows success message and stops loading', async () => { + expect(toast).not.toHaveBeenCalled(); + + await axios.waitForAll(); + + expect(toast).toHaveBeenCalledTimes(1); + expect(toast).toHaveBeenCalledWith('Extension marketplace settings updated.'); + expect(findSaveButton().props('loading')).toBe(false); + }); + + it('does not show error alert', () => { + expect(findErrorAlert().exists()).toBe(false); + }); + }); + + describe('when submitted and errored', () => { + beforeEach(() => { + submitSpy.mockReturnValue([400]); + + createComponent(); + + findFormFields().vm.$emit('submit'); + }); + + it('shows error message', async () => { + expect(findErrorAlert().exists()).toBe(false); + + await axios.waitForAll(); + + expect(findErrorAlert().exists()).toBe(true); + expect(findErrorAlert().props('dismissible')).toBe(false); + expect(findErrorAlert().text()).toBe( + 'Failed to update extension marketplace settings. Please see the browser console for more information and try again.', + ); + }); + + it('logs error', async () => { + expect(logError).not.toHaveBeenCalled(); + + await axios.waitForAll(); + + expect(logError).toHaveBeenCalledTimes(1); + expect(logError).toHaveBeenCalledWith( + 'Failed to update extension marketplace settings. See error info:', + expect.any(Error), + ); + }); + + it('hides error message with another submit', async () => { + await axios.waitForAll(); + + expect(findErrorAlert().exists()).toBe(true); + + findFormFields().vm.$emit('submit'); + await nextTick(); + + expect(findErrorAlert().exists()).toBe(false); + }); + }); + + describe('when submitted and server responds with structured error', () => { + beforeEach(() => { + submitSpy.mockReturnValue([ + 400, + { message: { vscode_extension_marketplace: ['LOREM', 'IPSUM'] } }, + ]); + + createComponent(); + + findFormFields().vm.$emit('submit'); + }); + + it('shows error message', async () => { + expect(findErrorAlert().exists()).toBe(false); + + await axios.waitForAll(); + + expect(findErrorAlert().exists()).toBe(true); + expect(findErrorAlert().text()).toContain('Failed to update extension marketplace settings.'); + expect(findErrorAlertItems()).toEqual([ + 'vscode_extension_marketplace : LOREM', + 'vscode_extension_marketplace : IPSUM', + ]); + }); + }); + + describe('with initialSettings', () => { + beforeEach(() => { + createComponent({ + initialSettings: TEST_NEW_SETTINGS, + }); + }); + + it('initializes the form with given settings', () => { + expect(findTextarea().props('value')).toBe(JSON.stringify(TEST_NEW_SETTINGS, null, 2)); + }); + }); +}); diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb index af71fe131fc98c..485728c007e9f3 100644 --- a/spec/helpers/application_settings_helper_spec.rb +++ b/spec/helpers/application_settings_helper_spec.rb @@ -447,4 +447,37 @@ end end end + + describe '#vscode_extension_marketplace_settings_view' do + let(:feature_flag) { true } + let(:application_setting) { build(:application_setting) } + let(:vscode_extension_marketplace) { { "enabled" => false } } + + before do + stub_feature_flags(vscode_extension_marketplace_settings: feature_flag) + + application_setting.vscode_extension_marketplace = vscode_extension_marketplace + helper.instance_variable_set(:@application_setting, application_setting) + end + + context 'with flag on' do + it 'returns hash of view properties' do + expect(helper.vscode_extension_marketplace_settings_view).to eq({ + title: _('VS Code Extension Marketplace'), + description: _('Enable VS Code Extension Marketplace and configure the extensions registry for Web IDE.'), + view_model: { + initialSettings: vscode_extension_marketplace + } + }) + end + end + + context 'with flag off' do + let(:feature_flag) { false } + + it 'returns nil' do + expect(helper.vscode_extension_marketplace_settings_view).to be_nil + end + end + end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index c5d9e729032b46..11ec72f150e190 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -226,7 +226,11 @@ concurrent_github_import_jobs_limit: 2, concurrent_bitbucket_import_jobs_limit: 2, concurrent_bitbucket_server_import_jobs_limit: 2, - require_personal_access_token_expiry: false + require_personal_access_token_expiry: false, + vscode_extension_marketplace: { + enabled: false, + preset: 'open_vsx' + } } expect(response).to have_gitlab_http_status(:ok) @@ -317,6 +321,7 @@ expect(json_response['concurrent_bitbucket_import_jobs_limit']).to be(2) expect(json_response['concurrent_bitbucket_server_import_jobs_limit']).to be(2) expect(json_response['require_personal_access_token_expiry']).to be(false) + expect(json_response['vscode_extension_marketplace']).to eq({ "enabled" => false, "preset" => 'open_vsx' }) end end diff --git a/spec/views/admin/application_settings/_extension_marketplace.html.haml_spec.rb b/spec/views/admin/application_settings/_extension_marketplace.html.haml_spec.rb new file mode 100644 index 00000000000000..e8b32a2305d42b --- /dev/null +++ b/spec/views/admin/application_settings/_extension_marketplace.html.haml_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'admin/application_settings/_extension_marketplace', feature_category: :web_ide do + let(:feature_available) { true } + + subject(:page) do + # We use `view.render`, because just `render` throws a "no implicit conversion of nil into String" exception + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53093#note_499060593 + view.assign({ application_setting: build(:application_setting) }) + rendered = view.render('admin/application_settings/extension_marketplace') + + rendered && Nokogiri::HTML.parse(rendered) + end + + before do + stub_feature_flags(vscode_extension_marketplace_settings: feature_available) + end + + context 'when feature available' do + it 'renders settings' do + settings = page.at('#js-extension-marketplace-settings') + + expect(settings).not_to be_nil + expect(settings).to have_text(_('VS Code Extension Marketplace')) + expect(settings).to have_text( + _('Enable VS Code Extension Marketplace and configure the extensions registry for Web IDE.') + ) + end + + it 'renders data-view-model for vue app' do + vue_app = page.at('#js-extension-marketplace-settings-app') + expected_json = { initialSettings: {} }.to_json + + expect(vue_app).not_to be_nil + expect(vue_app['data-view-model']).to eq(expected_json) + end + end + + context 'when feature not available' do + let(:feature_available) { false } + + it 'renders nothing' do + expect(page).to be_nil + end + end +end -- GitLab From 00fc43d1e6c0c86adc4b490c884a2ba04074322d Mon Sep 17 00:00:00 2001 From: Paul Slaughter Date: Mon, 10 Mar 2025 12:09:09 -0500 Subject: [PATCH 2/2] Update from review feedback - Change fallback error message - Polish a11y - Fix typos - Override just _description method --- .../components/settings_app.vue | 14 +++++++++---- app/helpers/application_settings_helper.rb | 10 +++++++--- .../_extension_marketplace.html.haml | 6 +++--- .../helpers/ee/application_settings_helper.rb | 20 +++++++------------ .../ee/application_settings_helper_spec.rb | 17 ++++------------ locale/gitlab.pot | 6 +++--- .../components/settings_app_spec.js | 12 ++++++++++- 7 files changed, 45 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/vscode_extension_marketplace/components/settings_app.vue b/app/assets/javascripts/vscode_extension_marketplace/components/settings_app.vue index 73de733f10043d..0b620281aa7c77 100644 --- a/app/assets/javascripts/vscode_extension_marketplace/components/settings_app.vue +++ b/app/assets/javascripts/vscode_extension_marketplace/components/settings_app.vue @@ -83,9 +83,7 @@ export default { this.errorMessage = e?.response?.data?.message || - s__( - 'ExtensionMarketplace|Please see the browser console for more information and try again.', - ); + s__('ExtensionMarketplace|An unknown error occurred. Please try again.'); } finally { this.isLoading = false; } @@ -101,7 +99,13 @@ export default {