Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-03-20 21:12:59 +00:00
parent 6cc1ef3307
commit 5a856c7946
23 changed files with 224 additions and 176 deletions

View File

@ -1,32 +0,0 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import ProjectStorageApp from './components/project_storage_app.vue';
Vue.use(VueApollo);
export default (containerId = 'js-project-storage-count-app') => {
const el = document.getElementById(containerId);
if (!el) {
return false;
}
const { projectPath } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
apolloProvider,
name: 'ProjectStorageApp',
provide: {
projectPath,
},
render(createElement) {
return createElement(ProjectStorageApp);
},
});
};

View File

@ -4,7 +4,7 @@ import getProjectStorageStatisticsQuery from 'ee_else_ce/usage_quotas/storage/qu
import ProjectStorageApp from './project_storage_app.vue';
const meta = {
title: 'usage_quotas/storage/project_storage_app',
title: 'usage_quotas/storage/project/project_storage_app',
component: ProjectStorageApp,
};

View File

@ -20,7 +20,7 @@ import {
NAMESPACE_STORAGE_TYPES,
usageQuotasHelpPaths,
storageTypeHelpPaths,
} from '../constants';
} from '../../constants';
import { getStorageTypesFromProjectStatistics, descendingStorageUsageSort } from '../utils';
import ProjectStorageDetail from './project_storage_detail.vue';

View File

@ -7,7 +7,7 @@ import {
HELP_LINK_ARIA_LABEL,
PROJECT_TABLE_LABEL_STORAGE_TYPE,
PROJECT_TABLE_LABEL_USAGE,
} from '../constants';
} from '../../constants';
import StorageTypeIcon from './storage_type_icon.vue';
export default {

View File

@ -0,0 +1,37 @@
/**
* Populates an array of storage types with usage value and other details
*
* @param {Array} selectedStorageTypes selected storage types that will be populated
* @param {Object} projectStatistics object of storage values, with storage type as keys
* @param {Object} statisticsDetailsPaths object of storage detail paths, with storage type as keys
* @param {Object} helpLinks object of help paths, with storage type as keys
* @returns {Array}
*/
export const getStorageTypesFromProjectStatistics = (
selectedStorageTypes,
projectStatistics,
statisticsDetailsPaths = {},
helpLinks = {},
) =>
selectedStorageTypes.reduce((types, currentType) => {
const helpPath = helpLinks[currentType.id];
const value = projectStatistics[`${currentType.id}Size`];
const detailsPath = statisticsDetailsPaths[currentType.id];
return types.concat({
...currentType,
helpPath,
detailsPath,
value,
});
}, []);
/**
* Creates a sorting function to sort storage types by usage in the graph and in the table
*
* @param {string} storageUsageKey key storing value of storage usage
* @returns {Function} sorting function
*/
export function descendingStorageUsageSort(storageUsageKey) {
return (a, b) => b[storageUsageKey] - a[storageUsageKey];
}

View File

@ -10,7 +10,7 @@ import {
STORAGE_TAB_METADATA_EL_SELECTOR,
} from '../constants';
import NamespaceStorageApp from './components/namespace_storage_app.vue';
import ProjectStorageApp from './components/project_storage_app.vue';
import ProjectStorageApp from './project/components/project_storage_app.vue';
const parseProjectProvideData = (el) => {
const { projectPath } = el.dataset;

View File

@ -3,44 +3,6 @@ import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import { storageTypeHelpPaths } from './constants';
/**
* Populates an array of storage types with usage value and other details
*
* @param {Array} selectedStorageTypes selected storage types that will be populated
* @param {Object} projectStatistics object of storage values, with storage type as keys
* @param {Object} statisticsDetailsPaths object of storage detail paths, with storage type as keys
* @param {Object} helpLinks object of help paths, with storage type as keys
* @returns {Array}
*/
export const getStorageTypesFromProjectStatistics = (
selectedStorageTypes,
projectStatistics,
statisticsDetailsPaths = {},
helpLinks = {},
) =>
selectedStorageTypes.reduce((types, currentType) => {
const helpPath = helpLinks[currentType.id];
const value = projectStatistics[`${currentType.id}Size`];
const detailsPath = statisticsDetailsPaths[currentType.id];
return types.concat({
...currentType,
helpPath,
detailsPath,
value,
});
}, []);
/**
* Creates a sorting function to sort storage types by usage in the graph and in the table
*
* @param {string} storageUsageKey key storing value of storage usage
* @returns {Function} sorting function
*/
export function descendingStorageUsageSort(storageUsageKey) {
return (a, b) => b[storageUsageKey] - a[storageUsageKey];
}
/**
* This method parses the results from `getNamespaceStorageStatistics`
* call.

View File

@ -26,19 +26,23 @@ class BaseActionController < ActionController::Base
next if p.directives.blank?
if helpers.vite_enabled?
vite_port = ViteRuby.instance.config.port
vite_origin = "#{Gitlab.config.gitlab.host}:#{vite_port}"
http_origin = "http://#{vite_origin}"
ws_origin = "ws://#{vite_origin}"
wss_origin = "wss://#{vite_origin}"
gitlab_ws_origin = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'vite-dev/')
http_path = Gitlab::Utils.append_path(http_origin, 'vite-dev/')
# Normally all Vite requests are proxied via Vite Ruby's middleware (example:
# https://gdk.test:3000/vite-dev/@fs/path/to/your/gdk), unless the
# skipProxy parameter is used (https://vite-ruby.netlify.app/config/#skipproxy-experimental).
#
# However, HMR requests go directly to another host, and we need to allow that.
# We need both Websocket and HTTP URLs because Vite will attempt to ping
# the HTTP URL if the Websocket isn't available:
# https://github.com/vitejs/vite/blob/899d9b1d272b7057aafc6fa01570d40f288a473b/packages/vite/src/client/client.ts#L320-L327
hmr_ws_url = Gitlab::Utils.append_path(helpers.vite_hmr_websocket_url, 'vite-dev/')
hmr_http_url = Gitlab::Utils.append_path(helpers.vite_hmr_http_url, 'vite-dev/')
http_path = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'vite-dev/')
connect_sources = p.directives['connect-src']
p.connect_src(*(Array.wrap(connect_sources) | [ws_origin, wss_origin, http_path]))
p.connect_src(*(Array.wrap(connect_sources) | [hmr_ws_url, hmr_http_url]))
worker_sources = p.directives['worker-src']
p.worker_src(*(Array.wrap(worker_sources) | [gitlab_ws_origin, http_path]))
p.worker_src(*(Array.wrap(worker_sources) | [hmr_ws_url, hmr_http_url, http_path]))
end
next unless Gitlab::CurrentSettings.snowplow_enabled? && !Gitlab::CurrentSettings.snowplow_collector_hostname.blank?

View File

@ -11,7 +11,8 @@ module Groups
button_testid: 'remove-group-button',
disabled: group.prevent_delete?.to_s,
confirm_danger_message: remove_group_message(group),
phrase: group.full_path
phrase: group.full_path,
html_confirmation_message: 'true'
}
end
end

View File

@ -86,8 +86,42 @@ module GroupsHelper
# Overridden in EE
def remove_group_message(group)
_("You are going to remove %{group_name}. This will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
content_tag :div do
content = ''.html_safe
content << content_tag(:span, _("You are about to remove the group %{group_name}.") % { group_name: group.name })
additional_content = additional_removed_items(group)
content << additional_content if additional_content.present?
content << remove_group_warning
end
end
def additional_removed_items(group)
list_content = ''.html_safe
list_content << content_tag(:li, pluralize(group.subgroup_count, _('subgroup'))) if group.subgroup_count > 0
list_content << content_tag(:li, pluralize(group.all_projects.non_archived.count, _('active project'))) if group.all_projects.non_archived.count > 0
list_content << content_tag(:li, pluralize(group.all_projects.archived.count, _('archived project'))) if group.all_projects.archived.count > 0
if list_content.present?
content_tag(:span, _(" This action will also remove:")) +
content_tag(:ul) do
list_content
end
else
''.html_safe
end
end
def remove_group_warning
message = _('After you remove a group, you %{strongOpen}cannot%{strongClose} restore it or its components.')
content_tag(:p, class: 'gl-mb-0') do
ERB::Util.html_escape(message) % {
strongOpen: '<strong>'.html_safe,
strongClose: '</strong>'.html_safe
}
end
end
def share_with_group_lock_help_text(group)

View File

@ -7,4 +7,12 @@ module ViteHelper
Gitlab::Utils.to_boolean(ViteRuby.env['VITE_ENABLED'], default: false)
end
def vite_hmr_websocket_url
ViteRuby.env['VITE_HMR_WS_URL']
end
def vite_hmr_http_url
ViteRuby.env['VITE_HMR_HTTP_URL']
end
end

View File

@ -21,7 +21,12 @@ module ViteGdk
hmr_host = hmr_config['host'] || host
hmr_port = hmr_config['clientPort'] || hmr_config['port'] || port
hmr_ws_protocol = hmr_config['protocol'] || 'ws'
hmr_http_protocol = hmr_ws_protocol == 'wss' ? 'https' : 'http'
ViteRuby.env['VITE_HMR_HOST'] = hmr_host
# If the Websocket connection to the HMR host is not up, Vite will attempt to
# ping the HMR host via HTTP or HTTPS:
# https://github.com/vitejs/vite/blob/899d9b1d272b7057aafc6fa01570d40f288a473b/packages/vite/src/client/client.ts#L320-L327
ViteRuby.env['VITE_HMR_HTTP_URL'] = "#{hmr_http_protocol}://#{hmr_host}:#{hmr_port}"
ViteRuby.env['VITE_HMR_WS_URL'] = "#{hmr_ws_protocol}://#{hmr_host}:#{hmr_port}"
ViteRuby.configure(

View File

@ -33,6 +33,9 @@ msgstr ""
msgid " Please sign in."
msgstr ""
msgid " This action will also remove:"
msgstr ""
msgid " Try to %{action} this file again."
msgstr ""
@ -4524,6 +4527,9 @@ msgstr ""
msgid "After you enable the integration, the following protected variables are created for CI/CD use:"
msgstr ""
msgid "After you remove a group, you %{strongOpen}cannot%{strongClose} restore it or its components."
msgstr ""
msgid "After you've reviewed these contribution guidelines, you'll be all set to"
msgstr ""
@ -57863,6 +57869,9 @@ msgstr ""
msgid "You are about to incur additional charges"
msgstr ""
msgid "You are about to remove the group %{group_name}."
msgstr ""
msgid "You are already a member of this %{member_source}."
msgstr ""
@ -57890,9 +57899,6 @@ msgstr ""
msgid "You are going to delete %{project_full_name}. Deleted projects CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
msgid "You are going to remove %{group_name}. This will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
msgid "You are going to remove the fork relationship from %{project_full_name}. Are you ABSOLUTELY sure?"
msgstr ""
@ -59053,6 +59059,9 @@ msgstr[1] ""
msgid "access:"
msgstr ""
msgid "active project"
msgstr ""
msgid "add at least one file to the repository"
msgstr ""
@ -59121,6 +59130,9 @@ msgid_plural "approvals"
msgstr[0] ""
msgstr[1] ""
msgid "archived project"
msgstr ""
msgid "archived:"
msgstr ""
@ -61046,6 +61058,9 @@ msgstr ""
msgid "stuck"
msgstr ""
msgid "subgroup"
msgstr ""
msgid "success"
msgstr ""

View File

@ -5,12 +5,12 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { getPreferredLocales } from '~/locale';
import ProjectStorageApp from '~/usage_quotas/storage/components/project_storage_app.vue';
import ProjectStorageApp from '~/usage_quotas/storage/project/components/project_storage_app.vue';
import SectionedPercentageBar from '~/usage_quotas/components/sectioned_percentage_bar.vue';
import {
descendingStorageUsageSort,
getStorageTypesFromProjectStatistics,
} from '~/usage_quotas/storage/utils';
} from '~/usage_quotas/storage/project/utils';
import {
storageTypeHelpPaths,
PROJECT_STORAGE_TYPES,
@ -24,7 +24,7 @@ import {
mockGetProjectStorageStatisticsGraphQLResponse,
mockEmptyResponse,
defaultProjectProvideValues,
} from '../mock_data';
} from '../../mock_data';
Vue.use(VueApollo);
@ -34,6 +34,7 @@ jest.mock('~/locale', () => ({
}));
describe('ProjectStorageApp', () => {
/** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
let wrapper;
const createMockApolloProvider = ({ reject = false, mockedValue } = {}) => {

View File

@ -1,11 +1,11 @@
import { GlTableLite } from '@gitlab/ui';
import { mount, Wrapper } from '@vue/test-utils'; // eslint-disable-line no-unused-vars
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ProjectStorageDetail from '~/usage_quotas/storage/components/project_storage_detail.vue';
import ProjectStorageDetail from '~/usage_quotas/storage/project/components/project_storage_detail.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
describe('ProjectStorageDetail', () => {
/** @type { Wrapper } */
/** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
let wrapper;
const generateStorageType = (props) => {

View File

@ -1,8 +1,9 @@
import { mount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import StorageTypeIcon from '~/usage_quotas/storage/components/storage_type_icon.vue';
import StorageTypeIcon from '~/usage_quotas/storage/project/components/storage_type_icon.vue';
describe('StorageTypeIcon', () => {
/** @type {import('@vue/test-utils').Wrapper} */
let wrapper;
const createComponent = (props = {}) => {

View File

@ -0,0 +1,72 @@
import { PROJECT_STORAGE_TYPES } from '~/usage_quotas/storage/constants';
import {
getStorageTypesFromProjectStatistics,
descendingStorageUsageSort,
} from '~/usage_quotas/storage/project/utils';
import { mockGetProjectStorageStatisticsGraphQLResponse } from 'jest/usage_quotas/storage/mock_data';
describe('getStorageTypesFromProjectStatistics', () => {
const {
statistics: projectStatistics,
statisticsDetailsPaths,
} = mockGetProjectStorageStatisticsGraphQLResponse.data.project;
describe('matches project statistics value with matching storage type', () => {
const typesWithStats = getStorageTypesFromProjectStatistics(
PROJECT_STORAGE_TYPES,
projectStatistics,
);
it.each(PROJECT_STORAGE_TYPES)('storage type: $id', ({ id }) => {
expect(typesWithStats).toContainEqual(
expect.objectContaining({
id,
value: projectStatistics[`${id}Size`],
}),
);
});
});
it('adds helpPath to a relevant type', () => {
const helpLinks = PROJECT_STORAGE_TYPES.reduce((acc, { id }) => {
return {
...acc,
[id]: `url://${id}`,
};
}, {});
const typesWithStats = getStorageTypesFromProjectStatistics(
PROJECT_STORAGE_TYPES,
projectStatistics,
{},
helpLinks,
);
typesWithStats.forEach((type) => {
expect(type.helpPath).toBe(helpLinks[type.id]);
});
});
it('adds details page path', () => {
const typesWithStats = getStorageTypesFromProjectStatistics(
PROJECT_STORAGE_TYPES,
projectStatistics,
statisticsDetailsPaths,
{},
);
typesWithStats.forEach((type) => {
expect(type.detailsPath).toBe(statisticsDetailsPaths[type.id]);
});
});
});
describe('descendingStorageUsageSort', () => {
it('sorts items by a given key in descending order', () => {
const items = [{ k: 1 }, { k: 3 }, { k: 2 }];
const sorted = [...items].sort(descendingStorageUsageSort('k'));
const expectedSorted = [{ k: 3 }, { k: 2 }, { k: 1 }];
expect(sorted).toEqual(expectedSorted);
});
});

View File

@ -1,79 +1,5 @@
import { PROJECT_STORAGE_TYPES } from '~/usage_quotas/storage/constants';
import {
getStorageTypesFromProjectStatistics,
descendingStorageUsageSort,
parseGetStorageResults,
} from '~/usage_quotas/storage/utils';
import {
mockGetProjectStorageStatisticsGraphQLResponse,
mockGetNamespaceStorageGraphQLResponse,
} from 'jest/usage_quotas/storage/mock_data';
describe('getStorageTypesFromProjectStatistics', () => {
const {
statistics: projectStatistics,
statisticsDetailsPaths,
} = mockGetProjectStorageStatisticsGraphQLResponse.data.project;
describe('matches project statistics value with matching storage type', () => {
const typesWithStats = getStorageTypesFromProjectStatistics(
PROJECT_STORAGE_TYPES,
projectStatistics,
);
it.each(PROJECT_STORAGE_TYPES)('storage type: $id', ({ id }) => {
expect(typesWithStats).toContainEqual(
expect.objectContaining({
id,
value: projectStatistics[`${id}Size`],
}),
);
});
});
it('adds helpPath to a relevant type', () => {
const helpLinks = PROJECT_STORAGE_TYPES.reduce((acc, { id }) => {
return {
...acc,
[id]: `url://${id}`,
};
}, {});
const typesWithStats = getStorageTypesFromProjectStatistics(
PROJECT_STORAGE_TYPES,
projectStatistics,
{},
helpLinks,
);
typesWithStats.forEach((type) => {
expect(type.helpPath).toBe(helpLinks[type.id]);
});
});
it('adds details page path', () => {
const typesWithStats = getStorageTypesFromProjectStatistics(
PROJECT_STORAGE_TYPES,
projectStatistics,
statisticsDetailsPaths,
{},
);
typesWithStats.forEach((type) => {
expect(type.detailsPath).toBe(statisticsDetailsPaths[type.id]);
});
});
});
describe('descendingStorageUsageSort', () => {
it('sorts items by a given key in descending order', () => {
const items = [{ k: 1 }, { k: 3 }, { k: 2 }];
const sorted = [...items].sort(descendingStorageUsageSort('k'));
const expectedSorted = [{ k: 3 }, { k: 2 }, { k: 1 }];
expect(sorted).toEqual(expectedSorted);
});
});
import { parseGetStorageResults } from '~/usage_quotas/storage/utils';
import { mockGetNamespaceStorageGraphQLResponse } from 'jest/usage_quotas/storage/mock_data';
describe('parseGetStorageResults', () => {
it('returns the object keys we use', () => {

View File

@ -29,7 +29,8 @@ RSpec.describe Groups::SettingsHelper do
remove_form_id: form_value_id,
phrase: group.full_path,
button_testid: "remove-group-button",
disabled: is_button_disabled
disabled: is_button_disabled,
html_confirmation_message: 'true'
})
end
end

View File

@ -28,6 +28,7 @@ RSpec.describe ViteGdk, feature_category: :tooling do
expect(ViteRuby).to receive(:configure).with(host: 'gdk.test', port: 3038)
expect(ViteRuby.env).to receive(:[]=).with('VITE_ENABLED', 'true')
expect(ViteRuby.env).to receive(:[]=).with('VITE_HMR_HOST', 'gdk.test')
expect(ViteRuby.env).to receive(:[]=).with('VITE_HMR_HTTP_URL', 'http://gdk.test:3038')
expect(ViteRuby.env).to receive(:[]=).with('VITE_HMR_WS_URL', 'ws://gdk.test:3038')
described_class.load_gdk_vite_config
@ -52,6 +53,7 @@ RSpec.describe ViteGdk, feature_category: :tooling do
expect(ViteRuby).to receive(:configure).with(host: 'gdk.test', port: 3038)
expect(ViteRuby.env).to receive(:[]=).with('VITE_ENABLED', 'true')
expect(ViteRuby.env).to receive(:[]=).with('VITE_HMR_HOST', 'hmr.gdk.test')
expect(ViteRuby.env).to receive(:[]=).with('VITE_HMR_HTTP_URL', 'https://hmr.gdk.test:9999')
expect(ViteRuby.env).to receive(:[]=).with('VITE_HMR_WS_URL', 'wss://hmr.gdk.test:9999')
described_class.load_gdk_vite_config
@ -76,6 +78,7 @@ RSpec.describe ViteGdk, feature_category: :tooling do
expect(ViteRuby).to receive(:configure).with(host: 'gdk.test', port: 3038)
expect(ViteRuby.env).to receive(:[]=).with('VITE_ENABLED', 'true')
expect(ViteRuby.env).to receive(:[]=).with('VITE_HMR_HOST', 'hmr.gdk.test')
expect(ViteRuby.env).to receive(:[]=).with('VITE_HMR_HTTP_URL', 'https://hmr.gdk.test:3038')
expect(ViteRuby.env).to receive(:[]=).with('VITE_HMR_WS_URL', 'wss://hmr.gdk.test:3038')
described_class.load_gdk_vite_config

View File

@ -47,31 +47,40 @@ RSpec.shared_examples 'Base action controller' do
end
context 'when configuring vite' do
let(:vite_origin) { "#{ViteRuby.instance.config.host}:#{ViteRuby.instance.config.port}" }
let(:vite_hmr_websocket_url) { "ws://gitlab.example.com:3808" }
let(:vite_hmr_http_url) { "http://gitlab.example.com:3808" }
let(:vite_gitlab_url) { Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'vite-dev/') }
context 'when vite enabled during development',
skip: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424334' do
before do
stub_rails_env('development')
allow(ViteHelper).to receive(:vite_enabled?).and_return(true)
allow(BaseActionController.helpers).to receive(:vite_enabled?).and_return(true)
allow(BaseActionController.helpers).to receive(:vite_hmr_websocket_url).and_return(vite_hmr_websocket_url)
allow(BaseActionController.helpers).to receive(:vite_hmr_http_url).and_return(vite_hmr_http_url)
end
it 'adds vite csp' do
request
expect(response.headers['Content-Security-Policy']).to include(vite_origin)
expect(response.headers['Content-Security-Policy']).to include("#{vite_hmr_websocket_url}/vite-dev/")
expect(response.headers['Content-Security-Policy']).to include("#{vite_hmr_http_url}/vite-dev/")
expect(response.headers['Content-Security-Policy']).to include(vite_gitlab_url)
end
end
context 'when vite disabled' do
before do
allow(ViteHelper).to receive(:vite_enabled?).and_return(false)
allow(BaseActionController.helpers).to receive(:vite_enabled?).and_return(false)
end
it "doesn't add vite csp" do
request
expect(response.headers['Content-Security-Policy']).not_to include(vite_origin)
expect(response.headers['Content-Security-Policy']).not_to include(vite_hmr_websocket_url)
expect(response.headers['Content-Security-Policy']).not_to include(vite_hmr_http_url)
expect(response.headers['Content-Security-Policy']).not_to include(vite_gitlab_url)
end
end
end

View File

@ -26,6 +26,7 @@ def webmock_allowed_hosts
if ViteRuby.env['VITE_ENABLED'] == "true"
hosts << ViteRuby.instance.config.host
hosts << ViteRuby.env['VITE_HMR_HOST']
end
end.compact.uniq
end