diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 6f858f0e0a6..6019fe636a8 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -1118,7 +1118,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab /ee/lib/audit/group_push_rules_changes_auditor.rb @gitlab-org/manage/compliance /ee/lib/ee/api/entities/audit_event.rb @gitlab-org/manage/compliance /ee/lib/ee/audit/ @gitlab-org/manage/compliance -/ee/lib/gitlab/audit/auditor.rb @gitlab-org/manage/compliance +/ee/lib/ee/gitlab/audit/ @gitlab-org/manage/compliance /ee/spec/controllers/admin/audit_log_reports_controller_spec.rb @gitlab-org/manage/compliance /ee/spec/controllers/admin/audit_logs_controller_spec.rb @gitlab-org/manage/compliance /ee/spec/controllers/groups/audit_events_controller_spec.rb @gitlab-org/manage/compliance @@ -1163,13 +1163,10 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab /ee/spec/support/shared_examples/features/audit_events_filter_shared_examples.rb @gitlab-org/manage/compliance /ee/spec/support/shared_examples/services/audit_event_logging_shared_examples.rb @gitlab-org/manage/compliance /ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb @gitlab-org/manage/compliance +/lib/gitlab/audit/auditor.rb @gitlab-org/manage/compliance /lib/gitlab/audit_json_logger.rb @gitlab-org/manage/compliance -/qa/qa/ee/page/admin/monitoring/ @gitlab-org/manage/compliance -/qa/qa/specs/features/ee/browser_ui/1_manage/group/group_audit_logs_1_spec.rb @gitlab-org/manage/compliance -/qa/qa/specs/features/ee/browser_ui/1_manage/group/group_audit_logs_2_spec.rb @gitlab-org/manage/compliance -/qa/qa/specs/features/ee/browser_ui/1_manage/instance/ @gitlab-org/manage/compliance -/qa/qa/specs/features/ee/browser_ui/1_manage/project/project_audit_logs_spec.rb @gitlab-org/manage/compliance /spec/factories/audit_events.rb @gitlab-org/manage/compliance +/spec/lib/gitlab/audit/auditor_spec.rb @gitlab-org/manage/compliance /spec/migrations/populate_audit_event_streaming_verification_token_spec.rb @gitlab-org/manage/compliance /spec/models/audit_event_spec.rb @gitlab-org/manage/compliance /spec/services/audit_event_service_spec.rb @gitlab-org/manage/compliance diff --git a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue new file mode 100644 index 00000000000..3af83ffa8ed --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue @@ -0,0 +1,104 @@ + + + diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql new file mode 100644 index 00000000000..f8e4dc55fa4 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql @@ -0,0 +1,30 @@ +#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql" + +mutation addGroupVariable( + $variable: CiVariable! + $endpoint: String! + $fullPath: ID! + $groupId: ID! +) { + addGroupVariable( + variable: $variable + endpoint: $endpoint + fullPath: $fullPath + groupId: $groupId + ) @client { + group { + id + ciVariables { + nodes { + ...BaseCiVariable + ... on CiGroupVariable { + environmentScope + masked + protected + } + } + } + } + errors + } +} diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql new file mode 100644 index 00000000000..310e4a6e551 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql @@ -0,0 +1,30 @@ +#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql" + +mutation deleteGroupVariable( + $variable: CiVariable! + $endpoint: String! + $fullPath: ID! + $groupId: ID! +) { + deleteGroupVariable( + variable: $variable + endpoint: $endpoint + fullPath: $fullPath + groupId: $groupId + ) @client { + group { + id + ciVariables { + nodes { + ...BaseCiVariable + ... on CiGroupVariable { + environmentScope + masked + protected + } + } + } + } + errors + } +} diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql new file mode 100644 index 00000000000..5291942eb87 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql @@ -0,0 +1,30 @@ +#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql" + +mutation updateGroupVariable( + $variable: CiVariable! + $endpoint: String! + $fullPath: ID! + $groupId: ID! +) { + updateGroupVariable( + variable: $variable + endpoint: $endpoint + fullPath: $fullPath + groupId: $groupId + ) @client { + group { + id + ciVariables { + nodes { + ...BaseCiVariable + ... on CiGroupVariable { + environmentScope + masked + protected + } + } + } + } + errors + } +} diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql new file mode 100644 index 00000000000..c6dd6d4faaf --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql @@ -0,0 +1,17 @@ +#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql" + +query getGroupVariables($fullPath: ID!) { + group(fullPath: $fullPath) { + id + ciVariables { + nodes { + ...BaseCiVariable + ... on CiGroupVariable { + environmentScope + masked + protected + } + } + } + } +} diff --git a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js b/app/assets/javascripts/ci_variable_list/graphql/resolvers.js index 7b57e97a4b8..be7e3f88cfd 100644 --- a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js +++ b/app/assets/javascripts/ci_variable_list/graphql/resolvers.js @@ -4,8 +4,9 @@ import { convertObjectPropsToSnakeCase, } from '../../lib/utils/common_utils'; import { getIdFromGraphQLId } from '../../graphql_shared/utils'; -import { instanceString } from '../constants'; +import { GRAPHQL_GROUP_TYPE, groupString, instanceString } from '../constants'; import getAdminVariables from './queries/variables.query.graphql'; +import getGroupVariables from './queries/group_variables.query.graphql'; const prepareVariableForApi = ({ variable, destroy = false }) => { return { @@ -27,6 +28,20 @@ const mapVariableTypes = (variables = [], kind) => { }); }; +const prepareGroupGraphQLResponse = ({ data, groupId, errors = [] }) => { + return { + errors, + group: { + __typename: GRAPHQL_GROUP_TYPE, + id: groupId, + ciVariables: { + __typename: 'CiVariableConnection', + nodes: mapVariableTypes(data.variables, groupString), + }, + }, + }; +}; + const prepareAdminGraphQLResponse = ({ data, errors = [] }) => { return { errors, @@ -37,6 +52,28 @@ const prepareAdminGraphQLResponse = ({ data, errors = [] }) => { }; }; +const callGroupEndpoint = async ({ + endpoint, + fullPath, + variable, + groupId, + cache, + destroy = false, +}) => { + try { + const { data } = await axios.patch(endpoint, { + variables_attributes: [prepareVariableForApi({ variable, destroy })], + }); + return prepareGroupGraphQLResponse({ data, groupId }); + } catch (e) { + return prepareGroupGraphQLResponse({ + data: cache.readQuery({ query: getGroupVariables, variables: { fullPath } }), + groupId, + errors: [...e.response.data], + }); + } +}; + const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false }) => { try { const { data } = await axios.patch(endpoint, { @@ -54,6 +91,15 @@ const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false }) export const resolvers = { Mutation: { + addGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => { + return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache }); + }, + updateGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => { + return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache }); + }, + deleteGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => { + return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache, destroy: true }); + }, addAdminVariable: async (_, { endpoint, variable }, { cache }) => { return callAdminEndpoint({ endpoint, variable, cache }); }, diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js index 713a453561e..a74af8aed12 100644 --- a/app/assets/javascripts/ci_variable_list/index.js +++ b/app/assets/javascripts/ci_variable_list/index.js @@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; import CiAdminVariables from './components/ci_admin_variables.vue'; +import CiGroupVariables from './components/ci_group_variables.vue'; import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue'; import { resolvers } from './graphql/resolvers'; import createStore from './store'; @@ -32,7 +33,11 @@ const mountCiVariableListApp = (containerEl) => { const parsedIsGroup = parseBoolean(isGroup); const isProtectedByDefault = parseBoolean(protectedByDefault); - const component = CiAdminVariables; + let component = CiAdminVariables; + + if (parsedIsGroup) { + component = CiGroupVariables; + } Vue.use(VueApollo); diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index 72fec17ac9d..f4b939fb20f 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -23,6 +23,9 @@ import { SEARCH_SHORTCUTS_MIN_CHARACTERS, SCOPE_TOKEN_MAX_LENGTH, INPUT_FIELD_PADDING, + IS_SEARCHING, + IS_FOCUSED, + IS_NOT_FOCUSED, } from '../constants'; import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue'; import HeaderSearchDefaultItems from './header_search_default_items.vue'; @@ -65,6 +68,7 @@ export default { data() { return { showDropdown: false, + isFocused: false, currentFocusIndex: SEARCH_BOX_INDEX, }; }, @@ -92,20 +96,18 @@ export default { if (!this.showDropdown || !this.isLoggedIn) { return false; } - return this.searchOptions?.length > 0; }, showDefaultItems() { return !this.searchText; }, - showScopes() { + searchTermOverMin() { return this.searchText?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS; }, defaultIndex() { if (this.showDefaultItems) { return SEARCH_BOX_INDEX; } - return FIRST_DROPDOWN_INDEX; }, @@ -132,12 +134,15 @@ export default { count: this.searchOptions.length, }); }, - searchBarStateIndicator() { - const hasIcon = - this.searchContext?.project || this.searchContext?.group ? 'has-icon' : 'has-no-icon'; - const isSearching = this.showScopes ? 'is-searching' : 'is-not-searching'; - const isActive = this.showSearchDropdown ? 'is-active' : 'is-not-active'; - return `${isActive} ${isSearching} ${hasIcon}`; + searchBarClasses() { + return { + [IS_SEARCHING]: this.searchTermOverMin, + [IS_FOCUSED]: this.isFocused, + [IS_NOT_FOCUSED]: !this.isFocused, + }; + }, + showScopeHelp() { + return this.searchTermOverMin && this.isFocused; }, searchBarItem() { return this.searchOptions?.[0]; @@ -158,11 +163,22 @@ export default { ...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']), openDropdown() { this.showDropdown = true; - this.$emit('toggleDropdown', this.showDropdown); + this.isFocused = true; + this.$emit('expandSearchBar', true); }, closeDropdown() { this.showDropdown = false; - this.$emit('toggleDropdown', this.showDropdown); + }, + collapseAndCloseSearchBar() { + // we need a delay on this method + // for the search bar not to remove + // the clear button from dom + // and register clicks on dropdown items + setTimeout(() => { + this.showDropdown = false; + this.isFocused = false; + this.$emit('collapseSearchBar'); + }, 200); }, submitSearch() { if (this.search?.length <= SEARCH_SHORTCUTS_MIN_CHARACTERS && this.currentFocusIndex < 0) { @@ -171,6 +187,7 @@ export default { return visitUrl(this.currentFocusedOption?.url || this.searchQuery); }, getAutocompleteOptions: debounce(function debouncedSearch(searchTerm) { + this.openDropdown(); if (!searchTerm) { this.clearAutocomplete(); } else { @@ -201,7 +218,7 @@ export default { role="search" :aria-label="$options.i18n.searchGitlab" class="header-search gl-relative gl-rounded-base gl-w-full" - :class="searchBarStateIndicator" + :class="searchBarClasses" data-testid="header-search-form" >