Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d67a86595f
commit
a7f478c9b1
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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') => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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') }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
```
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB |
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,26 +1,85 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
|
||||
import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
|
||||
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue