Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/merge_request_templates/Documentation.md13
-rw-r--r--app/assets/javascripts/pages/projects/usage_quotas/index.js23
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js6
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js7
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/app.vue13
-rw-r--r--app/assets/javascripts/projects/storage_counter/index.js17
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue32
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue88
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/status_token_config.js32
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token.vue1
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token_config.js12
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/type_token_config.js20
-rw-r--r--app/assets/javascripts/runner/constants.js6
-rw-r--r--app/assets/javascripts/runner/graphql/get_group_runners.query.graphql35
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue137
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js11
-rw-r--r--app/controllers/groups/runners_controller.rb2
-rw-r--r--app/controllers/projects/usage_quotas_controller.rb16
-rw-r--r--app/views/groups/runners/index.html.haml2
-rw-r--r--app/views/projects/usage_quotas/index.html.haml17
-rw-r--r--config/feature_flags/development/project_storage_ui.yml8
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/routes/project.rb2
-rw-r--r--doc/administration/geo/index.md4
-rw-r--r--doc/administration/geo/replication/configuration.md2
-rw-r--r--doc/administration/geo/replication/updating_the_geo_nodes.md53
-rw-r--r--doc/administration/geo/replication/updating_the_geo_sites.md52
-rw-r--r--doc/administration/geo/replication/version_specific_updates.md2
-rw-r--r--doc/administration/pages/index.md3
-rw-r--r--doc/api/discussions.md182
-rw-r--r--doc/architecture/blueprints/database/scalability/patterns/img/db_terminology_v14_2.pngbin51264 -> 0 bytes
-rw-r--r--doc/development/snowplow/index.md2
-rw-r--r--lib/sidebars/projects/menus/settings_menu.rb14
-rw-r--r--lib/system_check/incoming_email_check.rb8
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb13
-rw-r--r--spec/deprecation_toolkit_env.rb4
-rw-r--r--spec/frontend/fixtures/runner.rb47
-rw-r--r--spec/frontend/projects/storage_counter/app_spec.js22
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js97
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js33
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js2
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js239
-rw-r--r--spec/frontend/runner/mock_data.js16
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb26
-rw-r--r--spec/lib/system_check/incoming_email_check_spec.rb54
-rw-r--r--spec/requests/projects/usage_quotas_spec.rb53
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb3
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb26
50 files changed, 1171 insertions, 295 deletions
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index 63b50db08ea..303d3793ad8 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -1,13 +1,3 @@
-<!--
- Follow the documentation workflow https://docs.gitlab.com/ee/development/documentation/workflow.html
- Additional information is located at https://docs.gitlab.com/ee/development/documentation/
- To find the designated Tech Writer for the stage/group, see
- https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
-
- Mention "documentation" or "docs" in the MR title
- For changing documentation location use the Change Documentation Location.md template
--->
-
## What does this MR do?
<!-- Briefly describe what this MR is about. -->
@@ -20,7 +10,8 @@
- Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4)
- [ ] Follow the:
- - [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/).
+ - [Documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html).
+ - [Documentation guidelines](https://docs.gitlab.com/ee/development/documentation/).
- [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/).
- [ ] Ensure that the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#product-tier-badges) is added to topic's `h1`.
- [ ] [Request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-reviewers-feature) based on the:
diff --git a/app/assets/javascripts/pages/projects/usage_quotas/index.js b/app/assets/javascripts/pages/projects/usage_quotas/index.js
new file mode 100644
index 00000000000..9cd80b85c8a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/usage_quotas/index.js
@@ -0,0 +1,23 @@
+import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
+import storageCounter from '~/projects/storage_counter';
+import initSearchSettings from '~/search_settings';
+
+const initLinkedTabs = () => {
+ if (!document.querySelector('.js-usage-quota-tabs')) {
+ return false;
+ }
+
+ return new LinkedTabs({
+ defaultAction: '#storage-quota-tab',
+ parentEl: '.js-usage-quota-tabs',
+ hashedTabs: true,
+ });
+};
+
+const initVueApp = () => {
+ storageCounter('js-project-storage-count-app');
+};
+
+initVueApp();
+initLinkedTabs();
+initSearchSettings();
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 976ebe80aee..89b9091e6f9 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -56,7 +56,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers, { typeDefs, useGet: true }),
+ defaultClient: createDefaultClient(resolvers, {
+ typeDefs,
+ useGet: true,
+ assumeImmutableResults: true,
+ }),
});
const { cache } = apolloProvider.clients.defaultClient;
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index cff6f549b6e..8650db42076 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -134,7 +134,7 @@ export default {
update(data) {
const { ciConfig } = data || {};
const stageNodes = ciConfig?.stages?.nodes || [];
- const stages = unwrapStagesWithNeeds(stageNodes);
+ const stages = unwrapStagesWithNeeds(JSON.parse(JSON.stringify(stageNodes)));
return { ...ciConfig, stages };
},
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
index 1d4ec4c110b..2505c47147f 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
@@ -5,7 +5,12 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ ),
});
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
diff --git a/app/assets/javascripts/projects/storage_counter/components/app.vue b/app/assets/javascripts/projects/storage_counter/components/app.vue
new file mode 100644
index 00000000000..19f9d29efaa
--- /dev/null
+++ b/app/assets/javascripts/projects/storage_counter/components/app.vue
@@ -0,0 +1,13 @@
+<script>
+import { s__ } from '~/locale';
+
+export default {
+ name: 'StorageCounterApp',
+ i18n: {
+ placeholder: s__('UsageQuota|Usage'),
+ },
+};
+</script>
+<template>
+ <div>{{ $options.i18n.placeholder }}</div>
+</template>
diff --git a/app/assets/javascripts/projects/storage_counter/index.js b/app/assets/javascripts/projects/storage_counter/index.js
new file mode 100644
index 00000000000..915ee263063
--- /dev/null
+++ b/app/assets/javascripts/projects/storage_counter/index.js
@@ -0,0 +1,17 @@
+import Vue from 'vue';
+import StorageCounterApp from './components/app.vue';
+
+export default (containerId = 'js-project-storage-count-app') => {
+ const el = document.getElementById(containerId);
+
+ if (!el) {
+ return false;
+ }
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(StorageCounterApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index 23ecee449a4..fedd2519958 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -2,12 +2,16 @@
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
+import { formatNumber, sprintf, __ } from '~/locale';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
-import { INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
+import { statusTokenConfig } from '../components/search_tokens/status_token_config';
+import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
+import { typeTokenConfig } from '../components/search_tokens/type_token_config';
+import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
import {
fromUrlQueryToSearch,
@@ -78,6 +82,21 @@ export default {
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
+ activeRunnersMessage() {
+ return sprintf(__('Runners currently online: %{active_runners_count}'), {
+ active_runners_count: formatNumber(this.activeRunnersCount),
+ });
+ },
+ searchTokens() {
+ return [
+ statusTokenConfig,
+ typeTokenConfig,
+ {
+ ...tagTokenConfig,
+ recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
+ },
+ ];
+ },
},
watch: {
search: {
@@ -99,6 +118,7 @@ export default {
captureException({ error, component: this.$options.name });
},
},
+ filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE,
};
</script>
@@ -118,9 +138,13 @@ export default {
<runner-filtered-search-bar
v-model="search"
- namespace="admin_runners"
- :active-runners-count="activeRunnersCount"
- />
+ :tokens="searchTokens"
+ :namespace="$options.filteredSearchNamespace"
+ >
+ <template #runner-count>
+ {{ activeRunnersMessage }}
+ </template>
+ </runner-filtered-search-bar>
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
index e14b3b17fa8..e04ca8ddca0 100644
--- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
@@ -1,27 +1,8 @@
<script>
import { cloneDeep } from 'lodash';
-import { formatNumber, sprintf, __, s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { __ } from '~/locale';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import {
- STATUS_ACTIVE,
- STATUS_PAUSED,
- STATUS_ONLINE,
- STATUS_OFFLINE,
- STATUS_NOT_CONNECTED,
- INSTANCE_TYPE,
- GROUP_TYPE,
- PROJECT_TYPE,
- CREATED_DESC,
- CREATED_ASC,
- CONTACTED_DESC,
- CONTACTED_ASC,
- PARAM_KEY_STATUS,
- PARAM_KEY_RUNNER_TYPE,
- PARAM_KEY_TAG,
-} from '../constants';
-import TagToken from './search_tokens/tag_token.vue';
+import { CREATED_DESC, CREATED_ASC, CONTACTED_DESC, CONTACTED_ASC } from '../constants';
const sortOptions = [
{
@@ -58,10 +39,6 @@ export default {
type: String,
required: true,
},
- activeRunnersCount: {
- type: Number,
- required: true,
- },
},
data() {
// filtered_search_bar_root.vue may mutate the inital
@@ -73,62 +50,6 @@ export default {
initialSortBy: sort,
};
},
- computed: {
- searchTokens() {
- return [
- {
- icon: 'status',
- title: __('Status'),
- type: PARAM_KEY_STATUS,
- token: BaseToken,
- unique: true,
- options: [
- { value: STATUS_ACTIVE, title: s__('Runners|Active') },
- { value: STATUS_PAUSED, title: s__('Runners|Paused') },
- { value: STATUS_ONLINE, title: s__('Runners|Online') },
- { value: STATUS_OFFLINE, title: s__('Runners|Offline') },
-
- // Added extra quotes in this title to avoid splitting this value:
- // see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
- { value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
- ],
- // TODO In principle we could support more complex search rules,
- // this can be added to a separate issue.
- operators: OPERATOR_IS_ONLY,
- },
-
- {
- icon: 'file-tree',
- title: __('Type'),
- type: PARAM_KEY_RUNNER_TYPE,
- token: BaseToken,
- unique: true,
- options: [
- { value: INSTANCE_TYPE, title: s__('Runners|instance') },
- { value: GROUP_TYPE, title: s__('Runners|group') },
- { value: PROJECT_TYPE, title: s__('Runners|project') },
- ],
- // TODO We should support more complex search rules,
- // search for multiple states (OR) or have NOT operators
- operators: OPERATOR_IS_ONLY,
- },
-
- {
- icon: 'tag',
- title: s__('Runners|Tags'),
- type: PARAM_KEY_TAG,
- token: TagToken,
- recentTokenValuesStorageKey: `${this.namespace}-recent-tags`,
- operators: OPERATOR_IS_ONLY,
- },
- ];
- },
- activeRunnersMessage() {
- return sprintf(__('Runners currently online: %{active_runners_count}'), {
- active_runners_count: formatNumber(this.activeRunnersCount),
- });
- },
- },
methods: {
onFilter(filters) {
const { sort } = this.value;
@@ -161,12 +82,13 @@ export default {
:sort-options="$options.sortOptions"
:initial-filter-value="initialFilterValue"
:initial-sort-by="initialSortBy"
- :tokens="searchTokens"
:search-input-placeholder="__('Search or filter results...')"
data-testid="runners-filtered-search"
@onFilter="onFilter"
@onSort="onSort"
/>
- <div class="gl-text-right" data-testid="active-runners-message">{{ activeRunnersMessage }}</div>
+ <div class="gl-text-right" data-testid="runner-count">
+ <slot name="runner-count"></slot>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
new file mode 100644
index 00000000000..03dff5e61a5
--- /dev/null
+++ b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
@@ -0,0 +1,32 @@
+import { __, s__ } from '~/locale';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import {
+ STATUS_ACTIVE,
+ STATUS_PAUSED,
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_NOT_CONNECTED,
+ PARAM_KEY_STATUS,
+} from '../../constants';
+
+export const statusTokenConfig = {
+ icon: 'status',
+ title: __('Status'),
+ type: PARAM_KEY_STATUS,
+ token: BaseToken,
+ unique: true,
+ options: [
+ { value: STATUS_ACTIVE, title: s__('Runners|Active') },
+ { value: STATUS_PAUSED, title: s__('Runners|Paused') },
+ { value: STATUS_ONLINE, title: s__('Runners|Online') },
+ { value: STATUS_OFFLINE, title: s__('Runners|Offline') },
+
+ // Added extra quotes in this title to avoid splitting this value:
+ // see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
+ { value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
+ ],
+ // TODO In principle we could support more complex search rules,
+ // this can be added to a separate issue.
+ operators: OPERATOR_IS_ONLY,
+};
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
index 51fae60b6b7..ab67ac608e2 100644
--- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
@@ -33,6 +33,7 @@ export default {
// The API should
// 1) scope to the rights of the user
// 2) stay up to date to the removal of old tags
+ // 3) consider the scope of search, like searching within the tags of a group
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796
return axios
.get(TAG_SUGGESTIONS_PATH, {
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token_config.js b/app/assets/javascripts/runner/components/search_tokens/tag_token_config.js
new file mode 100644
index 00000000000..fdeba714385
--- /dev/null
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token_config.js
@@ -0,0 +1,12 @@
+import { s__ } from '~/locale';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { PARAM_KEY_TAG } from '../../constants';
+import TagToken from './tag_token.vue';
+
+export const tagTokenConfig = {
+ icon: 'tag',
+ title: s__('Runners|Tags'),
+ type: PARAM_KEY_TAG,
+ token: TagToken,
+ operators: OPERATOR_IS_ONLY,
+};
diff --git a/app/assets/javascripts/runner/components/search_tokens/type_token_config.js b/app/assets/javascripts/runner/components/search_tokens/type_token_config.js
new file mode 100644
index 00000000000..1da61c53386
--- /dev/null
+++ b/app/assets/javascripts/runner/components/search_tokens/type_token_config.js
@@ -0,0 +1,20 @@
+import { __, s__ } from '~/locale';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, PARAM_KEY_RUNNER_TYPE } from '../../constants';
+
+export const typeTokenConfig = {
+ icon: 'file-tree',
+ title: __('Type'),
+ type: PARAM_KEY_RUNNER_TYPE,
+ token: BaseToken,
+ unique: true,
+ options: [
+ { value: INSTANCE_TYPE, title: s__('Runners|instance') },
+ { value: GROUP_TYPE, title: s__('Runners|group') },
+ { value: PROJECT_TYPE, title: s__('Runners|project') },
+ ],
+ // TODO We should support more complex search rules,
+ // search for multiple states (OR) or have NOT operators
+ operators: OPERATOR_IS_ONLY,
+};
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index 2822882e0cc..46e55b322c7 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -2,6 +2,7 @@ import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
export const RUNNER_JOB_COUNT_LIMIT = 1000;
+export const GROUP_RUNNER_COUNT_LIMIT = 1000;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
@@ -50,3 +51,8 @@ export const CONTACTED_DESC = 'CONTACTED_DESC'; // TODO Add this to the API
export const CONTACTED_ASC = 'CONTACTED_ASC';
export const DEFAULT_SORT = CREATED_DESC;
+
+// Local storage namespaces
+
+export const ADMIN_FILTERED_SEARCH_NAMESPACE = 'admin_runners';
+export const GROUP_FILTERED_SEARCH_NAMESPACE = 'group_runners';
diff --git a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
new file mode 100644
index 00000000000..a601ee8d611
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
@@ -0,0 +1,35 @@
+#import "~/runner/graphql/runner_node.fragment.graphql"
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getGroupRunners(
+ $groupFullPath: ID!
+ $before: String
+ $after: String
+ $first: Int
+ $last: Int
+ $status: CiRunnerStatus
+ $type: CiRunnerType
+ $search: String
+ $sort: CiRunnerSort
+) {
+ group(fullPath: $groupFullPath) {
+ runners(
+ membership: DESCENDANTS
+ before: $before
+ after: $after
+ first: $first
+ last: $last
+ status: $status
+ type: $type
+ search: $search
+ sort: $sort
+ ) {
+ nodes {
+ ...RunnerNode
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index 07bbf60c453..42e1a9e1de9 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -1,18 +1,135 @@
<script>
+import createFlash from '~/flash';
+import { fetchPolicies } from '~/lib/graphql';
+import { updateHistory } from '~/lib/utils/url_utility';
+import { formatNumber, sprintf, s__ } from '~/locale';
+import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
+import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
+import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
-import { GROUP_TYPE } from '../constants';
+import { statusTokenConfig } from '../components/search_tokens/status_token_config';
+import { typeTokenConfig } from '../components/search_tokens/type_token_config';
+import {
+ I18N_FETCH_ERROR,
+ GROUP_FILTERED_SEARCH_NAMESPACE,
+ GROUP_TYPE,
+ GROUP_RUNNER_COUNT_LIMIT,
+} from '../constants';
+import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql';
+import {
+ fromUrlQueryToSearch,
+ fromSearchToUrl,
+ fromSearchToVariables,
+} from '../runner_search_utils';
+import { captureException } from '../sentry_utils';
export default {
+ name: 'GroupRunnersApp',
components: {
+ RunnerFilteredSearchBar,
+ RunnerList,
RunnerManualSetupHelp,
RunnerTypeHelp,
+ RunnerPagination,
},
props: {
registrationToken: {
type: String,
required: true,
},
+ groupFullPath: {
+ type: String,
+ required: true,
+ },
+ groupRunnersLimitedCount: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ search: fromUrlQueryToSearch(),
+ runners: {
+ items: [],
+ pageInfo: {},
+ },
+ };
+ },
+ apollo: {
+ runners: {
+ query: getGroupRunnersQuery,
+ // Runners can be updated by users directly in this list.
+ // A "cache and network" policy prevents outdated filtered
+ // results.
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ variables() {
+ return this.variables;
+ },
+ update(data) {
+ const { runners } = data?.group || {};
+ return {
+ items: runners?.nodes || [],
+ pageInfo: runners?.pageInfo || {},
+ };
+ },
+ error(error) {
+ createFlash({ message: I18N_FETCH_ERROR });
+
+ this.reportToSentry(error);
+ },
+ },
+ },
+ computed: {
+ variables() {
+ return {
+ ...fromSearchToVariables(this.search),
+ groupFullPath: this.groupFullPath,
+ };
+ },
+ runnersLoading() {
+ return this.$apollo.queries.runners.loading;
+ },
+ noRunnersFound() {
+ return !this.runnersLoading && !this.runners.items.length;
+ },
+ groupRunnersCount() {
+ if (this.groupRunnersLimitedCount > GROUP_RUNNER_COUNT_LIMIT) {
+ return `${formatNumber(GROUP_RUNNER_COUNT_LIMIT)}+`;
+ }
+ return formatNumber(this.groupRunnersLimitedCount);
+ },
+ runnerCountMessage() {
+ return sprintf(s__('Runners|Runners in this group: %{groupRunnersCount}'), {
+ groupRunnersCount: this.groupRunnersCount,
+ });
+ },
+ searchTokens() {
+ return [statusTokenConfig, typeTokenConfig];
+ },
+ filteredSearchNamespace() {
+ return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
+ },
+ },
+ watch: {
+ search: {
+ deep: true,
+ handler() {
+ // TODO Implement back button reponse using onpopstate
+ updateHistory({
+ url: fromSearchToUrl(this.search),
+ title: document.title,
+ });
+ },
+ },
+ },
+ errorCaptured(error) {
+ this.reportToSentry(error);
+ },
+ methods: {
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
+ },
},
GROUP_TYPE,
};
@@ -31,5 +148,23 @@ export default {
/>
</div>
</div>
+
+ <runner-filtered-search-bar
+ v-model="search"
+ :tokens="searchTokens"
+ :namespace="filteredSearchNamespace"
+ >
+ <template #runner-count>
+ {{ runnerCountMessage }}
+ </template>
+ </runner-filtered-search-bar>
+
+ <div v-if="noRunnersFound" class="gl-text-center gl-p-5">
+ {{ __('No runners found') }}
+ </div>
+ <template v-else>
+ <runner-list :runners="runners.items" :loading="runnersLoading" />
+ <runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js
index e14c583d73e..9545764c68d 100644
--- a/app/assets/javascripts/runner/group_runners/index.js
+++ b/app/assets/javascripts/runner/group_runners/index.js
@@ -12,7 +12,13 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
return null;
}
- const { registrationToken, groupId } = el.dataset;
+ const {
+ registrationToken,
+ runnerInstallHelpPage,
+ groupId,
+ groupFullPath,
+ groupRunnersLimitedCount,
+ } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
@@ -27,12 +33,15 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
el,
apolloProvider,
provide: {
+ runnerInstallHelpPage,
groupId,
},
render(h) {
return h(GroupRunnersApp, {
props: {
registrationToken,
+ groupFullPath,
+ groupRunnersLimitedCount: parseInt(groupRunnersLimitedCount, 10),
},
});
},
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index ff3a09a2d2d..f37c08da22a 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -10,6 +10,8 @@ class Groups::RunnersController < Groups::ApplicationController
feature_category :runner
def index
+ finder = Ci::RunnersFinder.new(current_user: current_user, params: { group: @group })
+ @group_runners_limited_count = finder.execute.except(:limit, :offset).page.total_count_with_limit(:all, limit: 1000)
end
def runner_list_group_view_vue_ui_enabled
diff --git a/app/controllers/projects/usage_quotas_controller.rb b/app/controllers/projects/usage_quotas_controller.rb
new file mode 100644
index 00000000000..6e2f7e45e8f
--- /dev/null
+++ b/app/controllers/projects/usage_quotas_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Projects::UsageQuotasController < Projects::ApplicationController
+ before_action :authorize_admin_project!
+ before_action :verify_usage_quotas_enabled!
+
+ layout "project_settings"
+
+ feature_category :utilization
+
+ private
+
+ def verify_usage_quotas_enabled!
+ render_404 unless Feature.enabled?(:project_storage_ui, project&.group, default_enabled: :yaml)
+ end
+end
diff --git a/app/views/groups/runners/index.html.haml b/app/views/groups/runners/index.html.haml
index 4e7bc99b1f0..78cd8fa10f0 100644
--- a/app/views/groups/runners/index.html.haml
+++ b/app/views/groups/runners/index.html.haml
@@ -3,4 +3,4 @@
%h2.page-title
= s_('Runners|Group Runners')
-#js-group-runners{ data: { registration_token: @group.runners_token, group_id: @group.id } }
+#js-group-runners{ data: { registration_token: @group.runners_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', group_id: @group.id, group_full_path: @group.full_path, group_runners_limited_count: @group_runners_limited_count } }
diff --git a/app/views/projects/usage_quotas/index.html.haml b/app/views/projects/usage_quotas/index.html.haml
new file mode 100644
index 00000000000..f45f8a2fd1d
--- /dev/null
+++ b/app/views/projects/usage_quotas/index.html.haml
@@ -0,0 +1,17 @@
+- page_title s_("UsageQuota|Usage")
+
+%h3.page-title
+ = s_('UsageQuota|Usage Quotas')
+
+.row
+ .col-sm-6
+ = s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_name: @project.name }
+
+.top-area.scrolling-tabs-container.inner-page-scroll-tabs
+ %ul.nav.nav-tabs.nav-links.scrolling-tabs.separator.js-usage-quota-tabs{ role: 'tablist' }
+ %li.nav-item
+ %a.nav-link#storage-quota{ data: { toggle: "tab", action: '#storage-quota-tab' }, href: '#storage-quota-tab', 'aria-controls': '#storage-quota-tab', 'aria-selected': 'true' }
+ = s_('UsageQuota|Storage')
+.tab-content
+ .tab-pane#storage-quota-tab
+ #js-project-storage-count-app
diff --git a/config/feature_flags/development/project_storage_ui.yml b/config/feature_flags/development/project_storage_ui.yml
new file mode 100644
index 00000000000..23a5b5c3d29
--- /dev/null
+++ b/config/feature_flags/development/project_storage_ui.yml
@@ -0,0 +1,8 @@
+---
+name: project_storage_ui
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68289
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334889
+milestone: '14.2'
+type: development
+group: group::utilization
+default_enabled: false
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index e71f1e1b028..34f8080ac23 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -251,6 +251,7 @@ Settings.gitlab_ci['url'] ||= Settings.__send__(:build_gitlab_ci
#
Settings['incoming_email'] ||= Settingslogic.new({})
Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
+Settings.incoming_email['inbox_method'] ||= 'imap'
#
# Service desk email
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 8ba9c100f71..8476f89142a 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -145,6 +145,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :packages_and_registries, only: [:show]
end
+ resources :usage_quotas, only: [:index]
+
resources :autocomplete_sources, only: [] do
collection do
get 'members'
diff --git a/doc/administration/geo/index.md b/doc/administration/geo/index.md
index 7175d41abd8..d2d53f52418 100644
--- a/doc/administration/geo/index.md
+++ b/doc/administration/geo/index.md
@@ -214,7 +214,7 @@ For information on configuring Geo, see [Geo configuration](replication/configur
### Updating Geo
-For information on how to update your Geo site(s) to the latest GitLab version, see [Updating the Geo sites](replication/updating_the_geo_nodes.md).
+For information on how to update your Geo site(s) to the latest GitLab version, see [Updating the Geo sites](replication/updating_the_geo_sites.md).
### Pausing and resuming replication
@@ -230,7 +230,7 @@ WARNING:
Pausing and resuming of replication is currently only supported for Geo installations using an
Omnibus GitLab-managed database. External databases are currently not supported.
-In some circumstances, like during [upgrades](replication/updating_the_geo_nodes.md) or a [planned failover](disaster_recovery/planned_failover.md), it is desirable to pause replication between the primary and secondary.
+In some circumstances, like during [upgrades](replication/updating_the_geo_sites.md) or a [planned failover](disaster_recovery/planned_failover.md), it is desirable to pause replication between the primary and secondary.
Pausing and resuming replication is done via a command line tool from the a node in the secondary site where the `postgresql` service is enabled.
diff --git a/doc/administration/geo/replication/configuration.md b/doc/administration/geo/replication/configuration.md
index 5b22741f578..5f98e16e378 100644
--- a/doc/administration/geo/replication/configuration.md
+++ b/doc/administration/geo/replication/configuration.md
@@ -338,7 +338,7 @@ when:
## Upgrading Geo
-See the [updating the Geo sites document](updating_the_geo_nodes.md).
+See the [updating the Geo sites document](updating_the_geo_sites.md).
## Troubleshooting
diff --git a/doc/administration/geo/replication/updating_the_geo_nodes.md b/doc/administration/geo/replication/updating_the_geo_nodes.md
index 03570048071..f07c8d547a4 100644
--- a/doc/administration/geo/replication/updating_the_geo_nodes.md
+++ b/doc/administration/geo/replication/updating_the_geo_nodes.md
@@ -1,52 +1,9 @@
---
-stage: Enablement
-group: Geo
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
-type: howto
+redirect_to: 'updating_the_geo_sites.md'
+remove_date: '2021-11-23'
---
-# Updating the Geo nodes **(PREMIUM SELF)**
+This file was moved to [another location](updating_the_geo_sites.md).
-WARNING:
-Read these sections carefully before updating your Geo nodes. Not following
-version-specific update steps may result in unexpected downtime. If you have
-any specific questions, [contact Support](https://about.gitlab.com/support/#contact-support).
-
-Updating Geo nodes involves performing:
-
-1. [Version-specific update steps](version_specific_updates.md), depending on the
- version being updated to or from.
-1. [General update steps](#general-update-steps), for all updates.
-
-## General update steps
-
-NOTE:
-These general update steps are not intended for [high-availability deployments](https://docs.gitlab.com/omnibus/update/README.html#multi-node--ha-deployment), and will cause downtime. If you want to avoid downtime, consider using [zero downtime updates](https://docs.gitlab.com/omnibus/update/README.html#zero-downtime-updates).
-
-To update the Geo nodes when a new GitLab version is released, update **primary**
-and all **secondary** nodes:
-
-1. **Optional:** [Pause replication on each **secondary** node.](../index.md#pausing-and-resuming-replication)
-1. Log into the **primary** node.
-1. [Update GitLab on the **primary** node using Omnibus](https://docs.gitlab.com/omnibus/update/#update-using-the-official-repositories).
-1. Log into each **secondary** node.
-1. [Update GitLab on each **secondary** node using Omnibus](https://docs.gitlab.com/omnibus/update/#update-using-the-official-repositories).
-1. If you paused replication in step 1, [resume replication on each **secondary**](../index.md#pausing-and-resuming-replication)
-1. [Test](#check-status-after-updating) **primary** and **secondary** nodes, and check version in each.
-
-### Check status after updating
-
-Now that the update process is complete, you may want to check whether
-everything is working correctly:
-
-1. Run the Geo Rake task on all nodes, everything should be green:
-
- ```shell
- sudo gitlab-rake gitlab:geo:check
- ```
-
-1. Check the **primary** node's Geo dashboard for any errors.
-1. Test the data replication by pushing code to the **primary** node and see if it
- is received by **secondary** nodes.
-
-If you encounter any issues, see the [Geo troubleshooting guide](troubleshooting.md).
+<!-- This redirect file can be deleted after <2021-11-23>. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
diff --git a/doc/administration/geo/replication/updating_the_geo_sites.md b/doc/administration/geo/replication/updating_the_geo_sites.md
new file mode 100644
index 00000000000..1213fd904ee
--- /dev/null
+++ b/doc/administration/geo/replication/updating_the_geo_sites.md
@@ -0,0 +1,52 @@
+---
+stage: Enablement
+group: Geo
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+type: howto
+---
+
+# Updating the Geo sites **(PREMIUM SELF)**
+
+WARNING:
+Read these sections carefully before updating your Geo sites. Not following
+version-specific update steps may result in unexpected downtime. If you have
+any specific questions, [contact Support](https://about.gitlab.com/support/#contact-support).
+
+Updating Geo sites involves performing:
+
+1. [Version-specific update steps](version_specific_updates.md), depending on the
+ version being updated to or from.
+1. [General update steps](#general-update-steps), for all updates.
+
+## General update steps
+
+NOTE:
+These general update steps are not intended for [high-availability deployments](https://docs.gitlab.com/omnibus/update/README.html#multi-node--ha-deployment), and will cause downtime. If you want to avoid downtime, consider using [zero downtime updates](https://docs.gitlab.com/omnibus/update/README.html#zero-downtime-updates).
+
+To update the Geo sites when a new GitLab version is released, update **primary**
+and all **secondary** sites:
+
+1. **Optional:** [Pause replication on each **secondary** sites.](../index.md#pausing-and-resuming-replication)
+1. SSH into each node of the **primary** site.
+1. [Update GitLab on the **primary** site using Omnibus](https://docs.gitlab.com/omnibus/update/#update-using-the-official-repositories).
+1. SSH into each node of **secondary** sites.
+1. [Update GitLab on each **secondary** site using Omnibus](https://docs.gitlab.com/omnibus/update/#update-using-the-official-repositories).
+1. If you paused replication in step 1, [resume replication on each **secondary**](../index.md#pausing-and-resuming-replication)
+1. [Test](#check-status-after-updating) **primary** and **secondary** sites, and check version in each.
+
+### Check status after updating
+
+Now that the update process is complete, you may want to check whether
+everything is working correctly:
+
+1. Run the Geo Rake task on an application node for the primary and secondary sites. Everything should be green:
+
+ ```shell
+ sudo gitlab-rake gitlab:geo:check
+ ```
+
+1. Check the **primary** site's Geo dashboard for any errors.
+1. Test the data replication by pushing code to the **primary** site and see if it
+ is received by **secondary** sites.
+
+If you encounter any issues, see the [Geo troubleshooting guide](troubleshooting.md).
diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md
index 155c06f90b8..8a0badbdd63 100644
--- a/doc/administration/geo/replication/version_specific_updates.md
+++ b/doc/administration/geo/replication/version_specific_updates.md
@@ -8,7 +8,7 @@ type: howto
# Version-specific update instructions **(PREMIUM SELF)**
Review this page for update instructions for your version. These steps
-accompany the [general steps](updating_the_geo_nodes.md#general-update-steps)
+accompany the [general steps](updating_the_geo_sites.md#general-update-steps)
for updating Geo nodes.
## Updating to GitLab 14.0/14.1
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 5aeb3eaef7f..4a8ec807d4b 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -742,6 +742,9 @@ database encryption. Proceed with caution.
gitlab_pages['gitlab_server'] = 'http://<gitlab_server_IP_or_URL>'
```
+1. If you have custom UID/GID settings on the **GitLab server**, add them to the **Pages server** `/etc/gitlab/gitlab.rb` as well,
+ otherwise running a `gitlab-ctl reconfigure` on the **GitLab server** can change file ownership and cause Pages requests to fail.
+
1. Create a backup of the secrets file on the **Pages server**:
```shell
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index 1c22f261e57..6d15c338f1c 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -15,7 +15,9 @@ Discussions are a set of related notes on:
- Merge requests
- Commits
-This includes system notes, which are notes about changes to the object (for example, when a milestone changes, a corresponding system note is added). Label notes are not part of this API, but recorded as separate events in [resource label events](resource_label_events.md).
+This includes system notes, which are notes about changes to the object (for example,
+when a milestone changes, a corresponding system note is added). Label notes are
+not part of this API, but recorded as separate events in [resource label events](resource_label_events.md).
## Discussions pagination
@@ -118,7 +120,8 @@ GET /projects/:id/issues/:issue_iid/discussions
```
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions"
+curl --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions"
```
### Get single issue discussion item
@@ -138,7 +141,8 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion item |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7"
+curl --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/<discussion_id>"
```
### Create new issue thread
@@ -159,7 +163,8 @@ Parameters:
| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions?body=comment"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions?body=comment"
```
### Add note to existing issue thread
@@ -185,7 +190,8 @@ Parameters:
| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/<discussion_id>/notes?body=comment"
```
### Modify existing issue thread note
@@ -207,7 +213,8 @@ Parameters:
| `body` | string | yes | The content of the note/reply |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment"
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/<discussion_id>/notes/1108?body=comment"
```
### Delete an issue thread note
@@ -228,7 +235,8 @@ Parameters:
| `note_id` | integer | yes | The ID of a discussion note |
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/636"
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/issues/11/discussions/636"
```
## Snippets
@@ -326,7 +334,8 @@ GET /projects/:id/snippets/:snippet_id/discussions
```
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions"
+curl --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions"
```
### Get single snippet discussion item
@@ -346,7 +355,8 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion item |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/<discussion_id>"
```
### Create new snippet thread
@@ -368,7 +378,8 @@ Parameters:
| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions?body=comment"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions?body=comment"
```
### Add note to existing snippet thread
@@ -391,7 +402,8 @@ Parameters:
| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/<discussion_id>/notes?body=comment"
```
### Modify existing snippet thread note
@@ -413,7 +425,8 @@ Parameters:
| `body` | string | yes | The content of the note/reply |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment"
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/<discussion_id>/notes/1108?body=comment"
```
### Delete a snippet thread note
@@ -434,7 +447,8 @@ Parameters:
| `note_id` | integer | yes | The ID of a discussion note |
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/636"
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/636"
```
## Epics **(ULTIMATE)**
@@ -533,7 +547,8 @@ GET /groups/:id/epics/:epic_id/discussions
```
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions"
+curl --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions"
```
### Get single epic discussion item
@@ -553,7 +568,8 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion item |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/<discussion_id>"
```
### Create new epic thread
@@ -575,7 +591,8 @@ Parameters:
| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions?body=comment"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions?body=comment"
```
### Add note to existing epic thread
@@ -599,7 +616,8 @@ Parameters:
| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/<discussion_id>/notes?body=comment"
```
### Modify existing epic thread note
@@ -621,7 +639,8 @@ Parameters:
| `body` | string | yes | The content of note/reply |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment"
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/<discussion_id>/notes/1108?body=comment"
```
### Delete an epic thread note
@@ -642,7 +661,8 @@ Parameters:
| `note_id` | integer | yes | The ID of a thread note |
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/636"
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/636"
```
## Merge requests
@@ -805,7 +825,8 @@ Diff comments also contain position:
```
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions"
+curl --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions"
```
### Get single merge request discussion item
@@ -825,7 +846,8 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion item |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7
+curl --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/<discussion_id>"
```
### Create new merge request thread
@@ -866,57 +888,67 @@ Parameters for all comments:
#### Create a new thread on the overview page
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions?body=comment"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions?body=comment"
```
#### Create a new thread in the merge request diff
-- Both `position[old_path]` and `position[new_path]` are required and must refer to the file path before and after the change.
-- To create a thread on an added line (highlighted in green in the merge request diff), use `position[new_line]` and don't include `position[old_line]`.
-- To create a thread on a removed line (highlighted in red in the merge request diff), use `position[old_line]` and don't include `position[new_line]`.
-- To create a thread on an unchanged line, include both `position[new_line]` and `position[old_line]` for the line. These positions might not be the same if earlier changes in the file changed the line number. This is a bug that we plan to fix in [GraphQL `createDiffNote` forces clients to compute redundant information (#325161)](https://gitlab.com/gitlab-org/gitlab/-/issues/325161).
-- If you specify incorrect `base`/`head`/`start` `SHA` parameters, you might run into the following bug: [Merge request comments receive "download" link instead of inline code (#296829)](https://gitlab.com/gitlab-org/gitlab/-/issues/296829).
+- Both `position[old_path]` and `position[new_path]` are required and must refer
+ to the file path before and after the change.
+- To create a thread on an added line (highlighted in green in the merge request diff),
+ use `position[new_line]` and don't include `position[old_line]`.
+- To create a thread on a removed line (highlighted in red in the merge request diff),
+ use `position[old_line]` and don't include `position[new_line]`.
+- To create a thread on an unchanged line, include both `position[new_line]` and
+ `position[old_line]` for the line. These positions might not be the same if earlier
+ changes in the file changed the line number. This is a bug that we plan to fix in
+ [GraphQL `createDiffNote` forces clients to compute redundant information (#325161)](https://gitlab.com/gitlab-org/gitlab/-/issues/325161).
+- If you specify incorrect `base`/`head`/`start` `SHA` parameters, you might run
+ into the following bug:
+ [Merge request comments receive "download" link instead of inline code (#296829)](https://gitlab.com/gitlab-org/gitlab/-/issues/296829).
To create a new thread:
1. [Get the latest merge request version](merge_requests.md#get-mr-diff-versions):
- ```shell
- curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/versions"
- ````
+ ```shell
+ curl --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/versions"
+ ```
1. Note the details of the latest version, which is listed first in the response array.
- ```json
- [
- {
- "id": 164560414,
- "head_commit_sha": "f9ce7e16e56c162edbc9e480108041cf6b0291fe",
- "base_commit_sha": "5e6dffa282c5129aa67cd227a0429be21bfdaf80",
- "start_commit_sha": "5e6dffa282c5129aa67cd227a0429be21bfdaf80",
- "created_at": "2021-03-30T09:18:27.351Z",
- "merge_request_id": 93958054,
- "state": "collected",
- "real_size": "2"
- },
- "previous versions are here"
- ]
- ```
+ ```json
+ [
+ {
+ "id": 164560414,
+ "head_commit_sha": "f9ce7e16e56c162edbc9e480108041cf6b0291fe",
+ "base_commit_sha": "5e6dffa282c5129aa67cd227a0429be21bfdaf80",
+ "start_commit_sha": "5e6dffa282c5129aa67cd227a0429be21bfdaf80",
+ "created_at": "2021-03-30T09:18:27.351Z",
+ "merge_request_id": 93958054,
+ "state": "collected",
+ "real_size": "2"
+ },
+ "previous versions are here"
+ ]
+ ```
1. Create a new diff thread. This example creates a thread on an added line:
- ```shell
- curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
- --form 'position[position_type]=text'\
- --form 'position[base_sha]=<use base_commit_sha from the versions response>'\
- --form 'position[head_sha]=<use head_commit_sha from the versions response>'\
- --form 'position[start_sha]=<use start_commit_sha from the versions response>'\
- --form 'position[new_path]=file.js'\
- --form 'position[old_path]=file.js'\
- --form 'position[new_line]=18'\
- --form 'body=test comment body'\
- "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions"
- ```
+ ```shell
+ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ --form 'position[position_type]=text'\
+ --form 'position[base_sha]=<use base_commit_sha from the versions response>'\
+ --form 'position[head_sha]=<use head_commit_sha from the versions response>'\
+ --form 'position[start_sha]=<use start_commit_sha from the versions response>'\
+ --form 'position[new_path]=file.js'\
+ --form 'position[old_path]=file.js'\
+ --form 'position[new_line]=18'\
+ --form 'body=test comment body'\
+ "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions"
+ ```
#### Parameters for multiline comments
@@ -960,7 +992,8 @@ Parameters:
| `resolved` | boolean | yes | Resolve/unresolve the discussion |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7?resolved=true"
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/<discussion_id>?resolved=true"
```
### Add note to existing merge request thread
@@ -984,7 +1017,8 @@ Parameters:
| `created_at` | string | no | Date time string, ISO 8601 formatted, such as `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/<discussion_id>/notes?body=comment"
```
### Modify an existing merge request thread note
@@ -1007,13 +1041,15 @@ Parameters:
| `resolved` | boolean | no | Resolve/unresolve the note (exactly one of `body` or `resolved` must be set |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment"
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/<discussion_id>/notes/1108?body=comment"
```
Resolving a note:
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?resolved=true"
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/<discussion_id>/notes/1108?resolved=true"
```
### Delete a merge request thread note
@@ -1034,7 +1070,8 @@ Parameters:
| `note_id` | integer | yes | The ID of a thread note |
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/636"
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/636"
```
## Commits
@@ -1177,7 +1214,8 @@ Diff comments contain also position:
```
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions"
+curl --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions"
```
### Get single commit discussion item
@@ -1197,7 +1235,8 @@ Parameters:
| `discussion_id` | integer | yes | The ID of a discussion item |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7"
+curl --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/<discussion_id>"
```
### Create new commit thread
@@ -1232,7 +1271,8 @@ Parameters:
| `position[y]` | integer | no | Y coordinate (for `image` diff notes) |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions?body=comment"
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions?body=comment"
```
The rules for creating the API request are the same as when
@@ -1259,7 +1299,8 @@ Parameters:
| `created_at` | string | no | Date time string, ISO 8601 formatted, such `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
```shell
-curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/<discussion_id>/notes?body=comment
```
### Modify an existing commit thread note
@@ -1281,13 +1322,15 @@ Parameters:
| `body` | string | no | The content of a note |
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment"
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/<discussion_id>/notes/1108?body=comment"
```
Resolving a note:
```shell
-curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?resolved=true"
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/<discussion_id>/notes/1108?resolved=true"
```
### Delete a commit thread note
@@ -1308,5 +1351,6 @@ Parameters:
| `note_id` | integer | yes | The ID of a thread note |
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/636"
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>"\
+ "https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/636"
```
diff --git a/doc/architecture/blueprints/database/scalability/patterns/img/db_terminology_v14_2.png b/doc/architecture/blueprints/database/scalability/patterns/img/db_terminology_v14_2.png
deleted file mode 100644
index 85ba1360f06..00000000000
--- a/doc/architecture/blueprints/database/scalability/patterns/img/db_terminology_v14_2.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/snowplow/index.md b/doc/development/snowplow/index.md
index 4a61beaf626..9b0f5178559 100644
--- a/doc/development/snowplow/index.md
+++ b/doc/development/snowplow/index.md
@@ -609,7 +609,7 @@ Snowplow Micro is a Docker-based solution for testing frontend and backend event
1. Restart GDK:
```shell
- `gdk restart`
+ gdk restart
```
1. Send a test Snowplow event from the Rails console:
diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb
index ac41e534842..6439c97d0bc 100644
--- a/lib/sidebars/projects/menus/settings_menu.rb
+++ b/lib/sidebars/projects/menus/settings_menu.rb
@@ -17,6 +17,7 @@ module Sidebars
add_item(monitor_menu_item)
add_item(pages_menu_item)
add_item(packages_and_registries_menu_item)
+ add_item(usage_quotas_menu_item)
true
end
@@ -141,6 +142,19 @@ module Sidebars
item_id: :packages_and_registries
)
end
+
+ def usage_quotas_menu_item
+ unless Feature.enabled?(:project_storage_ui, context.project&.group, default_enabled: :yaml)
+ return ::Sidebars::NilMenuItem.new(item_id: :usage_quotas)
+ end
+
+ ::Sidebars::MenuItem.new(
+ title: s_('UsageQuota|Usage Quotas'),
+ link: project_usage_quotas_path(context.project),
+ active_routes: { path: 'usage_quotas#index' },
+ item_id: :usage_quotas
+ )
+ end
end
end
end
diff --git a/lib/system_check/incoming_email_check.rb b/lib/system_check/incoming_email_check.rb
index e0e1147711c..84033ada710 100644
--- a/lib/system_check/incoming_email_check.rb
+++ b/lib/system_check/incoming_email_check.rb
@@ -7,9 +7,11 @@ module SystemCheck
def multi_check
if Gitlab.config.incoming_email.enabled
- checks = [
- SystemCheck::IncomingEmail::ImapAuthenticationCheck
- ]
+ checks = []
+
+ if Gitlab.config.incoming_email.inbox_method == 'imap'
+ checks << SystemCheck::IncomingEmail::ImapAuthenticationCheck
+ end
if Rails.env.production?
checks << SystemCheck::IncomingEmail::InitdConfiguredCheck
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 934f51254db..fc1af4f678f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -28892,6 +28892,9 @@ msgstr ""
msgid "Runners|Runners"
msgstr ""
+msgid "Runners|Runners in this group: %{groupRunnersCount}"
+msgstr ""
+
msgid "Runners|Shared runners are available to every project in a GitLab instance. If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner."
msgstr ""
@@ -36088,6 +36091,9 @@ msgstr ""
msgid "UsageQuota|Usage of group resources across the projects in the %{strong_start}%{group_name}%{strong_end} group"
msgstr ""
+msgid "UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project"
+msgstr ""
+
msgid "UsageQuota|Usage of resources across your projects"
msgstr ""
diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
index 1808969cd60..a8830efe653 100644
--- a/spec/controllers/groups/runners_controller_spec.rb
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -3,11 +3,13 @@
require 'spec_helper'
RSpec.describe Groups::RunnersController do
- let(:user) { create(:user) }
- let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, :group, groups: [group]) }
- let(:project) { create(:project, group: group) }
- let(:runner_project) { create(:ci_runner, :project, projects: [project]) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ let!(:runner) { create(:ci_runner, :group, groups: [group]) }
+ let!(:runner_project) { create(:ci_runner, :project, projects: [project]) }
+
let(:params_runner_project) { { group_id: group, id: runner_project } }
let(:params) { { group_id: group, id: runner } }
@@ -26,6 +28,7 @@ RSpec.describe Groups::RunnersController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
+ expect(assigns(:group_runners_limited_count)).to be(2)
end
end
diff --git a/spec/deprecation_toolkit_env.rb b/spec/deprecation_toolkit_env.rb
index b95a8c599bf..f76cd5b396c 100644
--- a/spec/deprecation_toolkit_env.rb
+++ b/spec/deprecation_toolkit_env.rb
@@ -57,9 +57,7 @@ module DeprecationToolkitEnv
# the dependency causing the problem.
# See https://gitlab.com/gitlab-org/gitlab/-/commit/aea37f506bbe036378998916d374966c031bf347#note_647515736
def self.allowed_kwarg_warning_paths
- %w[
- actionpack-6.1.3.2/lib/action_dispatch/routing/route_set.rb
- ]
+ %w[]
end
def self.configure!
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index e29a58f43b9..d5d6f534def 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -14,6 +14,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
+ let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
query_path = 'runner/graphql/'
@@ -27,14 +28,14 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
remove_repository(project)
end
- before do
- sign_in(admin)
- enable_admin_mode!(admin)
- end
-
describe GraphQL::Query, type: :request do
get_runners_query_name = 'get_runners.query.graphql'
+ before do
+ sign_in(admin)
+ enable_admin_mode!(admin)
+ end
+
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_runners_query_name}")
end
@@ -55,6 +56,11 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
describe GraphQL::Query, type: :request do
get_runner_query_name = 'get_runner.query.graphql'
+ before do
+ sign_in(admin)
+ enable_admin_mode!(admin)
+ end
+
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{get_runner_query_name}")
end
@@ -67,4 +73,35 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
end
+
+ describe GraphQL::Query, type: :request do
+ get_group_runners_query_name = 'get_group_runners.query.graphql'
+
+ let_it_be(:group_owner) { create(:user) }
+
+ before do
+ group.add_owner(group_owner)
+ end
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_group_runners_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_group_runners_query_name}.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{fixtures_path}#{get_group_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: group_owner, variables: {
+ groupFullPath: group.full_path,
+ first: 1
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
diff --git a/spec/frontend/projects/storage_counter/app_spec.js b/spec/frontend/projects/storage_counter/app_spec.js
new file mode 100644
index 00000000000..cf71a782f21
--- /dev/null
+++ b/spec/frontend/projects/storage_counter/app_spec.js
@@ -0,0 +1,22 @@
+import { shallowMount } from '@vue/test-utils';
+import StorageCounterApp from '~/projects/storage_counter/components/app.vue';
+
+describe('Storage counter app', () => {
+ let wrapper;
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(StorageCounterApp, { propsData });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders app successfully', () => {
+ expect(wrapper.text()).toBe('Usage');
+ });
+});
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index c1596711be7..3292f635f6b 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -2,6 +2,7 @@ import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -14,16 +15,20 @@ import RunnerPagination from '~/runner/components/runner_pagination.vue';
import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
import {
+ ADMIN_FILTERED_SEARCH_NAMESPACE,
CREATED_ASC,
CREATED_DESC,
DEFAULT_SORT,
INSTANCE_TYPE,
PARAM_KEY_STATUS,
+ PARAM_KEY_RUNNER_TYPE,
+ PARAM_KEY_TAG,
STATUS_ACTIVE,
RUNNER_PAGE_SIZE,
} from '~/runner/constants';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import { captureException } from '~/runner/sentry_utils';
+import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { runnersData, runnersDataPaginated } from '../mock_data';
@@ -47,10 +52,14 @@ describe('AdminRunnersApp', () => {
const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
const findRunnerList = () => wrapper.findComponent(RunnerList);
- const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
+ const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
+ const findRunnerPaginationPrev = () =>
+ findRunnerPagination().findByLabelText('Go to previous page');
+ const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
+ const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
- const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
const handlers = [[getRunnersQuery, mockRunnersQuery]];
wrapper = mountFn(AdminRunnersApp, {
@@ -68,7 +77,7 @@ describe('AdminRunnersApp', () => {
setWindowLocation('/admin/runners');
mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
- createComponentWithApollo();
+ createComponent();
await waitForPromises();
});
@@ -77,8 +86,16 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
+ it('shows the runner type help', () => {
+ expect(findRunnerTypeHelp().exists()).toBe(true);
+ });
+
+ it('shows the runner setup instructions', () => {
+ expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ });
+
it('shows the runners list', () => {
- expect(runnersData.data.runners.nodes).toMatchObject(findRunnerList().props('runners'));
+ expect(findRunnerList().props('runners')).toEqual(runnersData.data.runners.nodes);
});
it('requests the runners with no filters', () => {
@@ -90,20 +107,38 @@ describe('AdminRunnersApp', () => {
});
});
- it('shows the runner type help', () => {
- expect(findRunnerTypeHelp().exists()).toBe(true);
+ it('sets tokens in the filtered search', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findFilteredSearch().props('tokens')).toEqual([
+ expect.objectContaining({
+ type: PARAM_KEY_STATUS,
+ options: expect.any(Array),
+ }),
+ expect.objectContaining({
+ type: PARAM_KEY_RUNNER_TYPE,
+ options: expect.any(Array),
+ }),
+ expect.objectContaining({
+ type: PARAM_KEY_TAG,
+ recentTokenValuesStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`,
+ }),
+ ]);
});
- it('shows the runner setup instructions', () => {
- expect(findRunnerManualSetupHelp().exists()).toBe(true);
- expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ it('shows the active runner count', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findRunnerFilteredSearchBar().text()).toMatch(
+ `Runners currently online: ${mockActiveRunnersCount}`,
+ );
});
describe('when a filter is preselected', () => {
beforeEach(async () => {
setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}&tag[]=tag1`);
- createComponentWithApollo();
+ createComponent();
await waitForPromises();
});
@@ -133,7 +168,7 @@ describe('AdminRunnersApp', () => {
describe('when a filter is selected by the user', () => {
beforeEach(() => {
findRunnerFilteredSearchBar().vm.$emit('input', {
- filters: [{ type: PARAM_KEY_STATUS, value: { data: 'ACTIVE', operator: '=' } }],
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
sort: CREATED_ASC,
});
});
@@ -154,11 +189,19 @@ describe('AdminRunnersApp', () => {
});
});
+ it('when runners have not loaded, shows a loading state', () => {
+ createComponent();
+ expect(findRunnerList().props('loading')).toBe(true);
+ });
+
describe('when no runners are found', () => {
beforeEach(async () => {
- mockRunnersQuery = jest.fn().mockResolvedValue({ data: { runners: { nodes: [] } } });
- createComponentWithApollo();
- await waitForPromises();
+ mockRunnersQuery = jest.fn().mockResolvedValue({
+ data: {
+ runners: { nodes: [] },
+ },
+ });
+ createComponent();
});
it('shows a message for no results', async () => {
@@ -166,17 +209,14 @@ describe('AdminRunnersApp', () => {
});
});
- it('when runners have not loaded, shows a loading state', () => {
- createComponentWithApollo();
- expect(findRunnerList().props('loading')).toBe(true);
- });
-
describe('when runners query fails', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockRunnersQuery = jest.fn().mockRejectedValue(new Error('Error!'));
- createComponentWithApollo();
+ createComponent();
+ });
- await waitForPromises();
+ it('error is shown to the user', async () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
it('error is reported to sentry', async () => {
@@ -185,17 +225,13 @@ describe('AdminRunnersApp', () => {
component: 'AdminRunnersApp',
});
});
-
- it('error is shown to the user', async () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
- });
});
describe('Pagination', () => {
beforeEach(() => {
mockRunnersQuery = jest.fn().mockResolvedValue(runnersDataPaginated);
- createComponentWithApollo({ mountFn: mount });
+ createComponent({ mountFn: mount });
});
it('more pages can be selected', () => {
@@ -203,14 +239,11 @@ describe('AdminRunnersApp', () => {
});
it('cannot navigate to the previous page', () => {
- expect(findRunnerPagination().find('[aria-disabled]').text()).toBe('Prev');
+ expect(findRunnerPaginationPrev().attributes('aria-disabled')).toBe('true');
});
it('navigates to the next page', async () => {
- const nextPageBtn = findRunnerPagination().find('a');
- expect(nextPageBtn.text()).toBe('Next');
-
- await nextPageBtn.trigger('click');
+ await findRunnerPaginationNext().trigger('click');
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
sort: CREATED_DESC,
diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
index 85cf7ea92df..46948af1f28 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -2,8 +2,16 @@ import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
+import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config';
import TagToken from '~/runner/components/search_tokens/tag_token.vue';
-import { PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG } from '~/runner/constants';
+import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config';
+import { typeTokenConfig } from '~/runner/components/search_tokens/type_token_config';
+import {
+ PARAM_KEY_STATUS,
+ PARAM_KEY_RUNNER_TYPE,
+ PARAM_KEY_TAG,
+ STATUS_ACTIVE,
+} from '~/runner/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -13,12 +21,12 @@ describe('RunnerList', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const findSortOptions = () => wrapper.findAllComponents(GlDropdownItem);
- const findActiveRunnersMessage = () => wrapper.findByTestId('active-runners-message');
+ const findActiveRunnersMessage = () => wrapper.findByTestId('runner-count');
const mockDefaultSort = 'CREATED_DESC';
const mockOtherSort = 'CONTACTED_DESC';
const mockFilters = [
- { type: PARAM_KEY_STATUS, value: { data: 'ACTIVE', operator: '=' } },
+ { type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } },
{ type: 'filtered-search-term', value: { data: '' } },
];
const mockActiveRunnersCount = 2;
@@ -28,13 +36,16 @@ describe('RunnerList', () => {
shallowMount(RunnerFilteredSearchBar, {
propsData: {
namespace: 'runners',
+ tokens: [],
value: {
filters: [],
sort: mockDefaultSort,
},
- activeRunnersCount: mockActiveRunnersCount,
...props,
},
+ slots: {
+ 'runner-count': `Runners currently online: ${mockActiveRunnersCount}`,
+ },
stubs: {
FilteredSearch,
GlFilteredSearch,
@@ -64,12 +75,6 @@ describe('RunnerList', () => {
);
});
- it('Displays a large active runner count', () => {
- createComponent({ props: { activeRunnersCount: 2000 } });
-
- expect(findActiveRunnersMessage().text()).toBe('Runners currently online: 2,000');
- });
-
it('sets sorting options', () => {
const SORT_OPTIONS_COUNT = 2;
@@ -78,7 +83,13 @@ describe('RunnerList', () => {
expect(findSortOptions().at(1).text()).toBe('Last contact');
});
- it('sets tokens', () => {
+ it('sets tokens to the filtered search', () => {
+ createComponent({
+ props: {
+ tokens: [statusTokenConfig, typeTokenConfig, tagTokenConfig],
+ },
+ });
+
expect(findFilteredSearch().props('tokens')).toEqual([
expect.objectContaining({
type: PARAM_KEY_STATUS,
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index 5fff3581e39..344d1e5c150 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -56,7 +56,7 @@ describe('RunnerList', () => {
});
it('Displays a list of runners', () => {
- expect(findRows()).toHaveLength(3);
+ expect(findRows()).toHaveLength(4);
expect(findSkeletonLoader().exists()).toBe(false);
});
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 6a0863e92b4..e80da40e3bd 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -1,26 +1,85 @@
-import { shallowMount } from '@vue/test-utils';
+import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import { updateHistory } from '~/lib/utils/url_utility';
+
+import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
+import RunnerList from '~/runner/components/runner_list.vue';
import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
+import RunnerPagination from '~/runner/components/runner_pagination.vue';
import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
+
+import {
+ CREATED_ASC,
+ CREATED_DESC,
+ DEFAULT_SORT,
+ INSTANCE_TYPE,
+ PARAM_KEY_STATUS,
+ PARAM_KEY_RUNNER_TYPE,
+ STATUS_ACTIVE,
+ RUNNER_PAGE_SIZE,
+} from '~/runner/constants';
+import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
+import { captureException } from '~/runner/sentry_utils';
+import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { groupRunnersData, groupRunnersDataPaginated } from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+const mockGroupFullPath = 'group1';
const mockRegistrationToken = 'AABBCC';
+const mockRunners = groupRunnersData.data.group.runners.nodes;
+const mockGroupRunnersLimitedCount = mockRunners.length;
+
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
describe('GroupRunnersApp', () => {
let wrapper;
+ let mockGroupRunnersQuery;
const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
+ const findRunnerList = () => wrapper.findComponent(RunnerList);
+ const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
+ const findRunnerPaginationPrev = () =>
+ findRunnerPagination().findByLabelText('Go to previous page');
+ const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
+ const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
+ const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
+
+ const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const handlers = [[getGroupRunnersQuery, mockGroupRunnersQuery]];
- const createComponent = ({ mountFn = shallowMount } = {}) => {
wrapper = mountFn(GroupRunnersApp, {
+ localVue,
+ apolloProvider: createMockApollo(handlers),
propsData: {
registrationToken: mockRegistrationToken,
+ groupFullPath: mockGroupFullPath,
+ groupRunnersLimitedCount: mockGroupRunnersLimitedCount,
+ ...props,
},
});
};
- beforeEach(() => {
+ beforeEach(async () => {
+ setWindowLocation(`/groups/${mockGroupFullPath}/-/runners`);
+
+ mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersData);
+
createComponent();
+ await waitForPromises();
});
it('shows the runner type help', () => {
@@ -28,7 +87,179 @@ describe('GroupRunnersApp', () => {
});
it('shows the runner setup instructions', () => {
- expect(findRunnerManualSetupHelp().exists()).toBe(true);
expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
});
+
+ it('shows the runners list', () => {
+ expect(findRunnerList().props('runners')).toEqual(groupRunnersData.data.group.runners.nodes);
+ });
+
+ it('requests the runners with group path and no other filters', () => {
+ expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: undefined,
+ type: undefined,
+ sort: DEFAULT_SORT,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+
+ it('sets tokens in the filtered search', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findFilteredSearch().props('tokens')).toEqual([
+ expect.objectContaining({
+ type: PARAM_KEY_STATUS,
+ options: expect.any(Array),
+ }),
+ expect.objectContaining({
+ type: PARAM_KEY_RUNNER_TYPE,
+ options: expect.any(Array),
+ }),
+ ]);
+ });
+
+ describe('shows the active runner count', () => {
+ it('with a regular value', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findRunnerFilteredSearchBar().text()).toMatch(
+ `Runners in this group: ${mockGroupRunnersLimitedCount}`,
+ );
+ });
+
+ it('at the limit', () => {
+ createComponent({ props: { groupRunnersLimitedCount: 1000 }, mountFn: mount });
+
+ expect(findRunnerFilteredSearchBar().text()).toMatch(`Runners in this group: 1,000`);
+ });
+
+ it('over the limit', () => {
+ createComponent({ props: { groupRunnersLimitedCount: 1001 }, mountFn: mount });
+
+ expect(findRunnerFilteredSearchBar().text()).toMatch(`Runners in this group: 1,000+`);
+ });
+ });
+
+ describe('when a filter is preselected', () => {
+ beforeEach(async () => {
+ setWindowLocation(`?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('sets the filters in the search bar', () => {
+ expect(findRunnerFilteredSearchBar().props('value')).toEqual({
+ filters: [
+ { type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
+ { type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
+ ],
+ sort: 'CREATED_DESC',
+ pagination: { page: 1 },
+ });
+ });
+
+ it('requests the runners with filter parameters', () => {
+ expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: STATUS_ACTIVE,
+ type: INSTANCE_TYPE,
+ sort: DEFAULT_SORT,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+ });
+
+ describe('when a filter is selected by the user', () => {
+ beforeEach(() => {
+ findRunnerFilteredSearchBar().vm.$emit('input', {
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ACTIVE, operator: '=' } }],
+ sort: CREATED_ASC,
+ });
+ });
+
+ it('updates the browser url', () => {
+ expect(updateHistory).toHaveBeenLastCalledWith({
+ title: expect.any(String),
+ url: 'http://test.host/groups/group1/-/runners?status[]=ACTIVE&sort=CREATED_ASC',
+ });
+ });
+
+ it('requests the runners with filters', () => {
+ expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
+ groupFullPath: mockGroupFullPath,
+ status: STATUS_ACTIVE,
+ sort: CREATED_ASC,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+ });
+
+ it('when runners have not loaded, shows a loading state', () => {
+ createComponent();
+ expect(findRunnerList().props('loading')).toBe(true);
+ });
+
+ describe('when no runners are found', () => {
+ beforeEach(async () => {
+ mockGroupRunnersQuery = jest.fn().mockResolvedValue({
+ data: {
+ group: {
+ runners: { nodes: [] },
+ },
+ },
+ });
+ createComponent();
+ });
+
+ it('shows a message for no results', async () => {
+ expect(wrapper.text()).toContain('No runners found');
+ });
+ });
+
+ describe('when runners query fails', () => {
+ beforeEach(() => {
+ mockGroupRunnersQuery = jest.fn().mockRejectedValue(new Error('Error!'));
+ createComponent();
+ });
+
+ it('error is shown to the user', async () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ });
+
+ it('error is reported to sentry', async () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error('Network error: Error!'),
+ component: 'GroupRunnersApp',
+ });
+ });
+ });
+
+ describe('Pagination', () => {
+ beforeEach(() => {
+ mockGroupRunnersQuery = jest.fn().mockResolvedValue(groupRunnersDataPaginated);
+
+ createComponent({ mountFn: mount });
+ });
+
+ it('more pages can be selected', () => {
+ expect(findRunnerPagination().text()).toMatchInterpolatedText('Prev Next');
+ });
+
+ it('cannot navigate to the previous page', () => {
+ expect(findRunnerPaginationPrev().attributes('aria-disabled')).toBe('true');
+ });
+
+ it('navigates to the next page', async () => {
+ await findRunnerPaginationNext().trigger('click');
+
+ expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({
+ groupFullPath: mockGroupFullPath,
+ sort: CREATED_DESC,
+ first: RUNNER_PAGE_SIZE,
+ after: groupRunnersDataPaginated.data.group.runners.pageInfo.endCursor,
+ });
+ });
+ });
});
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index 8f551feca6e..c90b9a4c426 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -1,6 +1,14 @@
+const runnerFixture = (filename) => getJSONFixture(`graphql/runner/${filename}`);
+
// Fixtures generated by: spec/frontend/fixtures/runner.rb
-export const runnersData = getJSONFixture('graphql/runner/get_runners.query.graphql.json');
-export const runnersDataPaginated = getJSONFixture(
- 'graphql/runner/get_runners.query.graphql.paginated.json',
+
+// Admin queries
+export const runnersData = runnerFixture('get_runners.query.graphql.json');
+export const runnersDataPaginated = runnerFixture('get_runners.query.graphql.paginated.json');
+export const runnerData = runnerFixture('get_runner.query.graphql.json');
+
+// Group queries
+export const groupRunnersData = runnerFixture('get_group_runners.query.graphql.json');
+export const groupRunnersDataPaginated = runnerFixture(
+ 'get_group_runners.query.graphql.paginated.json',
);
-export const runnerData = getJSONFixture('graphql/runner/get_runner.query.graphql.json');
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index 9b79614db20..3079c781d73 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -158,5 +158,31 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
end
end
end
+
+ describe 'Usage Quotas' do
+ let(:item_id) { :usage_quotas }
+
+ describe 'with project_storage_ui feature flag enabled' do
+ before do
+ stub_feature_flags(project_storage_ui: true)
+ end
+
+ specify { is_expected.not_to be_nil }
+
+ describe 'when the user does not have access' do
+ let(:user) { nil }
+
+ specify { is_expected.to be_nil }
+ end
+ end
+
+ describe 'with project_storage_ui feature flag disabled' do
+ before do
+ stub_feature_flags(project_storage_ui: false)
+ end
+
+ specify { is_expected.to be_nil }
+ end
+ end
end
end
diff --git a/spec/lib/system_check/incoming_email_check_spec.rb b/spec/lib/system_check/incoming_email_check_spec.rb
new file mode 100644
index 00000000000..710702b93fc
--- /dev/null
+++ b/spec/lib/system_check/incoming_email_check_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SystemCheck::IncomingEmailCheck do
+ before do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ end
+
+ describe '#multi_check' do
+ context 'when incoming e-mail is disabled' do
+ before do
+ stub_incoming_email_setting(enabled: false)
+ end
+
+ it 'does not run any checks' do
+ expect(SystemCheck).not_to receive(:run)
+
+ subject.multi_check
+ end
+ end
+
+ context 'when incoming e-mail is enabled for IMAP' do
+ before do
+ stub_incoming_email_setting(enabled: true)
+ end
+
+ it 'runs IMAP and mailroom checks' do
+ expect(SystemCheck).to receive(:run).with('Reply by email', [
+ SystemCheck::IncomingEmail::ImapAuthenticationCheck,
+ SystemCheck::IncomingEmail::InitdConfiguredCheck,
+ SystemCheck::IncomingEmail::MailRoomRunningCheck
+ ])
+
+ subject.multi_check
+ end
+ end
+
+ context 'when incoming e-mail is enabled for Microsoft Graph' do
+ before do
+ stub_incoming_email_setting(enabled: true, inbox_method: 'microsoft_graph')
+ end
+
+ it 'runs mailroom checks' do
+ expect(SystemCheck).to receive(:run).with('Reply by email', [
+ SystemCheck::IncomingEmail::InitdConfiguredCheck,
+ SystemCheck::IncomingEmail::MailRoomRunningCheck
+ ])
+
+ subject.multi_check
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/usage_quotas_spec.rb b/spec/requests/projects/usage_quotas_spec.rb
new file mode 100644
index 00000000000..4586d3bec4f
--- /dev/null
+++ b/spec/requests/projects/usage_quotas_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project Usage Quotas' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:role) { :maintainer }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ project.add_role(user, role)
+ login_as(user)
+ end
+
+ shared_examples 'response with 404 status' do
+ it 'renders :not_found' do
+ get project_usage_quotas_path(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).not_to include(project_usage_quotas_path(project))
+ end
+ end
+
+ describe 'GET /:namespace/:project/usage_quotas' do
+ context 'with project_storage_ui feature flag enabled' do
+ before do
+ stub_feature_flags(project_storage_ui: true)
+ end
+
+ it 'renders usage quotas path' do
+ get project_usage_quotas_path(project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to include(project_usage_quotas_path(project))
+ expect(response.body).to include("Usage of project resources across the <strong>#{project.name}</strong> project")
+ end
+
+ context 'renders :not_found for user without permission' do
+ let(:role) { :developer }
+
+ it_behaves_like 'response with 404 status'
+ end
+ end
+
+ context 'with project_storage_ui feature flag disabled' do
+ before do
+ stub_feature_flags(project_storage_ui: false)
+ end
+
+ it_behaves_like 'response with 404 status'
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 8ae0885056e..2abc52fce85 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -118,7 +118,8 @@ RSpec.shared_context 'project navbar structure' do
_('Access Tokens'),
_('Repository'),
_('CI/CD'),
- _('Monitor')
+ _('Monitor'),
+ (s_('UsageQuota|Usage Quotas') if Feature.enabled?(:project_storage_ui, default_enabled: :yaml))
]
}
].compact
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index 3afebfbedab..9a31d599808 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -968,6 +968,32 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
end
+
+ describe 'Usage Quotas' do
+ context 'with project_storage_ui feature flag enabled' do
+ before do
+ stub_feature_flags(project_storage_ui: true)
+ end
+
+ it 'has a link to Usage Quotas' do
+ render
+
+ expect(rendered).to have_link('Usage Quotas', href: project_usage_quotas_path(project))
+ end
+ end
+
+ context 'with project_storage_ui feature flag disabled' do
+ before do
+ stub_feature_flags(project_storage_ui: false)
+ end
+
+ it 'does not have a link to Usage Quotas' do
+ render
+
+ expect(rendered).not_to have_link('Usage Quotas', href: project_usage_quotas_path(project))
+ end
+ end
+ end
end
describe 'Hidden menus' do