Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fb5de8adc2
commit
b7b4bb751c
|
|
@ -121,7 +121,6 @@ Layout/LineEndStringConcatenationIndentation:
|
|||
- 'danger/experiments/Dangerfile'
|
||||
- 'danger/feature_flag/Dangerfile'
|
||||
- 'db/post_migrate/20231124133015_drop_idx_projects_id_created_at_disable_overriding_approvers_false_for_gitlab_com.rb'
|
||||
- 'ee/app/components/namespaces/block_seat_overages/alert_component.rb'
|
||||
- 'ee/app/components/namespaces/combined_storage_users/base_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/combined_storage_users/non_owner_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/combined_storage_users/owner_alert_component.rb'
|
||||
|
|
@ -277,7 +276,6 @@ Layout/LineEndStringConcatenationIndentation:
|
|||
- 'ee/lib/system_check/geo/ssh_port_check.rb'
|
||||
- 'ee/lib/tasks/gitlab/custom_roles/check_docs_task.rb'
|
||||
- 'ee/lib/tasks/gitlab/geo.rake'
|
||||
- 'ee/spec/components/namespaces/block_seat_overages/alert_component_spec.rb'
|
||||
- 'ee/spec/components/namespaces/combined_storage_users/non_owner_alert_component_spec.rb'
|
||||
- 'ee/spec/components/namespaces/combined_storage_users/owner_alert_component_spec.rb'
|
||||
- 'ee/spec/components/namespaces/free_user_cap/enforcement_alert_component_spec.rb'
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
|
||||
import {
|
||||
I18N_JOB_STATUS_RUNNING,
|
||||
I18N_JOB_STATUS_ACTIVE,
|
||||
I18N_JOB_STATUS_IDLE,
|
||||
JOB_STATUS_ACTIVE,
|
||||
JOB_STATUS_RUNNING,
|
||||
JOB_STATUS_IDLE,
|
||||
} from '../constants';
|
||||
|
|
@ -24,10 +25,11 @@ export default {
|
|||
computed: {
|
||||
badge() {
|
||||
switch (this.jobStatus) {
|
||||
case JOB_STATUS_ACTIVE:
|
||||
case JOB_STATUS_RUNNING:
|
||||
return {
|
||||
classes: 'gl-text-blue-600! gl-shadow-inner-1-gray-400 gl-border-blue-600!',
|
||||
label: I18N_JOB_STATUS_RUNNING,
|
||||
label: I18N_JOB_STATUS_ACTIVE,
|
||||
};
|
||||
case JOB_STATUS_IDLE:
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const I18N_STATUS_OFFLINE = s__('Runners|Offline');
|
|||
export const I18N_STATUS_STALE = s__('Runners|Stale');
|
||||
|
||||
// Executor Status
|
||||
export const I18N_JOB_STATUS_RUNNING = s__('Runners|Running');
|
||||
export const I18N_JOB_STATUS_ACTIVE = s__('Runners|Active');
|
||||
export const I18N_JOB_STATUS_IDLE = s__('Runners|Idle');
|
||||
|
||||
// Status tooltips
|
||||
|
|
@ -163,6 +163,7 @@ export const STATUS_STALE = 'STALE';
|
|||
|
||||
// CiRunnerJobExecutionStatus
|
||||
|
||||
export const JOB_STATUS_ACTIVE = 'ACTIVE';
|
||||
export const JOB_STATUS_RUNNING = 'RUNNING';
|
||||
export const JOB_STATUS_IDLE = 'IDLE';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const chartData = generateRawData(30, 4);
|
|||
|
||||
export default {
|
||||
component: ContributorAreaChart,
|
||||
title: 'ce/contributors/contributor_area_chart',
|
||||
title: 'contributors/contributor_area_chart',
|
||||
decorators: [withVuexStore],
|
||||
args: {
|
||||
data: getMasterChartData(parsedData({ chartData })),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import Contributors from './contributors.vue';
|
|||
|
||||
export default {
|
||||
component: Contributors,
|
||||
title: 'ce/contributors/contributors',
|
||||
title: 'contributors/contributors',
|
||||
decorators: [withVuexStore],
|
||||
args: {
|
||||
commitsPath: '/gitlab-org/gitlab/-/commits/master?ref_type=heads',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlModal, GlEmptyState } from '@gitlab/ui';
|
||||
import emptySearchIllustration from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg?url';
|
||||
import { createAlert } from '~/alert';
|
||||
import { HTTP_STATUS_FORBIDDEN } from '~/lib/utils/http_status';
|
||||
import { mergeUrlParams, getParameterByName } from '~/lib/utils/url_utility';
|
||||
|
|
@ -16,13 +17,13 @@ export default {
|
|||
description: __('Edit your search and try again'),
|
||||
},
|
||||
},
|
||||
emptySearchIllustration,
|
||||
components: {
|
||||
GroupsComponent,
|
||||
GlModal,
|
||||
GlLoadingIcon,
|
||||
GlEmptyState,
|
||||
},
|
||||
inject: ['emptySearchIllustration'],
|
||||
props: {
|
||||
action: {
|
||||
type: String,
|
||||
|
|
@ -246,7 +247,7 @@ export default {
|
|||
<groups-component v-if="hasGroups" :groups="groups" :page-info="pageInfo" :action="action" />
|
||||
<gl-empty-state
|
||||
v-else-if="fromSearch"
|
||||
:svg-path="emptySearchIllustration"
|
||||
:svg-path="$options.emptySearchIllustration"
|
||||
:title="$options.i18n.searchEmptyState.title"
|
||||
:description="$options.i18n.searchEmptyState.description"
|
||||
data-testid="search-empty-state"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import { GlEmptyState } from '@gitlab/ui';
|
||||
import groupsEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/empty-state/empty-groups-md.svg?url';
|
||||
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: { GlEmptyState },
|
||||
inject: ['groupsEmptyStateIllustration'],
|
||||
groupsEmptyStateIllustration,
|
||||
i18n: {
|
||||
title: s__('GroupsEmptyState|A group is a collection of several projects'),
|
||||
description: s__(
|
||||
|
|
@ -19,7 +20,7 @@ export default {
|
|||
<gl-empty-state
|
||||
:title="$options.i18n.title"
|
||||
:description="$options.i18n.description"
|
||||
:svg-path="groupsEmptyStateIllustration"
|
||||
:svg-path="$options.groupsEmptyStateIllustration"
|
||||
:svg-height="null"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import { GlEmptyState } from '@gitlab/ui';
|
||||
import groupsEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/empty-state/empty-groups-md.svg?url';
|
||||
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: { GlEmptyState },
|
||||
inject: ['groupsEmptyStateIllustration'],
|
||||
groupsEmptyStateIllustration,
|
||||
computed: {
|
||||
title() {
|
||||
return s__('GroupsEmptyState|No public or internal groups');
|
||||
|
|
@ -15,5 +16,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-empty-state :title="title" :svg-path="groupsEmptyStateIllustration" :svg-height="null" />
|
||||
<gl-empty-state
|
||||
:title="title"
|
||||
:svg-path="$options.groupsEmptyStateIllustration"
|
||||
:svg-height="null"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,30 +3,34 @@ import { isEqual } from 'lodash';
|
|||
import FilteredSearchAndSort from '~/groups_projects/components/filtered_search_and_sort.vue';
|
||||
import { RECENT_SEARCHES_STORAGE_KEY_GROUPS } from '~/filtered_search/recent_searches_storage_keys';
|
||||
import {
|
||||
EXPLORE_SORTING_ITEMS,
|
||||
GROUPS_LIST_SORTING_ITEMS,
|
||||
SORTING_ITEM_NAME,
|
||||
EXPLORE_FILTERED_SEARCH_TERM_KEY,
|
||||
EXPLORE_FILTERED_SEARCH_NAMESPACE,
|
||||
GROUPS_LIST_FILTERED_SEARCH_TERM_KEY,
|
||||
} from '../constants';
|
||||
import GroupsService from '../service/groups_service';
|
||||
import GroupsStore from '../store/groups_store';
|
||||
import eventHub from '../event_hub';
|
||||
import GroupsApp from './app.vue';
|
||||
import EmptyState from './empty_states/groups_explore_empty_state.vue';
|
||||
|
||||
export default {
|
||||
filteredSearch: {
|
||||
tokens: [],
|
||||
namespace: EXPLORE_FILTERED_SEARCH_NAMESPACE,
|
||||
recentSearchesStorageKey: RECENT_SEARCHES_STORAGE_KEY_GROUPS,
|
||||
searchTermKey: EXPLORE_FILTERED_SEARCH_TERM_KEY,
|
||||
},
|
||||
components: {
|
||||
FilteredSearchAndSort,
|
||||
GroupsApp,
|
||||
EmptyState,
|
||||
},
|
||||
inject: ['endpoint', 'initialSort'],
|
||||
props: {
|
||||
filteredSearchNamespace: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
initialSort: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
service: new GroupsService(this.endpoint),
|
||||
|
|
@ -34,6 +38,14 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
filteredSearch() {
|
||||
return {
|
||||
tokens: [],
|
||||
namespace: this.filteredSearchNamespace,
|
||||
recentSearchesStorageKey: RECENT_SEARCHES_STORAGE_KEY_GROUPS,
|
||||
searchTermKey: GROUPS_LIST_FILTERED_SEARCH_TERM_KEY,
|
||||
};
|
||||
},
|
||||
sortByQuery() {
|
||||
return this.$route.query?.sort;
|
||||
},
|
||||
|
|
@ -45,7 +57,7 @@ export default {
|
|||
},
|
||||
activeSortItem() {
|
||||
return (
|
||||
EXPLORE_SORTING_ITEMS.find(
|
||||
GROUPS_LIST_SORTING_ITEMS.find(
|
||||
(sortItem) => sortItem.asc === this.sortBy || sortItem.desc === this.sortBy,
|
||||
) || SORTING_ITEM_NAME
|
||||
);
|
||||
|
|
@ -64,7 +76,7 @@ export default {
|
|||
return this.activeSortItem.asc === this.sortBy;
|
||||
},
|
||||
sortOptions() {
|
||||
return EXPLORE_SORTING_ITEMS.map((sortItem) => {
|
||||
return GROUPS_LIST_SORTING_ITEMS.map((sortItem) => {
|
||||
return {
|
||||
value: this.isAscending ? sortItem.asc : sortItem.desc,
|
||||
text: sortItem.label,
|
||||
|
|
@ -104,14 +116,14 @@ export default {
|
|||
this.pushQuery({ ...this.routeQueryWithoutPagination, sort: sortBy });
|
||||
},
|
||||
onFilter(filtersQuery) {
|
||||
const search = filtersQuery[EXPLORE_FILTERED_SEARCH_TERM_KEY];
|
||||
const search = filtersQuery[GROUPS_LIST_FILTERED_SEARCH_TERM_KEY];
|
||||
|
||||
eventHub.$emit('fetchFilteredAndSortedGroups', {
|
||||
filterGroupsBy: search,
|
||||
sortBy: this.sortBy,
|
||||
});
|
||||
this.pushQuery({
|
||||
...(search ? { [EXPLORE_FILTERED_SEARCH_TERM_KEY]: search } : {}),
|
||||
...(search ? { [GROUPS_LIST_FILTERED_SEARCH_TERM_KEY]: search } : {}),
|
||||
sort: this.sortByQuery,
|
||||
});
|
||||
},
|
||||
|
|
@ -122,12 +134,10 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<filtered-search-and-sort
|
||||
:filtered-search-namespace="$options.filteredSearch.namespace"
|
||||
:filtered-search-tokens="$options.filteredSearch.tokens"
|
||||
:filtered-search-term-key="$options.filteredSearch.searchTermKey"
|
||||
:filtered-search-recent-searches-storage-key="
|
||||
$options.filteredSearch.recentSearchesStorageKey
|
||||
"
|
||||
:filtered-search-namespace="filteredSearch.namespace"
|
||||
:filtered-search-tokens="filteredSearch.tokens"
|
||||
:filtered-search-term-key="filteredSearch.searchTermKey"
|
||||
:filtered-search-recent-searches-storage-key="filteredSearch.recentSearchesStorageKey"
|
||||
:filtered-search-query="$route.query"
|
||||
:is-ascending="isAscending"
|
||||
:sort-options="sortOptions"
|
||||
|
|
@ -138,7 +148,7 @@ export default {
|
|||
/>
|
||||
<groups-app :service="service" :store="store">
|
||||
<template #empty-state>
|
||||
<empty-state />
|
||||
<slot name="empty-state"></slot>
|
||||
</template>
|
||||
</groups-app>
|
||||
</div>
|
||||
|
|
@ -6,9 +6,6 @@ export const ACTIVE_TAB_SUBGROUPS_AND_PROJECTS = 'subgroups_and_projects';
|
|||
export const ACTIVE_TAB_SHARED = 'shared';
|
||||
export const ACTIVE_TAB_INACTIVE = 'inactive';
|
||||
|
||||
export const GROUPS_LIST_HOLDER_CLASS = '.js-groups-list-holder';
|
||||
export const CONTENT_LIST_CLASS = '.groups-list';
|
||||
|
||||
export const COMMON_STR = {
|
||||
FAILURE: __('An error occurred. Please try again.'),
|
||||
LEAVE_FORBIDDEN: s__(
|
||||
|
|
@ -49,14 +46,16 @@ export const SORTING_ITEM_STARS = {
|
|||
desc: 'stars_desc',
|
||||
};
|
||||
|
||||
export const EXPLORE_FILTERED_SEARCH_TERM_KEY = 'filter';
|
||||
export const EXPLORE_FILTERED_SEARCH_NAMESPACE = 'explore';
|
||||
export const EXPLORE_SORTING_ITEMS = [
|
||||
export const GROUPS_LIST_FILTERED_SEARCH_TERM_KEY = 'filter';
|
||||
export const GROUPS_LIST_SORTING_ITEMS = [
|
||||
SORTING_ITEM_NAME,
|
||||
SORTING_ITEM_CREATED,
|
||||
SORTING_ITEM_UPDATED,
|
||||
];
|
||||
|
||||
export const EXPLORE_FILTERED_SEARCH_NAMESPACE = 'explore';
|
||||
export const DASHBOARD_FILTERED_SEARCH_NAMESPACE = 'dashboard';
|
||||
|
||||
export const OVERVIEW_TABS_FILTERED_SEARCH_TERM_KEY = 'filter';
|
||||
export const OVERVIEW_TABS_FILTERED_SEARCH_NAMESPACE = 'overview';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import VueRouter from 'vue-router';
|
|||
import GroupItem from 'jh_else_ce/groups/components/group_item.vue';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import GroupFolder from './components/group_folder.vue';
|
||||
import GroupsExploreApp from './components/groups_explore_app.vue';
|
||||
import GroupsListWithFilteredSearchApp from './components/groups_list_with_filtered_search_app.vue';
|
||||
|
||||
export const createRouter = () => {
|
||||
const routes = [{ path: '/', name: 'root' }];
|
||||
|
|
@ -17,8 +17,8 @@ export const createRouter = () => {
|
|||
return router;
|
||||
};
|
||||
|
||||
export const initGroupsExplore = () => {
|
||||
const el = document.getElementById('js-groups-explore');
|
||||
export const initGroupsListWithFilteredSearch = ({ filteredSearchNamespace, EmptyState }) => {
|
||||
const el = document.getElementById('js-groups-list-with-filtered-search');
|
||||
|
||||
if (!el) return false;
|
||||
|
||||
|
|
@ -30,8 +30,7 @@ export const initGroupsExplore = () => {
|
|||
const {
|
||||
dataset: { appData },
|
||||
} = el;
|
||||
const { groupsEmptyStateIllustration, emptySearchIllustration, endpoint, initialSort } =
|
||||
convertObjectPropsToCamelCase(JSON.parse(appData));
|
||||
const { endpoint, initialSort } = convertObjectPropsToCamelCase(JSON.parse(appData));
|
||||
|
||||
Vue.use(VueRouter);
|
||||
const router = createRouter();
|
||||
|
|
@ -39,10 +38,18 @@ export const initGroupsExplore = () => {
|
|||
return new Vue({
|
||||
el,
|
||||
name: 'GroupsExploreRoot',
|
||||
provide: { groupsEmptyStateIllustration, emptySearchIllustration, endpoint, initialSort },
|
||||
router,
|
||||
render(createElement) {
|
||||
return createElement(GroupsExploreApp);
|
||||
return createElement(GroupsListWithFilteredSearchApp, {
|
||||
props: {
|
||||
filteredSearchNamespace,
|
||||
endpoint,
|
||||
initialSort,
|
||||
},
|
||||
scopedSlots: {
|
||||
'empty-state': () => createElement(EmptyState),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -26,9 +26,10 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
|||
import eventHub from '../event_hub';
|
||||
import animateMixin from '../mixins/animate';
|
||||
import {
|
||||
deleteTaskListItem,
|
||||
convertDescriptionWithNewSort,
|
||||
deleteTaskListItem,
|
||||
extractTaskTitleAndDescription,
|
||||
insertNextToTaskListItemText,
|
||||
} from '../utils';
|
||||
import TaskListItemActions from './task_list_item_actions.vue';
|
||||
|
||||
|
|
@ -265,12 +266,15 @@ export default {
|
|||
createTaskListItemActions() {
|
||||
const app = new Vue({
|
||||
el: document.createElement('div'),
|
||||
provide: { issuableType: this.issuableType },
|
||||
provide: { id: this.issueId, issuableType: this.issuableType },
|
||||
render: (createElement) => createElement(TaskListItemActions),
|
||||
});
|
||||
return app.$el;
|
||||
},
|
||||
convertTaskListItem(sourcepos) {
|
||||
convertTaskListItem({ id, sourcepos }) {
|
||||
if (this.issueId !== id) {
|
||||
return;
|
||||
}
|
||||
const oldDescription = this.descriptionText;
|
||||
const { newDescription, taskDescription, taskTitle } = deleteTaskListItem(
|
||||
oldDescription,
|
||||
|
|
@ -279,7 +283,10 @@ export default {
|
|||
this.$emit('saveDescription', newDescription);
|
||||
this.createTask({ taskTitle, taskDescription, oldDescription });
|
||||
},
|
||||
deleteTaskListItem(sourcepos) {
|
||||
deleteTaskListItem({ id, sourcepos }) {
|
||||
if (this.issueId !== id) {
|
||||
return;
|
||||
}
|
||||
const { newDescription } = deleteTaskListItem(this.descriptionText, sourcepos);
|
||||
this.$emit('saveDescription', newDescription);
|
||||
},
|
||||
|
|
@ -290,27 +297,11 @@ export default {
|
|||
|
||||
taskListItems?.forEach((item) => {
|
||||
const dropdown = this.createTaskListItemActions();
|
||||
this.insertNextToTaskListItemText(dropdown, item);
|
||||
insertNextToTaskListItemText(dropdown, item);
|
||||
this.addPointerEventListeners(item, '.task-list-item-actions');
|
||||
this.hasTaskListItemActions = true;
|
||||
});
|
||||
},
|
||||
insertNextToTaskListItemText(element, listItem) {
|
||||
const children = Array.from(listItem.children);
|
||||
const paragraph = children.find((el) => el.tagName === 'P');
|
||||
const list = children.find((el) => el.classList.contains('task-list'));
|
||||
if (paragraph) {
|
||||
// If there's a `p` element, then it's a multi-paragraph task item
|
||||
// and the task text exists within the `p` element as the last child
|
||||
paragraph.append(element);
|
||||
} else if (list) {
|
||||
// Otherwise, the task item can have a child list which exists directly after the task text
|
||||
list.insertAdjacentElement('beforebegin', element);
|
||||
} else {
|
||||
// Otherwise, the task item is a simple one where the task text exists as the last child
|
||||
listItem.append(element);
|
||||
}
|
||||
},
|
||||
stripClientState(description) {
|
||||
return description.replaceAll('<details open="true">', '<details>');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default {
|
|||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownItem,
|
||||
},
|
||||
inject: ['issuableType'],
|
||||
inject: ['id', 'issuableType'],
|
||||
computed: {
|
||||
showConvertToTaskItem() {
|
||||
return [TYPE_INCIDENT, TYPE_ISSUE].includes(this.issuableType);
|
||||
|
|
@ -16,10 +16,16 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
convertToTask() {
|
||||
eventHub.$emit('convert-task-list-item', this.$el.closest('li').dataset.sourcepos);
|
||||
eventHub.$emit('convert-task-list-item', this.eventPayload());
|
||||
},
|
||||
deleteTaskListItem() {
|
||||
eventHub.$emit('delete-task-list-item', this.$el.closest('li').dataset.sourcepos);
|
||||
eventHub.$emit('delete-task-list-item', this.eventPayload());
|
||||
},
|
||||
eventPayload() {
|
||||
return {
|
||||
id: this.id,
|
||||
sourcepos: this.$el.closest('li').dataset.sourcepos,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -228,3 +228,28 @@ export const extractTaskTitleAndDescription = (taskTitle, taskDescription) => {
|
|||
description: taskDescription,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert an element, such as a dropdown, next to a checkbox
|
||||
* in an issue/work item description rendered from markdown.
|
||||
*
|
||||
* @param element Element to insert
|
||||
* @param listItem The list item containing the checkbox
|
||||
*/
|
||||
export const insertNextToTaskListItemText = (element, listItem) => {
|
||||
const children = Array.from(listItem.children);
|
||||
const paragraph = children.find((el) => el.tagName === 'P');
|
||||
const list = children.find((el) => el.classList.contains('task-list'));
|
||||
|
||||
if (paragraph) {
|
||||
// If there's a `p` element, then it's a multi-paragraph task item
|
||||
// and the task text exists within the `p` element as the last child
|
||||
paragraph.append(element);
|
||||
} else if (list) {
|
||||
// Otherwise, the task item can have a child list which exists directly after the task text
|
||||
list.insertAdjacentElement('beforebegin', element);
|
||||
} else {
|
||||
// Otherwise, the task item is a simple one where the task text exists as the last child
|
||||
listItem.append(element);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import EmptyState from '~/groups/components/empty_states/groups_dashboard_empty_state.vue';
|
||||
import initGroupsList from '~/groups';
|
||||
import { initGroupsListWithFilteredSearch } from '~/groups/init_groups_list_with_filtered_search';
|
||||
import { DASHBOARD_FILTERED_SEARCH_NAMESPACE } from '~/groups/constants';
|
||||
|
||||
initGroupsList(EmptyState);
|
||||
initGroupsListWithFilteredSearch({
|
||||
filteredSearchNamespace: DASHBOARD_FILTERED_SEARCH_NAMESPACE,
|
||||
EmptyState,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
import { initGroupsExplore } from '~/groups/init_groups_explore';
|
||||
import EmptyState from '~/groups/components/empty_states/groups_explore_empty_state.vue';
|
||||
import { initGroupsListWithFilteredSearch } from '~/groups/init_groups_list_with_filtered_search';
|
||||
import { EXPLORE_FILTERED_SEARCH_NAMESPACE } from '~/groups/constants';
|
||||
|
||||
initGroupsExplore();
|
||||
initGroupsListWithFilteredSearch({
|
||||
filteredSearchNamespace: EXPLORE_FILTERED_SEARCH_NAMESPACE,
|
||||
EmptyState,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -353,8 +353,11 @@ export default {
|
|||
<work-item-description-rendered
|
||||
v-else
|
||||
:work-item-description="workItemDescription"
|
||||
:work-item-id="workItemId"
|
||||
:work-item-type="workItemType"
|
||||
:can-edit="canEdit"
|
||||
:disable-truncation="disableTruncation"
|
||||
:is-updating="isSubmitting"
|
||||
@startEditing="startEditing"
|
||||
@descriptionUpdated="handleDescriptionTextUpdated"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
<script>
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import Vue from 'vue';
|
||||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
|
||||
import eventHub from '~/issues/show/event_hub';
|
||||
import { deleteTaskListItem, insertNextToTaskListItemText } from '~/issues/show/utils';
|
||||
import { isDragging } from '~/sortable/utils';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
const FULL_OPACITY = 'gl-opacity-10';
|
||||
const isCheckbox = (target) => target?.classList.contains('task-list-item-checkbox');
|
||||
|
||||
export default {
|
||||
|
|
@ -21,10 +27,25 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isUpdating: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
workItemDescription: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
workItemId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
workItemType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
|
@ -32,6 +53,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
hasTaskListItemActions: false,
|
||||
truncated: false,
|
||||
checkboxes: [],
|
||||
};
|
||||
|
|
@ -58,6 +80,13 @@ export default {
|
|||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
eventHub.$on('delete-task-list-item', this.deleteTaskListItem);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('delete-task-list-item', this.deleteTaskListItem);
|
||||
this.removeAllPointerEventListeners();
|
||||
},
|
||||
methods: {
|
||||
async renderGFM() {
|
||||
await this.$nextTick();
|
||||
|
|
@ -66,16 +95,88 @@ export default {
|
|||
gl?.lazyLoader?.searchLazyImages();
|
||||
|
||||
if (this.canEdit) {
|
||||
this.checkboxes = this.$el.querySelectorAll('.task-list-item-checkbox');
|
||||
|
||||
// enable boxes, disabled by default in markdown
|
||||
this.checkboxes.forEach((checkbox) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
checkbox.disabled = false;
|
||||
});
|
||||
this.initCheckboxes();
|
||||
this.removeAllPointerEventListeners();
|
||||
this.renderTaskListItemActions();
|
||||
}
|
||||
|
||||
this.truncateLongDescription();
|
||||
},
|
||||
initCheckboxes() {
|
||||
this.checkboxes = this.$el.querySelectorAll('.task-list-item-checkbox');
|
||||
|
||||
// enable boxes, disabled by default in markdown
|
||||
this.checkboxes.forEach((checkbox) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
checkbox.disabled = false;
|
||||
});
|
||||
},
|
||||
renderTaskListItemActions() {
|
||||
const taskListItems = this.$el.querySelectorAll?.(
|
||||
'.task-list-item:not(.inapplicable, table .task-list-item)',
|
||||
);
|
||||
|
||||
taskListItems?.forEach((listItem) => {
|
||||
const dropdown = this.createTaskListItemActions();
|
||||
insertNextToTaskListItemText(dropdown, listItem);
|
||||
this.addPointerEventListeners(listItem, '.task-list-item-actions');
|
||||
this.hasTaskListItemActions = true;
|
||||
});
|
||||
},
|
||||
createTaskListItemActions() {
|
||||
const app = new Vue({
|
||||
el: document.createElement('div'),
|
||||
provide: { id: this.workItemId, issuableType: this.workItemType },
|
||||
render: (createElement) => createElement(TaskListItemActions),
|
||||
});
|
||||
return app.$el;
|
||||
},
|
||||
addPointerEventListeners(listItem, elementSelector) {
|
||||
const pointeroverListener = (event) => {
|
||||
const element = event.target.closest('li').querySelector(elementSelector);
|
||||
if (!element || isDragging() || this.isUpdating) {
|
||||
return;
|
||||
}
|
||||
element.classList.add(FULL_OPACITY);
|
||||
};
|
||||
const pointeroutListener = (event) => {
|
||||
const element = event.target.closest('li').querySelector(elementSelector);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.classList.remove(FULL_OPACITY);
|
||||
};
|
||||
|
||||
// We use pointerover/pointerout instead of CSS so that when we hover over a
|
||||
// list item with children, the grip icons of its children do not become visible.
|
||||
listItem.addEventListener('pointerover', pointeroverListener);
|
||||
listItem.addEventListener('pointerout', pointeroutListener);
|
||||
|
||||
this.pointerEventListeners = this.pointerEventListeners || new Map();
|
||||
const events = [
|
||||
{ type: 'pointerover', listener: pointeroverListener },
|
||||
{ type: 'pointerout', listener: pointeroutListener },
|
||||
];
|
||||
if (this.pointerEventListeners.has(listItem)) {
|
||||
const concatenatedEvents = this.pointerEventListeners.get(listItem).concat(events);
|
||||
this.pointerEventListeners.set(listItem, concatenatedEvents);
|
||||
} else {
|
||||
this.pointerEventListeners.set(listItem, events);
|
||||
}
|
||||
},
|
||||
removeAllPointerEventListeners() {
|
||||
this.pointerEventListeners?.forEach((events, listItem) => {
|
||||
events.forEach((event) => listItem.removeEventListener(event.type, event.listener));
|
||||
this.pointerEventListeners.delete(listItem);
|
||||
});
|
||||
},
|
||||
deleteTaskListItem({ id, sourcepos }) {
|
||||
if (this.workItemId !== id) {
|
||||
return;
|
||||
}
|
||||
const { newDescription } = deleteTaskListItem(this.descriptionText, sourcepos);
|
||||
this.$emit('descriptionUpdated', newDescription);
|
||||
},
|
||||
toggleCheckboxes(event) {
|
||||
const { target } = event;
|
||||
|
||||
|
|
@ -128,12 +229,16 @@ export default {
|
|||
<template>
|
||||
<div class="gl-my-5">
|
||||
<div v-if="isDescriptionEmpty" class="gl-text-secondary">{{ __('No description') }}</div>
|
||||
<div v-else ref="description" class="work-item-description md gl-clearfix gl-relative">
|
||||
<div
|
||||
v-else
|
||||
ref="description"
|
||||
class="work-item-description description md gl-clearfix gl-relative"
|
||||
>
|
||||
<div
|
||||
ref="gfm-content"
|
||||
v-safe-html="descriptionHtml"
|
||||
data-testid="work-item-description"
|
||||
:class="{ truncated: isTruncated }"
|
||||
:class="{ truncated: isTruncated, 'has-task-list-item-actions': hasTaskListItemActions }"
|
||||
@change="toggleCheckboxes"
|
||||
></div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -21,44 +21,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-nav-container {
|
||||
.nav-controls {
|
||||
.group-filter-form {
|
||||
flex: 1 1 auto;
|
||||
margin-right: $gl-padding-8;
|
||||
}
|
||||
|
||||
.dropdown-menu-right {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.dropdown,
|
||||
.dropdown .dropdown-toggle,
|
||||
.btn-success {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.group-filter-form,
|
||||
.dropdown {
|
||||
margin-bottom: 10px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&,
|
||||
.group-filter-form,
|
||||
.group-filter-form-field,
|
||||
.dropdown,
|
||||
.dropdown .dropdown-toggle,
|
||||
.btn-success {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove this selector once https://gitlab.com/gitlab-org/gitlab/-/issues/370050 is addressed.
|
||||
.scrolling-tabs-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
class MlController < ::Projects::ApplicationController
|
||||
feature_category :mlops
|
||||
|
||||
include PreviewMarkdown
|
||||
|
||||
before_action :authorize_read_model_registry!
|
||||
|
||||
private
|
||||
|
||||
def authorize_read_model_registry!
|
||||
render_404 unless can?(current_user, :read_model_registry, @project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -52,7 +52,7 @@ module Types
|
|||
|
||||
def job_execution_status
|
||||
BatchLoader::GraphQL.for(runner_manager.id).batch(key: :running_builds_exist) do |runner_manager_ids, loader|
|
||||
statuses = ::Ci::RunnerManager.id_in(runner_manager_ids).with_running_builds.index_by(&:id)
|
||||
statuses = ::Ci::RunnerManager.id_in(runner_manager_ids).with_executing_builds.index_by(&:id)
|
||||
|
||||
runner_manager_ids.each do |runner_manager_id|
|
||||
loader.call(runner_manager_id, statuses[runner_manager_id] ? :running : :idle)
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ module Types
|
|||
|
||||
def job_execution_status
|
||||
BatchLoader::GraphQL.for(runner.id).batch(key: :running_builds_exist) do |runner_ids, loader|
|
||||
statuses = ::Ci::Runner.id_in(runner_ids).with_running_builds.index_by(&:id)
|
||||
statuses = ::Ci::Runner.id_in(runner_ids).with_executing_builds.index_by(&:id)
|
||||
|
||||
runner_ids.each do |runner_id|
|
||||
loader.call(runner_id, statuses[runner_id] ? :running : :idle)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ module Types
|
|||
|
||||
present_using ::Ml::ModelPresenter
|
||||
|
||||
markdown_field :description_html, null: true
|
||||
|
||||
field :id, ::Types::GlobalIDType[::Ml::Model], null: false, description: 'ID of the model.'
|
||||
|
||||
field :name, ::GraphQL::Types::String, null: false, description: 'Name of the model.'
|
||||
|
|
@ -42,6 +44,10 @@ module Types
|
|||
field :version, ::Types::Ml::ModelVersionType, null: true,
|
||||
description: 'Version of the model.',
|
||||
resolver: ::Resolvers::Ml::FindModelVersionResolver
|
||||
|
||||
def description_html_resolver
|
||||
::MarkupHelper.markdown(object.description, context.to_h.dup)
|
||||
end
|
||||
end
|
||||
# rubocop: enable Graphql/AuthorizeTypes
|
||||
end
|
||||
|
|
|
|||
|
|
@ -293,11 +293,9 @@ module GroupsHelper
|
|||
dropdown_data
|
||||
end
|
||||
|
||||
def groups_explore_app_data
|
||||
def groups_list_with_filtered_search_app_data(endpoint)
|
||||
{
|
||||
endpoint: explore_groups_path(format: :json),
|
||||
empty_search_illustration: image_path('illustrations/empty-state/empty-search-md.svg'),
|
||||
groups_empty_state_illustration: image_path('illustrations/empty-state/empty-groups-md.svg'),
|
||||
endpoint: endpoint,
|
||||
initial_sort: project_list_sort_by
|
||||
}.to_json
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ module Projects
|
|||
create_model_path: new_project_ml_model_path(project),
|
||||
can_write_model_registry: can_write_model_registry?(user, project),
|
||||
mlflow_tracking_url: mlflow_tracking_url(project),
|
||||
max_allowed_file_size: max_allowed_file_size(project)
|
||||
max_allowed_file_size: max_allowed_file_size(project),
|
||||
markdown_preview_path: ::Gitlab::Routing.url_helpers.project_ml_preview_markdown_url(project)
|
||||
}
|
||||
|
||||
to_json(data)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ module Emails
|
|||
setup_issue_mail(issue_id, recipient_id)
|
||||
|
||||
previous_assignees = []
|
||||
previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
|
||||
previous_assignees = User.where(id: previous_assignee_ids).order(:id) if previous_assignee_ids.any?
|
||||
@added_assignees = @issue.assignees.map(&:name) - previous_assignees.map(&:name)
|
||||
@removed_assignees = previous_assignees.map(&:name) - @issue.assignees.map(&:name)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module Emails
|
|||
setup_note_mail(note_id, recipient_id)
|
||||
|
||||
@issue = @note.noteable
|
||||
@target_url = project_issue_url(*note_target_url_options)
|
||||
@target_url = Gitlab::UrlBuilder.build(@issue, **note_target_url_query_params)
|
||||
mail_answer_note_thread(
|
||||
@issue,
|
||||
@note,
|
||||
|
|
@ -85,6 +85,7 @@ module Emails
|
|||
@note = note_id.is_a?(Note) ? note_id : Note.find(note_id)
|
||||
@project = @note.project
|
||||
@group = @note.noteable.try(:group)
|
||||
@group ||= @note.noteable.resource_parent if @note.noteable.try(:resource_parent).is_a?(Group)
|
||||
@recipient = User.find(recipient_id)
|
||||
|
||||
if (@project || @group) && @note.persisted?
|
||||
|
|
|
|||
|
|
@ -115,9 +115,9 @@ module Ci
|
|||
scope :ordered, -> { order(id: :desc) }
|
||||
|
||||
scope :with_recent_runner_queue, -> { where(arel_table[:contacted_at].gt(recent_queue_deadline)) }
|
||||
scope :with_running_builds, -> do
|
||||
where('EXISTS(?)',
|
||||
::Ci::Build.running.select(1)
|
||||
scope :with_executing_builds, -> do
|
||||
where_exists(
|
||||
::Ci::Build.executing
|
||||
.where("#{::Ci::Build.quoted_table_name}.runner_id = #{quoted_table_name}.id")
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -84,14 +84,13 @@ module Ci
|
|||
where(system_xid: system_xid)
|
||||
end
|
||||
|
||||
scope :with_running_builds, -> do
|
||||
where('EXISTS(?)',
|
||||
Ci::Build.select(1)
|
||||
scope :with_executing_builds, -> do
|
||||
where_exists(
|
||||
Ci::Build
|
||||
.joins(:runner_manager_build)
|
||||
.running
|
||||
.executing
|
||||
.where("#{::Ci::Build.quoted_table_name}.runner_id = #{quoted_table_name}.runner_id")
|
||||
.where("#{::Ci::RunnerManagerBuild.quoted_table_name}.runner_machine_id = #{quoted_table_name}.id")
|
||||
.limit(1)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ module Ci
|
|||
ORDERED_STATUSES = %w[failed preparing pending running waiting_for_callback waiting_for_resource manual scheduled canceling canceled success skipped created].freeze
|
||||
PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
|
||||
IGNORED_STATUSES = %w[manual].to_set.freeze
|
||||
EXECUTING_STATUSES = %w[running canceling].freeze
|
||||
ALIVE_STATUSES = ORDERED_STATUSES - COMPLETED_STATUSES - BLOCKED_STATUS
|
||||
CANCELABLE_STATUSES = (ALIVE_STATUSES + ['scheduled'] - ['canceling']).freeze
|
||||
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
|
||||
|
|
@ -93,6 +94,7 @@ module Ci
|
|||
scope :alive, -> { with_status(*ALIVE_STATUSES) }
|
||||
scope :created_or_pending, -> { with_status(:created, :pending) }
|
||||
scope :running_or_pending, -> { with_status(:running, :pending) }
|
||||
scope :executing, -> { with_status(*EXECUTING_STATUSES) }
|
||||
scope :finished, -> { with_status(:success, :failed, :canceled) }
|
||||
scope :failed_or_canceled, -> { with_status(:failed, :canceled, :canceling) }
|
||||
scope :complete, -> { with_status(completed_statuses) }
|
||||
|
|
|
|||
|
|
@ -4,8 +4,3 @@
|
|||
- if current_user.can_create_group?
|
||||
= render Pajamas::ButtonComponent.new(href: new_group_path, variant: :confirm, button_options: { data: { testid: "new-group-button" } }) do
|
||||
= _("New group")
|
||||
|
||||
.gl-flex.gl-py-3.gl-gap-3
|
||||
.gl-w-full
|
||||
= render 'shared/groups/search_form'
|
||||
= render 'shared/groups/dropdown'
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
.js-groups-list-holder
|
||||
#js-groups-tree{ data: { endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap', groups_empty_state_illustration: image_path('illustrations/empty-state/empty-groups-md.svg'), empty_search_illustration: image_path('illustrations/empty-state/empty-search-md.svg') } }
|
||||
#js-groups-list-with-filtered-search{ data: { app_data: groups_list_with_filtered_search_app_data(dashboard_groups_path(format: :json)) } }
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
#js-groups-explore{ data: { app_data: groups_explore_app_data } }
|
||||
#js-groups-list-with-filtered-search{ data: { app_data: groups_list_with_filtered_search_app_data(explore_groups_path(format: :json)) } }
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
.top-area.gl-p-3.gl-justify-content-end
|
||||
.nav-controls
|
||||
= render 'shared/groups/search_form'
|
||||
= render 'shared/groups/dropdown'
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
- if @work_item.present?
|
||||
= render 'shared/work_item_metadata', work_item: @work_item
|
||||
- add_work_item_show_breadcrumb(@group, @work_item.iid)
|
||||
- add_page_specific_style 'page_bundles/issues_show'
|
||||
- add_page_specific_style 'page_bundles/work_items'
|
||||
- add_page_specific_style 'page_bundles/design_management'
|
||||
- @gfm_form = true
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
|
||||
= dispensable_render_if_exists "shared/web_hooks/group_web_hook_disabled_alert"
|
||||
= dispensable_render_if_exists "shared/free_user_cap_alert", source: @group
|
||||
= dispensable_render_if_exists "shared/seat_overage_alert", resource: @group
|
||||
= dispensable_render_if_exists "shared/unlimited_members_during_trial_alert", resource: @group
|
||||
|
||||
= render template: base_layout || "layouts/application"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@
|
|||
= dispensable_render_if_exists "projects/importing_alert", project: @project
|
||||
= dispensable_render_if_exists "shared/web_hooks/web_hook_disabled_alert"
|
||||
= dispensable_render_if_exists "projects/free_user_cap_alert", project: @project
|
||||
= dispensable_render_if_exists "shared/seat_overage_alert", resource: @project
|
||||
= dispensable_render_if_exists 'shared/unlimited_members_during_trial_alert', resource: @project
|
||||
|
||||
= render template: "layouts/application"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
- page_title _('Issues')
|
||||
- add_page_specific_style 'page_bundles/issuable_list'
|
||||
- add_page_specific_style 'page_bundles/issues_list'
|
||||
- add_page_specific_style 'page_bundles/issues_show'
|
||||
- add_page_specific_style 'page_bundles/work_items'
|
||||
- add_page_specific_style 'page_bundles/design_management'
|
||||
= content_for :meta_tags do
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
- if @work_item.present?
|
||||
= render 'shared/work_item_metadata', work_item: @work_item
|
||||
- add_work_item_show_breadcrumb(@project, @work_item.iid)
|
||||
- add_page_specific_style 'page_bundles/issues_show'
|
||||
- add_page_specific_style 'page_bundles/work_items'
|
||||
- add_page_specific_style 'page_bundles/design_management'
|
||||
- @gfm_form = true
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
= form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f|
|
||||
= search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter', data: { testid: 'groups-filter-field' }, spellcheck: false, id: 'group-filter-form-field'
|
||||
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/454583
|
|||
milestone: '16.11'
|
||||
type: beta
|
||||
group: group::secret detection
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: sidekiq_ip_address
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/452532
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155817
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/465177
|
||||
milestone: '17.1'
|
||||
group: group::duo chat
|
||||
type: beta
|
||||
default_enabled: true
|
||||
|
|
@ -483,6 +483,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
resources :models, only: [:index, :show, :destroy, :new], controller: 'models', param: :model_id do
|
||||
resources :versions, only: [:show], controller: 'model_versions', param: :model_version_id
|
||||
end
|
||||
post :preview_markdown
|
||||
end
|
||||
|
||||
namespace :service_desk do
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ The following API resources are available in the project context:
|
|||
| [Protected branches](protected_branches.md) | `/projects/:id/protected_branches` |
|
||||
| [Protected container registry](project_container_registry_protection_rules.md) | `/projects/:id/registry/protection/rules` |
|
||||
| [Protected environments](protected_environments.md) | `/projects/:id/protected_environments` |
|
||||
| [Protected packages](project_packages_protection_rules.md) | `/projects/:id/protection/rules` |
|
||||
| [Protected packages](project_packages_protection_rules.md) | `/projects/:id/packages/protection/rules` |
|
||||
| [Protected tags](protected_tags.md) | `/projects/:id/protected_tags` |
|
||||
| [PyPI packages](packages/pypi.md) | `/projects/:id/packages/pypi` (also available for groups) |
|
||||
| [Release links](releases/links.md) | `/projects/:id/releases/.../assets/links` |
|
||||
|
|
|
|||
|
|
@ -26356,6 +26356,7 @@ Machine learning model in the model registry.
|
|||
| <a id="mlmodelcandidates"></a>`candidates` | [`MlCandidateConnection`](#mlcandidateconnection) | Version candidates of the model. (see [Connections](#connections)) |
|
||||
| <a id="mlmodelcreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
|
||||
| <a id="mlmodeldescription"></a>`description` | [`String`](#string) | Description of the model. |
|
||||
| <a id="mlmodeldescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
|
||||
| <a id="mlmodelid"></a>`id` | [`MlModelID!`](#mlmodelid) | ID of the model. |
|
||||
| <a id="mlmodellatestversion"></a>`latestVersion` | [`MlModelVersion`](#mlmodelversion) | Latest version of the model. |
|
||||
| <a id="mlmodelname"></a>`name` | [`String!`](#string) | Name of the model. |
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ curl --request POST \
|
|||
}'
|
||||
```
|
||||
|
||||
### Update a package protection rule
|
||||
## Update a package protection rule
|
||||
|
||||
Update a package protection rule for a project.
|
||||
|
||||
|
|
|
|||
|
|
@ -328,18 +328,18 @@ insight into what went wrong.
|
|||
|
||||
The following table gives an overview of how the API functions generally behave.
|
||||
|
||||
| Request type | Description |
|
||||
|:--------------|:--------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `GET` | Access one or more resources and return the result as JSON. |
|
||||
| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
|
||||
| `GET` / `PUT` | Return `200 OK` if the resource is accessed or modified successfully. The (modified) result is returned as JSON. |
|
||||
| `DELETE` | Returns `204 No Content` if the resource was deleted successfully or `202 Accepted` if the resource is scheduled to be deleted. |
|
||||
| Request type | Description |
|
||||
|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `GET` | Access one or more resources and return the result as JSON. |
|
||||
| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
|
||||
| `GET` / `PUT` / `PATCH` | Return `200 OK` if the resource is accessed or modified successfully. The (modified) result is returned as JSON. |
|
||||
| `DELETE` | Returns `204 No Content` if the resource was deleted successfully or `202 Accepted` if the resource is scheduled to be deleted. |
|
||||
|
||||
The following table shows the possible return codes for API requests.
|
||||
|
||||
| Return values | Description |
|
||||
|:--------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, and the resource itself is returned as JSON. |
|
||||
| `200 OK` | The `GET`, `PUT`, `PATCH` or `DELETE` request was successful, and the resource itself is returned as JSON. |
|
||||
| `201 Created` | The `POST` request was successful, and the resource is returned as JSON. |
|
||||
| `202 Accepted` | The `GET`, `PUT` or `DELETE` request was successful, and the resource is scheduled for processing. |
|
||||
| `204 No Content` | The server has successfully fulfilled the request, and there is no additional content to send in the response payload body. |
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
|
|
@ -405,7 +405,7 @@ When we have a blueprint merged ideally the confidence should move to 👍 becau
|
|||
| Routing | group::tenant scale | [Blueprint](../http_routing_service.md) | 👍 |
|
||||
| Cell Control Plane | group::Delivery/team::Foundations | To-Do | 👎 |
|
||||
| Cell Sizing | team::Scalability-Observability | [To-Do](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2838) | 👎 |
|
||||
| CI Runners | team::Scalability-Practices | To-Do | 👎 |
|
||||
| CI Runners | team::Scalability-Practices | [Blueprint](runner.md) | 👎 |
|
||||
| Databases | team::Database Reliability | [Blueprint](postgresql.md) | 👍 |
|
||||
| Deployments | group::Delivery | [Blueprint](deployments.md) | 👍 |
|
||||
| Observability | team::Scalability-Observability | [Blueprint](observability.md) | 👎 |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
owning-stage: "~devops::verify"
|
||||
group: Runner
|
||||
description: 'Cells: Runner'
|
||||
creation-date: "2024-07-10"
|
||||
authors: [ "@rehab", "" ]
|
||||
coach:
|
||||
approvers: ["@josephburnett", "@tmaczukin", "@amknight", "@skarbek"]
|
||||
---
|
||||
|
||||
# Cells: Runner
|
||||
|
||||
This blueprint describes the architecture and roadmap of the Runner service in a cellular architecture.
|
||||
|
||||
## Introduction
|
||||
|
||||
[GitLab Runner](https://docs.gitlab.com/runner/) is a service that is relatively easy to provision and update compared to other GitLab.com services.
|
||||
|
||||
Currently having two exemplary setups, [SaaS Runners](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/ci/runners/index.md) and [Dedicated Runners](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/team/-/blob/main/architecture/blueprints/dedicated-runners-beta.md?ref_type=heads), with the latter having a superior setup enhanced by implementing [GRIT](https://gitlab.com/gitlab-org/ci-cd/runner-tools/grit).
|
||||
|
||||
Following [Cell's philosophy](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/architecture/blueprints/cells/infrastructure/index.md#philosophy), the Runner service is a perfect candidate for implementing a cell-local service, with almost no need for any change to its [codebase](https://gitlab.com/gitlab-org/gitlab-runner/)(_TBC_).
|
||||
|
||||
In [Cells 1.0](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/architecture/blueprints/cells/iterations/cells-1.0.md), new customers are expected to enroll in the first Cell, in this iteration, we'll utilize the existing work made as part of Dedicated [Beta release](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/team/-/blob/main/architecture/blueprints/dedicated-runners-beta.md?ref_type=heads#dedicated-runners-beta) to provision the Runners.
|
||||
|
||||
### Architecture
|
||||
|
||||
<img src="../diagrams/term-cell-runner.drawio.png" alt="Cell Runner diagram">
|
||||
|
||||
- There's a 1:n relationship between a Cell and a Runner, i.e, one Cell can have many Runners, while a Runner is only registered to one Cell.
|
||||
- Primary Cell (GitLab.com) is using Runners that are managed via [config-mgmt](https://gitlab.com/gitlab-com/gl-infra/config-mgmt) and [chef-repo](https://gitlab.com/gitlab-com/gl-infra/chef-repo), while the Secondary Cells are using [Transistor](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/transistor/)-like setup, which vendors in GRIT.
|
||||
|
||||
Note, there's currently no Runner Reference Architectures, however, some links the reader can reference:
|
||||
|
||||
- [GitLab-runner/docs/fleet_scaling](https://gitlab.com/gitlab-org/gitlab-runner/-/blob/main/docs/fleet_scaling/index.md).
|
||||
- [Create a test framework for testing runner fleet autoscaling configurations](https://gitlab.com/gitlab-org/gitlab/-/issues/458311).
|
||||
- [Document jump start configurations for an autoscaled runner fleet on Google Compute](https://gitlab.com/gitlab-org/gitlab/-/issues/458313).
|
||||
- [Runner Fleet Scaling and Configuration Best Practices Center](https://gitlab.com/groups/gitlab-org/-/epics/8952).
|
||||
|
||||
### Provision
|
||||
|
||||
Cells are expected to follow provisioning design and tooling similar to that of Dedicated; Runners in this case would utilize a tool similar to [Transistor](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/transistor/), which in a nutshell, is a collection of Terraform modules and bash scripts.
|
||||
|
||||
Transistor's [development guidelines](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/transistor/-/blob/main/DEVELOPMENT.md) have more elaborate info, but a simplified version of the provisioning steps of a Runner could look like the following:
|
||||
|
||||
1. Generate a runner model.
|
||||
1. Provision the runner via one of the following steps:
|
||||
- Execute ./bin/prepare, ./bin/onboard/ then ./bin/provision.
|
||||
- Alternatively, create an [MR in Switchboard](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/sandbox/switchboard_runners/#triggering-a-deployment-via-merge-request), which will execute the scripts above in order.
|
||||
|
||||
The deployment pipeline for a Runner could be triggered as a downstream pipeline following Instrumentor's deployment of a GitLab instance, this would require Instrumentor (or a higher-level job, e.g. in switchboard) to generate a runner model before the downstream pipeline is triggered.
|
||||
|
||||
#### Cells vs Dedicated
|
||||
|
||||
The Dedicated team has restricted access to the GitLab instances in which Runners are registered, this however, is not the case with Cells.
|
||||
|
||||
The SRE team fully owns and maintains a Cell, which means that registration token management will not require any external coordination, and can be a step that is automated using the provisioning scripts.
|
||||
|
||||
### Deployment
|
||||
|
||||
Ideally, Cells:Runner should be managed similarly to the rest of the services in a Cell.
|
||||
|
||||
At this moment, GitLab.com SaaS Runners have a separate ad-hoc [deployer tool](https://gitlab.com/gitlab-com/gl-infra/ci-runners/deployer/), not to be confused with the [GitLab.com grand Deployer](https://ops.gitlab.net/gitlab-com/gl-infra/deployer), which is run and maintained by the Runner group.
|
||||
|
||||
In Cells, the Runner service will utilize Dedicated-like tooling, which is similar to the rest of the services, this will ensure visibility, observability and maintainability. Runners Dedicated are highly dependent on GRIT, which promises to introduce a [blue-green mechanism](https://gitlab.com/groups/gitlab-org/ci-cd/runner-tools/-/epics/1) which will be eventually utilized by both Dedicated and Cells.
|
||||
|
||||
### Observability
|
||||
|
||||
Observability for Runners on Dedicated is currently a WIP, Runners should be treated as the rest of the services, and should utilize the recording rules and dashboards used currently in GitLab.com.
|
||||
|
||||
## Roadmap
|
||||
|
||||
To keep [Centralize tooling](index.md#philosophy), 2 open questions:
|
||||
|
||||
1. How do we feel about eventually combining [Transistor](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/transistor/) back into [Instrumentor](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/instrumentor/)?
|
||||
|
||||
Combining Transistor and Instrumentor could "make changes to runners INCREDIBLY costly and slow to have to go through the whole instrumentor deployment process." (@amknight , 2024), additionally, keeping both projects modular would enable a more efficient and safer release process as the QA testing process is separate (@skarbek, 2024).
|
||||
|
||||
1. How do we feel about incorporating Runner deployments with the rest of GitLab service deployments in a Cell?
|
||||
|
||||
As mentioned under [Provision](runner.md#provision), we could possible use downstream pipelines to achieve this.
|
||||
|
||||
### Iterations [WIP]
|
||||
|
||||
#### Cells 1.0
|
||||
|
||||
Linux Runners of type `small` will be available.
|
||||
|
||||
#### Cells 1.5
|
||||
|
||||
#### Cells 2.0
|
||||
|
||||
## FAQ [WIP]
|
||||
|
||||
**Q1**
|
||||
|
||||
A1
|
||||
|
||||
**Q2**
|
||||
|
||||
A2
|
||||
|
||||
## References
|
||||
|
||||
- @amknight (2024): [!146768 (comment 1823581090)](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146768#note_1823581090).
|
||||
- @skarbek (2024):[!146768 (comment 1819418987)](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146768#note_1819418987).
|
||||
|
|
@ -10,14 +10,9 @@ DETAILS:
|
|||
**Tier:** Free, Premium, Ultimate
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
When a new pipeline starts, GitLab checks the pipeline configuration to determine
|
||||
which jobs should run in that pipeline. You can configure jobs to run depending on
|
||||
factors like the status of variables, or the pipeline type.
|
||||
|
||||
To configure a job to be included or excluded from certain pipelines, use [`rules`](job_rules.md).
|
||||
|
||||
Use [`needs`](../yaml/index.md#needs) to configure a job to run as soon as the
|
||||
earlier jobs it depends on finish running.
|
||||
Before a new pipeline starts, GitLab checks the pipeline configuration to determine
|
||||
which jobs can run in that pipeline. You can configure jobs to run depending on
|
||||
conditions like the value of variables or the pipeline type with [`rules`](job_rules.md).
|
||||
|
||||
## Create a job that must be run manually
|
||||
|
||||
|
|
@ -39,8 +34,7 @@ Manual jobs can be either optional or blocking.
|
|||
In optional manual jobs:
|
||||
|
||||
- [`allow_failure`](../yaml/index.md#allow_failure) is `true`, which is the default
|
||||
setting for jobs that have `when: manual` and no [`rules`](../yaml/index.md#rules),
|
||||
or `when: manual` defined outside of `rules`.
|
||||
setting for jobs that have `when: manual` defined outside of `rules`.
|
||||
- The status does not contribute to the overall pipeline status. A pipeline can
|
||||
succeed even if all of its manual jobs fail.
|
||||
|
||||
|
|
@ -69,7 +63,9 @@ You can also [add custom CI/CD variables when running a manual job](index.md#spe
|
|||
|
||||
### Add a confirmation dialog for manual jobs
|
||||
|
||||
Use [`manual_confirmation`](../yaml/index.md#manual_confirmation) with `when: manual` to add a confirmation dialog for manual jobs. The confirmation dialog helps to prevent accidental deployments or deletions, especially for sensitive jobs like those that deploy to production.
|
||||
Use [`manual_confirmation`](../yaml/index.md#manual_confirmation) with `when: manual` to add a confirmation dialog for manual jobs.
|
||||
The confirmation dialog helps to prevent accidental deployments or deletions,
|
||||
especially for sensitive jobs like those that deploy to production.
|
||||
|
||||
Users are prompted to confirm the action before the manual job runs, which provides an additional layer of safety and control.
|
||||
|
||||
|
|
@ -185,7 +181,8 @@ Test Boosters reports usage statistics to the author.
|
|||
|
||||
### Run a one-dimensional matrix of parallel jobs
|
||||
|
||||
You can create a one-dimensional matrix of parallel jobs:
|
||||
To run a job multiple times in parallel in a single pipeline, but with different variable values for each instance of the job,
|
||||
use the [`parallel:matrix`](../yaml/index.md#parallelmatrix) keyword:
|
||||
|
||||
```yaml
|
||||
deploystacks:
|
||||
|
|
@ -198,8 +195,6 @@ deploystacks:
|
|||
environment: production/$PROVIDER
|
||||
```
|
||||
|
||||
You can also [create a multi-dimensional matrix](../yaml/index.md#parallelmatrix).
|
||||
|
||||
### Run a matrix of parallel trigger jobs
|
||||
|
||||
You can run a [trigger](../yaml/index.md#trigger) job multiple times in parallel in a single pipeline,
|
||||
|
|
@ -240,6 +235,8 @@ keyword for dynamic runner selection:
|
|||
```yaml
|
||||
deploystacks:
|
||||
stage: deploy
|
||||
script:
|
||||
- bin/deploy
|
||||
parallel:
|
||||
matrix:
|
||||
- PROVIDER: aws
|
||||
|
|
@ -251,7 +248,7 @@ deploystacks:
|
|||
environment: $PROVIDER/$STACK
|
||||
```
|
||||
|
||||
#### Fetch artifacts from a `parallel:matrix` job
|
||||
### Fetch artifacts from a `parallel:matrix` job
|
||||
|
||||
You can fetch artifacts from a job created with [`parallel:matrix`](../yaml/index.md#parallelmatrix)
|
||||
by using the [`dependencies`](../yaml/index.md#dependencies) keyword. Use the job name
|
||||
|
|
|
|||
|
|
@ -613,7 +613,7 @@ flow of how we construct a Chat prompt:
|
|||
which calls `ai_client.stream`
|
||||
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/chain/requests/ai_gateway.rb#L20-27))
|
||||
1. `ai_client.stream` routes to `Gitlab::Llm::AiGateway::Client#stream`, which
|
||||
makes an API request to the AI Gateway `/v1/chat/completion` endpoint
|
||||
makes an API request to the AI Gateway `/v1/chat/agent` endpoint
|
||||
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/ai_gateway/client.rb#L64-82))
|
||||
1. We've now made our first request to the AI Gateway. If the LLM says that the
|
||||
answer to the first request is a final answer, we
|
||||
|
|
|
|||
|
|
@ -51,9 +51,10 @@ If the highest number stable branch is unclear, check the [GitLab blog](https://
|
|||
|:------------------------|:----------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [Ruby](#2-ruby) | `3.1.x` | From GitLab 16.7, Ruby 3.1 is required. You must use the standard MRI implementation of Ruby. We love [JRuby](https://www.jruby.org/) and [Rubinius](https://github.com/rubinius/rubinius#the-rubinius-language-platform), but GitLab needs several Gems that have native extensions. |
|
||||
| [RubyGems](#3-rubygems) | `3.5.x` | A specific RubyGems version is not required, but you should update to benefit from some known performance improvements. |
|
||||
| [Go](#4-go) | `1.20.x` | From GitLab 16.4, Go 1.20 or later is required. |
|
||||
| [Go](#4-go) | `1.22.x` | From GitLab 17.1, Go 1.22 or later is required. |
|
||||
| [Git](#git) | `2.44.x` | From GitLab 17.1, Git 2.44.x and later is required. You should use the [Git version provided by Gitaly](#git). |
|
||||
| [Node.js](#5-node) | `20.13.x` | From GitLab 17.0, Node.js 20.13 or later is required. |
|
||||
| [PostgreSQL](#7-database) | `14.x` | From GitLab 17.0, PostgreSQL 14 or later is required. |
|
||||
|
||||
## GitLab directory structure
|
||||
|
||||
|
|
@ -253,11 +254,11 @@ Linux. You can find downloads for other platforms at the
|
|||
# Remove former Go installation folder
|
||||
sudo rm -rf /usr/local/go
|
||||
|
||||
curl --remote-name --location --progress-bar "https://go.dev/dl/go1.20.8.linux-amd64.tar.gz"
|
||||
echo 'cc97c28d9c252fbf28f91950d830201aa403836cbed702a05932e63f7f0c7bc4 go1.20.8.linux-amd64.tar.gz' | shasum -a256 -c - && \
|
||||
sudo tar -C /usr/local -xzf go1.20.8.linux-amd64.tar.gz
|
||||
curl --remote-name --location --progress-bar "https://go.dev/dl/go1.22.5.linux-amd64.tar.gz"
|
||||
echo '904b924d435eaea086515bc63235b192ea441bd8c9b198c507e85009e6e4c7f0 go1.22.5.linux-amd64.tar.gz' | shasum -a256 -c - && \
|
||||
sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz
|
||||
sudo ln -sf /usr/local/go/bin/{go,gofmt} /usr/local/bin/
|
||||
rm go1.20.8.linux-amd64.tar.gz
|
||||
rm go1.22.5.linux-amd64.tar.gz
|
||||
```
|
||||
|
||||
## 5. Node
|
||||
|
|
@ -296,7 +297,7 @@ sudo adduser --disabled-login --gecos 'GitLab' git
|
|||
|
||||
NOTE:
|
||||
Only PostgreSQL is supported.
|
||||
In GitLab 16.0 and later, we [require PostgreSQL 13+](requirements.md#postgresql-requirements).
|
||||
In GitLab 17.0 and later, we [require PostgreSQL 14+](requirements.md#postgresql-requirements).
|
||||
|
||||
1. Install the database packages.
|
||||
|
||||
|
|
|
|||
|
|
@ -357,6 +357,42 @@ Experiment locally before you push to the remote repository.
|
|||
git rebase -i commit-id
|
||||
```
|
||||
|
||||
### Redact text
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/450701) in GitLab 17.1 [with a flag](../../administration/feature_flags.md) named `rewrite_history_ui`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/462999) in GitLab 17.2.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
This feature is available for testing, but not ready for production use.
|
||||
|
||||
Permanently delete sensitive or confidential information that was accidentally committed, ensuring
|
||||
it's no longer accessible in your repository's history.
|
||||
Replaces a list of strings with `***REMOVED***`.
|
||||
|
||||
Alternatively, to completely delete specific files from a repository, see
|
||||
[Remove blobs](../../user/project/repository/reducing_the_repo_size_using_git.md#remove-blobs).
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have the Owner role for the instance.
|
||||
|
||||
To redact text from your repository:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Settings > Repository**.
|
||||
1. Expand **Repository maintenance**.
|
||||
1. Select **Redact text**.
|
||||
1. On the drawer, enter the text to redact.
|
||||
You can use regex and glob patterns.
|
||||
1. Select **Redact matching strings**.
|
||||
1. On the confirmation dialog, enter your project path.
|
||||
1. Select **Yes, redact matching strings**.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
1. Expand **Advanced**.
|
||||
1. Select **Run housekeeping**.
|
||||
|
||||
### Delete sensitive information from commits
|
||||
|
||||
You can use Git to delete sensitive information from your past commits. However,
|
||||
|
|
|
|||
|
|
@ -99,11 +99,11 @@ Download and install Go (for Linux, 64-bit):
|
|||
# Remove former Go installation folder
|
||||
sudo rm -rf /usr/local/go
|
||||
|
||||
curl --remote-name --location --progress-bar "https://go.dev/dl/go1.20.8.linux-amd64.tar.gz"
|
||||
echo 'cc97c28d9c252fbf28f91950d830201aa403836cbed702a05932e63f7f0c7bc4 go1.20.8.linux-amd64.tar.gz' | shasum -a256 -c - && \
|
||||
sudo tar -C /usr/local -xzf go1.20.8.linux-amd64.tar.gz
|
||||
curl --remote-name --location --progress-bar "https://go.dev/dl/go1.22.5.linux-amd64.tar.gz"
|
||||
echo '904b924d435eaea086515bc63235b192ea441bd8c9b198c507e85009e6e4c7f0 go1.22.5.linux-amd64.tar.gz' | shasum -a256 -c - && \
|
||||
sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz
|
||||
sudo ln -sf /usr/local/go/bin/{go,gofmt} /usr/local/bin/
|
||||
rm go1.20.8.linux-amd64.tar.gz
|
||||
rm go1.22.5.linux-amd64.tar.gz
|
||||
```
|
||||
|
||||
### 6. Update Git
|
||||
|
|
@ -136,7 +136,7 @@ Remember to set `git -> bin_path` to `/usr/local/bin/git` in `config/gitlab.yml`
|
|||
### 7. Update PostgreSQL
|
||||
|
||||
WARNING:
|
||||
GitLab 16.0 requires at least PostgreSQL 13.
|
||||
GitLab 17.0 requires at least PostgreSQL 14.
|
||||
|
||||
The latest version of GitLab might depend on a more recent PostgreSQL version
|
||||
than what you are running. You may also have to enable some
|
||||
|
|
|
|||
|
|
@ -531,7 +531,7 @@ You can interact with the results of the security scanning tools in several loca
|
|||
|
||||
- [Scan information in merge requests](#merge-request)
|
||||
- [Project Security Dashboard](security_dashboard/index.md#project-security-dashboard)
|
||||
- [Security pipeline tab](security_dashboard/index.md)
|
||||
- [Security pipeline tab](vulnerability_report/pipeline.md)
|
||||
- [Group Security Dashboard](security_dashboard/index.md#group-security-dashboard)
|
||||
- [Security Center](security_dashboard/index.md#security-center)
|
||||
- [Vulnerability Report](vulnerability_report/index.md)
|
||||
|
|
|
|||
|
|
@ -8,20 +8,21 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate
|
||||
**Offering:** GitLab.com, GitLab Dedicated
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
**Status:** Beta
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/11439) in GitLab 16.7 as an [experiment](../../../../policy/experiment-beta-support.md) for GitLab Dedicated customers.
|
||||
> - [Changed](https://gitlab.com/groups/gitlab-org/-/epics/12729) to Beta and made available on GitLab.com in GitLab 17.1.
|
||||
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156907) in GitLab 17.2.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flags](../../../../administration/feature_flags.md) named `pre_receive_secret_detection_beta_release` and `pre_receive_secret_detection_push_check`.
|
||||
|
||||
Secret push protection blocks secrets such as keys and API tokens from being pushed to GitLab.
|
||||
The content of each commit is checked for secrets when pushed to GitLab. If any secrets are
|
||||
detected, the push is blocked.
|
||||
|
||||
Secret push protection is available on GitLab.com and GitLab Dedicated. To scan for secrets
|
||||
in your GitLab self-managed instance, use [pipeline secret detection](../index.md)
|
||||
instead. Pipeline secret detection can be used together with secret push protection to
|
||||
further secure your GitLab.com or Dedicated instance.
|
||||
Use [pipeline secret detection](../index.md) together with secret push protection to further strengthen your security.
|
||||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For an overview, see the playlist [Get Started with Secret Push Protection](https://www.youtube.com/playlist?list=PL05JrBw4t0KoADm-g2vxfyR0m6QLphTv-).
|
||||
|
|
@ -50,7 +51,9 @@ If secret push protection does not detect any secrets in your commits, no messag
|
|||
|
||||
## Enable secret push protection
|
||||
|
||||
On GitLab Dedicated and GitLab.com, secret push protection must be enabled at the instance level and then you must enable it per project.
|
||||
On GitLab Dedicated and Self-managed instances, secret push protection must be enabled at the instance level and then you must enable it per project.
|
||||
|
||||
On GitLab.com, this setting has been enabled at the instance level. You must enable it per project.
|
||||
|
||||
### Allow the use of secret push protection in your GitLab instance
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Because this is an experimental feature,
|
|||
| Flag | Description | Actor | Status |
|
||||
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ----- | ------ |
|
||||
| `work_item_epics` | Consolidated flag that contains all the changes needed to get epic work items to work for a given group. | Group | **Required** |
|
||||
| `sync_work_item_to_epic` | Synchronizes data from a legacy epic to a corresponding work item. | Group | **Required** |
|
||||
| `synced_epic_work_item_editable` | Allows editing epic work items when they have a legacy epic. | Group | **Required** |
|
||||
| `work_items_rolledup_dates` | Calculates the start and due dates in a hierarchy for work items. | Group | **Required** |
|
||||
| `epic_and_work_item_labels_unification` | Delegates labels between epics and epic work item. | Group | **Required** |
|
||||
| `epic_and_work_item_associations_unification` | Delegates other epic and work item associations. | Group | **Required** |
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ When using repository cleanup, note:
|
|||
## Remove blobs
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/450701) in GitLab 17.1 [with a flag](../../../administration/feature_flags.md) named `rewrite_history_ui`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/462999) in GitLab 17.2.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
|
|
@ -248,6 +249,9 @@ This feature is available for testing, but not ready for production use.
|
|||
Permanently delete sensitive or confidential information that was accidentally committed, ensuring
|
||||
it's no longer accessible in your repository's history.
|
||||
|
||||
Alternatively, to replace strings with `***REMOVED***`, see
|
||||
[Redact text](../../../topics/git/undo.md#redact-text).
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have the Owner role for the instance.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ module.exports = (path, options = {}) => {
|
|||
roots: extRoots = [],
|
||||
rootsEE: extRootsEE = [],
|
||||
rootsJH: extRootsJH = [],
|
||||
isEE = IS_EE,
|
||||
isJH = IS_JH,
|
||||
} = options;
|
||||
|
||||
const reporters = ['default'];
|
||||
|
|
@ -80,11 +82,11 @@ module.exports = (path, options = {}) => {
|
|||
|
||||
const glob = `${path}/**/*@([._])spec.js`;
|
||||
let testMatch = [`<rootDir>/${glob}`];
|
||||
if (IS_EE) {
|
||||
if (isEE) {
|
||||
testMatch.push(`<rootDir>/ee/${glob}`);
|
||||
}
|
||||
|
||||
if (IS_JH) {
|
||||
if (isJH) {
|
||||
testMatch.push(`<rootDir>/jh/${glob}`);
|
||||
}
|
||||
// workaround for eslint-import-resolver-jest only resolving in test files
|
||||
|
|
@ -130,7 +132,7 @@ module.exports = (path, options = {}) => {
|
|||
|
||||
const collectCoverageFrom = ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'];
|
||||
|
||||
if (IS_EE) {
|
||||
if (isEE) {
|
||||
const rootDirEE = '<rootDir>/ee/app/assets/javascripts$1';
|
||||
const specDirEE = '<rootDir>/ee/spec/frontend/$1';
|
||||
Object.assign(moduleNameMapper, {
|
||||
|
|
@ -148,7 +150,7 @@ module.exports = (path, options = {}) => {
|
|||
collectCoverageFrom.push(rootDirEE.replace('$1', '/**/*.{js,vue}'));
|
||||
}
|
||||
|
||||
if (IS_JH) {
|
||||
if (isJH) {
|
||||
// DO NOT add additional path to Jihu side, it might break things.
|
||||
const rootDirJH = '<rootDir>/jh/app/assets/javascripts$1';
|
||||
const specDirJH = '<rootDir>/jh/spec/frontend/$1';
|
||||
|
|
@ -266,8 +268,8 @@ module.exports = (path, options = {}) => {
|
|||
},
|
||||
testEnvironment: '<rootDir>/spec/frontend/environment.js',
|
||||
testEnvironmentOptions: {
|
||||
IS_EE,
|
||||
IS_JH,
|
||||
IS_EE: isEE,
|
||||
IS_JH: isJH,
|
||||
url: TEST_HOST,
|
||||
},
|
||||
testRunner: 'jest-jasmine2',
|
||||
|
|
@ -278,8 +280,8 @@ module.exports = (path, options = {}) => {
|
|||
roots: [
|
||||
'<rootDir>/app/assets/javascripts/',
|
||||
...extRoots,
|
||||
...(IS_EE ? ['<rootDir>/ee/app/assets/javascripts/', ...extRootsEE] : []),
|
||||
...(IS_JH ? ['<rootDir>/jh/app/assets/javascripts/', ...extRootsJH] : []),
|
||||
...(isEE ? ['<rootDir>/ee/app/assets/javascripts/', ...extRootsEE] : []),
|
||||
...(isJH ? ['<rootDir>/jh/app/assets/javascripts/', ...extRootsJH] : []),
|
||||
],
|
||||
/*
|
||||
Reduce the amount of max workers in development mode.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ module Gitlab
|
|||
module SidekiqMiddleware
|
||||
class SetIpAddress
|
||||
def call(_worker_class, job, _queue)
|
||||
return yield if Feature.disabled?(:sidekiq_ip_address) # rubocop: disable Gitlab/FeatureFlagWithoutActor -- not applicable
|
||||
return yield unless job.key?('ip_address_state')
|
||||
|
||||
::Gitlab::IpAddressState.with(job['ip_address_state']) do # rubocop: disable CodeReuse/ActiveRecord -- Non-AR
|
||||
|
|
|
|||
|
|
@ -8711,18 +8711,6 @@ msgstr ""
|
|||
msgid "Block user"
|
||||
msgstr ""
|
||||
|
||||
msgid "BlockSeatOverages|%{root_namespace_name} has exceeded the number of seats in its subscription and is now %{link_start}read-only%{link_end}. To remove the read-only state, reduce the number of users in your top-level group to make seats available, or purchase more seats for the subscription."
|
||||
msgstr ""
|
||||
|
||||
msgid "BlockSeatOverages|The top-level group %{root_namespace_name} is now read-only."
|
||||
msgstr ""
|
||||
|
||||
msgid "BlockSeatOverages|To remove the %{link_start}read-only%{link_end} state, ask a user with the Owner role for %{root_namespace_name} to reduce the number of users in the group, or to purchase more seats for the subscription."
|
||||
msgstr ""
|
||||
|
||||
msgid "BlockSeatOverages|Your top-level group %{root_namespace_name} is now read-only."
|
||||
msgstr ""
|
||||
|
||||
msgid "BlockSeatOverages|Your top-level group is over the number of seats in its subscription and has been placed in a read-only state."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -25871,9 +25859,6 @@ msgstr ""
|
|||
msgid "GroupsTree|Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupsTree|Search by name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Groups|An error occurred updating this group. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -45247,6 +45232,9 @@ msgstr ""
|
|||
msgid "Runners|A runner configuration is where you configure runners based on your requirements. Each runner in this table represents a %{linkStart}single runner entry%{linkEnd} in the %{codeStart}config.toml%{codeEnd}. Multiple runners can be linked to the same configuration. When there are multiple runners in a configuration, the one that contacted GitLab most recently displays here."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Active"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Active runners"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -45913,9 +45901,6 @@ msgstr ""
|
|||
msgid "Runners|Runners performance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Running"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Running Jobs"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -51664,6 +51649,9 @@ msgstr ""
|
|||
msgid "SubscriptionBanner|Upload new license"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionGroupsNew|Group URL %{error}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SubscriptionGroupsNew|Group name %{error}"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -126,12 +126,12 @@ module QA
|
|||
run_git("git rev-parse --abbrev-ref HEAD").to_s
|
||||
end
|
||||
|
||||
def push_changes(branch = @default_branch, push_options: nil, max_attempts: 3)
|
||||
def push_changes(branch = @default_branch, push_options: nil, max_attempts: 3, raise_on_failure: true)
|
||||
cmd = ['git push']
|
||||
cmd << push_options_hash_to_string(push_options)
|
||||
cmd << uri
|
||||
cmd << branch
|
||||
run_git(cmd.compact.join(' '), max_attempts: max_attempts).to_s
|
||||
run_git(cmd.compact.join(' '), raise_on_failure: raise_on_failure, max_attempts: max_attempts).to_s
|
||||
end
|
||||
|
||||
def push_all_branches
|
||||
|
|
@ -333,9 +333,10 @@ module QA
|
|||
read_netrc_content.grep(/^#{Regexp.escape(netrc_content)}$/).any?
|
||||
end
|
||||
|
||||
def run_git(command_str, env: env_vars, max_attempts: 1)
|
||||
def run_git(command_str, raise_on_failure: true, env: env_vars, max_attempts: 1)
|
||||
run(
|
||||
command_str,
|
||||
raise_on_failure: raise_on_failure,
|
||||
env: env,
|
||||
max_attempts: max_attempts,
|
||||
sleep_internal: command_retry_sleep_interval,
|
||||
|
|
|
|||
|
|
@ -9,10 +9,6 @@ module QA
|
|||
def self.included(base)
|
||||
super
|
||||
|
||||
base.view 'app/views/shared/groups/_search_form.html.haml' do
|
||||
element 'groups-filter-field'
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/groups/components/groups.vue' do
|
||||
element 'groups-list-tree-container'
|
||||
end
|
||||
|
|
@ -33,7 +29,10 @@ module QA
|
|||
# @param name [String] group name
|
||||
# @return [Boolean] whether the filter returned any group
|
||||
def filter_group(name)
|
||||
fill_element('groups-filter-field', name).send_keys(:return)
|
||||
filter_input = find_element('filtered-search-term-input')
|
||||
filter_input.click
|
||||
filter_input.set(name)
|
||||
click_element 'search-button'
|
||||
# Loading starts a moment after `return` is sent. We mustn't jump ahead
|
||||
wait_for_requests if spinner_exists?
|
||||
has_element?('groups-list-tree-container', wait: 1)
|
||||
|
|
|
|||
|
|
@ -22,14 +22,7 @@ module QA
|
|||
end
|
||||
|
||||
def has_subgroup?(name)
|
||||
filter_input = find_element('filtered-search-term-input')
|
||||
filter_input.click
|
||||
filter_input.set(name)
|
||||
click_element 'search-button'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.has_link?(name, wait: 0) # element containing link to group
|
||||
has_filtered_group?(name)
|
||||
end
|
||||
|
||||
def go_to_new_subgroup
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
def run(command_str, env: [], max_attempts: 1, sleep_internal: 0, log_prefix: '')
|
||||
def run(command_str, raise_on_failure: true, env: [], max_attempts: 1, sleep_internal: 0, log_prefix: '')
|
||||
command = [*env, command_str, '2>&1'].compact.join(' ')
|
||||
result = nil
|
||||
|
||||
|
|
@ -36,8 +36,11 @@ module QA
|
|||
result.success?
|
||||
end
|
||||
|
||||
unless result.success?
|
||||
raise CommandError, "The command #{result.command} failed (#{result.exitstatus}) with the following output:\n#{result.response}"
|
||||
raise_error = raise_on_failure && !result.success?
|
||||
|
||||
if raise_error
|
||||
raise CommandError,
|
||||
"The command #{result.command} failed (#{result.exitstatus}) with the following output:\n#{result.response}"
|
||||
end
|
||||
|
||||
result
|
||||
|
|
|
|||
|
|
@ -33,9 +33,8 @@ module QA
|
|||
logger.info("Number of mapping files found: #{mapping_files.size}")
|
||||
|
||||
mapping_data = mapping_files.flat_map { |file| JSON.parse(File.read(file)) }.reduce(:merge!)
|
||||
file = "#{ENV['CI_COMMIT_REF_SLUG']}/#{ENV['QA_RUN_TYPE']}/test-
|
||||
code-paths-mapping-merged-pipeline-#{ENV['CI_PIPELINE_ID'] || 'local'}.json"
|
||||
File.write(file, mapping_data.to_json) && logger.debug("Saved test code paths mapping to #{file}")
|
||||
file = "#{ENV['CI_COMMIT_REF_SLUG']}/#{ENV['QA_RUN_TYPE']}/test-code-paths-mapping-merged-pipeline-#{\
|
||||
ENV['CI_PIPELINE_ID'] || 'local'}.json"
|
||||
upload_to_gcs(file, mapping_data)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ RSpec.describe QA::Git::Repository do
|
|||
let(:repo_uri_with_credentials) { 'http://root@foo/bar.git' }
|
||||
let(:env_vars) { [%q(HOME="temp")] }
|
||||
let(:extra_env_vars) { [] }
|
||||
let(:run_params) { { env: env_vars + extra_env_vars, sleep_internal: 0, log_prefix: "Git: " } }
|
||||
let(:run_params) { { raise_on_failure: true, env: env_vars + extra_env_vars, sleep_internal: 0, log_prefix: "Git: " } }
|
||||
let(:repository) do
|
||||
described_class.new(command_retry_sleep_interval: 0).tap do |r|
|
||||
r.uri = repo_uri
|
||||
|
|
|
|||
|
|
@ -24,4 +24,10 @@ RSpec.describe QA::Support::Run do
|
|||
|
||||
expect { class_instance.run(command) }.to raise_error(QA::Support::Run::CommandError, /The command .* failed \(1\) with the following output:\nFAILURE/)
|
||||
end
|
||||
|
||||
it 'returns the error message in a non-zero response when raise_on_failure is false' do
|
||||
allow(Open3).to receive(:capture2e).and_return([+'FAILURE', double(exitstatus: 1)])
|
||||
|
||||
expect(class_instance.run(command, raise_on_failure: false).response).to eql('FAILURE')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ RSpec.describe QA::Tools::Ci::ExportCodePathsMapping do
|
|||
}
|
||||
end
|
||||
|
||||
let(:commit_ref) { 'master' }
|
||||
let(:run_type) { 'e2e-test-on-gdk' }
|
||||
let(:file_path) { "#{commit_ref}/#{run_type}/test-code-paths-mapping-merged-pipeline-1.json" }
|
||||
|
||||
let(:pretty_generated_mapping_json) do
|
||||
JSON.pretty_generate(code_path_mappings_data)
|
||||
end
|
||||
|
|
@ -31,15 +35,15 @@ RSpec.describe QA::Tools::Ci::ExportCodePathsMapping do
|
|||
allow(Dir).to receive(:glob).with(glob) { file_paths }
|
||||
allow(::File).to receive(:read).with(anything).and_return(code_path_mappings_data.to_json)
|
||||
stub_env('QA_CODE_PATH_MAPPINGS_GCS_CREDENTIALS', gcs_credentials)
|
||||
stub_env('QA_RUN_TYPE', 'package-and-test')
|
||||
stub_env('QA_RUN_TYPE', run_type)
|
||||
stub_env('CI_COMMIT_REF_SLUG', commit_ref)
|
||||
stub_env('CI_PIPELINE_ID', 1)
|
||||
end
|
||||
|
||||
context "with mapping files present" do
|
||||
it "exports mapping json to GCS and writes it as job artifact", :aggregate_failures do
|
||||
expect(logger).to receive(:info).with("Number of mapping files found: #{file_paths.size}")
|
||||
expect(::File).to receive(:write).with(String, mapping_json).once
|
||||
expect(gcs_client).to receive(:put_object).with(gcs_bucket_name,
|
||||
String, pretty_generated_mapping_json)
|
||||
expect(gcs_client).to receive(:put_object).with(gcs_bucket_name, file_path, pretty_generated_mapping_json)
|
||||
described_class.export(glob)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env node
|
||||
import { relative, dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import Runtime from 'jest-runtime';
|
||||
import { readConfig } from 'jest-config';
|
||||
|
||||
import createJestConfig from '../../jest.config.base.js';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../');
|
||||
|
||||
function resolveDependenciesRecursively(context, target, seen) {
|
||||
const dependencies = context.hasteFS.getDependencies(target);
|
||||
|
||||
seen.add(target);
|
||||
if (!dependencies) {
|
||||
return [target];
|
||||
}
|
||||
|
||||
return [
|
||||
target,
|
||||
...dependencies.flatMap((file) => {
|
||||
const fileNormalized = file.replace(/\/+/g, '/');
|
||||
|
||||
const resolved = relative(
|
||||
context.config.rootDir,
|
||||
context.resolver.resolveModule(target, fileNormalized),
|
||||
);
|
||||
|
||||
if (resolved.startsWith('node_modules/')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (seen.has(resolved)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return resolveDependenciesRecursively(context, resolved, seen);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
async function getJestConfig(opts = {}) {
|
||||
return (
|
||||
await readConfig(
|
||||
{
|
||||
config: JSON.stringify(
|
||||
createJestConfig('spec/frontend', {
|
||||
roots: ['<rootDir>/spec/frontend/'],
|
||||
rootsEE: ['<rootDir>/ee/spec/frontend/'],
|
||||
rootsJH: ['<rootDir>/jh/spec/frontend/'],
|
||||
...opts,
|
||||
}),
|
||||
),
|
||||
},
|
||||
ROOT,
|
||||
)
|
||||
).projectConfig;
|
||||
}
|
||||
|
||||
async function resolveDependenciesWithJest(target, opts = {}) {
|
||||
const seen = new Set();
|
||||
const { isEE = false, isJH = false } = opts;
|
||||
const config = await getJestConfig({ isEE, isJH });
|
||||
const context = await Runtime.default.createContext(config, {
|
||||
maxWorkers: 1,
|
||||
console,
|
||||
watch: false,
|
||||
});
|
||||
return resolveDependenciesRecursively(context, target, seen);
|
||||
}
|
||||
|
||||
/**
|
||||
* This script takes a frontend files as its only argument and walks the tree to find all files it
|
||||
* depends on, then prints all their paths to stdin, including the provided file.
|
||||
*
|
||||
* Usage:
|
||||
* scripts/frontend/find_frontend_files.mjs path/to/file.js
|
||||
*
|
||||
* This can be useful when migrating Tailwind utils in a given file and we'd like to migrate all
|
||||
* dependents at the same time. Eg:
|
||||
* scripts/frontend/find_frontend_files.mjs path/to/file.js | ./node_modules/@gitlab/ui/bin/migrate_custom_utils_to_tw.bundled.mjs --from-stdin
|
||||
*/
|
||||
async function main() {
|
||||
if (process.argv.length !== 3) {
|
||||
console.warn('Please use with one argument exactly');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const target = process.argv[2];
|
||||
|
||||
const resolvedWithJest = new Set([
|
||||
...(await resolveDependenciesWithJest(target, { isEE: true })),
|
||||
...(await resolveDependenciesWithJest(target)),
|
||||
]);
|
||||
|
||||
console.log(
|
||||
Array.from(resolvedWithJest)
|
||||
.sort()
|
||||
.filter((x) => {
|
||||
return x.endsWith('.vue') || x.endsWith('.js');
|
||||
})
|
||||
.filter((x) => {
|
||||
return x.includes('app/assets/');
|
||||
})
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -38,6 +38,10 @@ FactoryBot.define do
|
|||
status { 'failed' }
|
||||
end
|
||||
|
||||
trait :canceling do
|
||||
status { 'canceling' }
|
||||
end
|
||||
|
||||
trait :canceled do
|
||||
status { 'canceled' }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -228,7 +228,10 @@ FactoryBot.define do
|
|||
discussion = discussion.to_discussion if discussion.is_a?(Note)
|
||||
next unless discussion
|
||||
|
||||
note.assign_attributes(discussion.reply_attributes.merge(project: discussion.project))
|
||||
parent_attributes = { project: discussion.project, namespace: discussion.namespace }.compact
|
||||
note.assign_attributes(
|
||||
discussion.reply_attributes.merge(parent_attributes)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -91,14 +91,14 @@ RSpec.describe "Admin Runners", :freeze_time, feature_category: :fleet_visibilit
|
|||
end
|
||||
end
|
||||
|
||||
it 'shows a running status badge that links to jobs tab' do
|
||||
it 'shows an Active status badge that links to jobs tab' do
|
||||
runner = create(:ci_runner, :project, projects: [project])
|
||||
job = create(:ci_build, :running, runner: runner)
|
||||
|
||||
visit admin_runners_path
|
||||
|
||||
within_runner_row(runner.id) do
|
||||
click_on(s_('Runners|Running'))
|
||||
click_on(s_('Runners|Active'))
|
||||
end
|
||||
|
||||
expect(current_url).to match(admin_runner_path(runner))
|
||||
|
|
|
|||
|
|
@ -2,18 +2,18 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Dashboard Group', feature_category: :groups_and_projects do
|
||||
RSpec.describe 'Dashboard Group', :js, feature_category: :groups_and_projects do
|
||||
before do
|
||||
sign_in(create(:user))
|
||||
end
|
||||
|
||||
it 'defaults sort dropdown to last created' do
|
||||
it 'defaults sort dropdown to Created date' do
|
||||
visit dashboard_groups_path
|
||||
|
||||
expect(page).to have_button('Last created')
|
||||
expect(page).to have_button('Created date')
|
||||
end
|
||||
|
||||
it 'creates new group', :js do
|
||||
it 'creates new group' do
|
||||
visit dashboard_groups_path
|
||||
find_by_testid('new-group-button').click
|
||||
click_link 'Create group'
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ RSpec.describe 'Dashboard Groups page', :js, feature_category: :groups_and_proje
|
|||
end
|
||||
|
||||
it 'expands when filtering groups' do
|
||||
fill_in 'filter', with: nested_group.name
|
||||
search(nested_group.name)
|
||||
wait_for_requests
|
||||
|
||||
expect(page).not_to have_content(group.name)
|
||||
|
|
@ -70,20 +70,21 @@ RSpec.describe 'Dashboard Groups page', :js, feature_category: :groups_and_proje
|
|||
end
|
||||
|
||||
it 'resets search when user cleans the input' do
|
||||
fill_in 'filter', with: group.name
|
||||
search(group.name)
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content(group.name)
|
||||
expect(page).not_to have_content(nested_group.parent.name)
|
||||
|
||||
fill_in 'filter', with: ''
|
||||
page.find('[name="filter"]').send_keys(:enter)
|
||||
find_by_testid('filtered-search-clear-button').click
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content(group.name)
|
||||
expect(page).to have_content(nested_group.parent.name)
|
||||
expect(page).not_to have_content(another_group.name)
|
||||
expect(page.all('.js-groups-list-holder .groups-list li').length).to eq 2
|
||||
within find_by_testid('groups-list-tree-container') do
|
||||
expect(find_all('li').length).to eq 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -248,4 +249,11 @@ RSpec.describe 'Dashboard Groups page', :js, feature_category: :groups_and_proje
|
|||
expect(page).to have_content(s_('GroupsEmptyState|A group is a collection of several projects'))
|
||||
end
|
||||
end
|
||||
|
||||
def search(term)
|
||||
filter_input = find_by_testid('filtered-search-term-input')
|
||||
filter_input.click
|
||||
filter_input.set(term)
|
||||
click_button 'Search'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ import { GlBadge } from '@gitlab/ui';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
|
||||
import {
|
||||
I18N_JOB_STATUS_RUNNING,
|
||||
I18N_JOB_STATUS_ACTIVE,
|
||||
I18N_JOB_STATUS_IDLE,
|
||||
JOB_STATUS_ACTIVE,
|
||||
JOB_STATUS_RUNNING,
|
||||
JOB_STATUS_IDLE,
|
||||
} from '~/ci/runner/constants';
|
||||
|
|
@ -24,7 +25,8 @@ describe('RunnerTypeBadge', () => {
|
|||
|
||||
it.each`
|
||||
jobStatus | classes | text
|
||||
${JOB_STATUS_RUNNING} | ${['gl-text-blue-600!', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_RUNNING}
|
||||
${JOB_STATUS_ACTIVE} | ${['gl-text-blue-600!', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_ACTIVE}
|
||||
${JOB_STATUS_RUNNING} | ${['gl-text-blue-600!', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_ACTIVE}
|
||||
${JOB_STATUS_IDLE} | ${['gl-text-gray-700!', 'gl-border-gray-500!']} | ${I18N_JOB_STATUS_IDLE}
|
||||
`(
|
||||
'renders $jobStatus job status with "$text" text and styles',
|
||||
|
|
@ -52,7 +54,7 @@ describe('RunnerTypeBadge', () => {
|
|||
});
|
||||
|
||||
it('adds arbitrary attributes', () => {
|
||||
createComponent({ props: { jobStatus: JOB_STATUS_RUNNING }, attrs: { href: '/url' } });
|
||||
createComponent({ props: { jobStatus: JOB_STATUS_ACTIVE }, attrs: { href: '/url' } });
|
||||
|
||||
expect(findBadge().attributes('href')).toBe('/url');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,16 +3,15 @@ import { GlEmptyState } from '@gitlab/ui';
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import GroupsDashboardEmptyState from '~/groups/components/empty_states/groups_dashboard_empty_state.vue';
|
||||
|
||||
jest.mock(
|
||||
'@gitlab/svgs/dist/illustrations/empty-state/empty-groups-md.svg?url',
|
||||
() => 'empty-groups-md.svg',
|
||||
);
|
||||
|
||||
let wrapper;
|
||||
|
||||
const defaultProvide = {
|
||||
groupsEmptyStateIllustration: '/assets/illustrations/empty-state/empty-groups-md.svg',
|
||||
};
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMountExtended(GroupsDashboardEmptyState, {
|
||||
provide: defaultProvide,
|
||||
});
|
||||
wrapper = shallowMountExtended(GroupsDashboardEmptyState);
|
||||
};
|
||||
|
||||
describe('GroupsDashboardEmptyState', () => {
|
||||
|
|
@ -23,7 +22,7 @@ describe('GroupsDashboardEmptyState', () => {
|
|||
title: 'A group is a collection of several projects',
|
||||
description:
|
||||
"If you organize your projects under a group, it works like a folder. You can manage your group member's permissions and access to each project in the group.",
|
||||
svgPath: defaultProvide.groupsEmptyStateIllustration,
|
||||
svgPath: 'empty-groups-md.svg',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,16 +3,15 @@ import { GlEmptyState } from '@gitlab/ui';
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import GroupsExploreEmptyState from '~/groups/components/empty_states/groups_explore_empty_state.vue';
|
||||
|
||||
jest.mock(
|
||||
'@gitlab/svgs/dist/illustrations/empty-state/empty-groups-md.svg?url',
|
||||
() => 'empty-groups-md.svg',
|
||||
);
|
||||
|
||||
let wrapper;
|
||||
|
||||
const defaultProvide = {
|
||||
groupsEmptyStateIllustration: '/assets/illustrations/empty-state/empty-groups-md.svg',
|
||||
};
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMountExtended(GroupsExploreEmptyState, {
|
||||
provide: defaultProvide,
|
||||
});
|
||||
wrapper = shallowMountExtended(GroupsExploreEmptyState);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -25,7 +24,7 @@ describe('GroupsExploreEmptyState', () => {
|
|||
|
||||
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
|
||||
title: 'No public or internal groups',
|
||||
svgPath: defaultProvide.groupsEmptyStateIllustration,
|
||||
svgPath: 'empty-groups-md.svg',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
import App from '~/groups/components/groups_explore_app.vue';
|
||||
import { createRouter } from '~/groups/init_groups_explore';
|
||||
import GroupsListWithFilteredSearchApp from '~/groups/components/groups_list_with_filtered_search_app.vue';
|
||||
import { createRouter } from '~/groups/init_groups_list_with_filtered_search';
|
||||
import {
|
||||
SORTING_ITEM_CREATED,
|
||||
SORTING_ITEM_UPDATED,
|
||||
EXPLORE_SORTING_ITEMS,
|
||||
EXPLORE_FILTERED_SEARCH_TERM_KEY,
|
||||
GROUPS_LIST_SORTING_ITEMS,
|
||||
GROUPS_LIST_FILTERED_SEARCH_TERM_KEY,
|
||||
EXPLORE_FILTERED_SEARCH_NAMESPACE,
|
||||
} from '~/groups/constants';
|
||||
import { RECENT_SEARCHES_STORAGE_KEY_GROUPS } from '~/filtered_search/recent_searches_storage_keys';
|
||||
import FilteredSearchAndSort from '~/groups_projects/components/filtered_search_and_sort.vue';
|
||||
import GroupsApp from '~/groups/components/app.vue';
|
||||
import EmptyState from '~/groups/components/empty_states/groups_explore_empty_state.vue';
|
||||
import GroupsService from '~/groups/service/groups_service';
|
||||
import GroupsStore from '~/groups/store/groups_store';
|
||||
import eventHub from '~/groups/event_hub';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
describe('GroupsExploreApp', () => {
|
||||
describe('GroupsListWithFilteredSearch', () => {
|
||||
const router = createRouter();
|
||||
const routerMock = {
|
||||
push: jest.fn(),
|
||||
|
|
@ -24,16 +23,22 @@ describe('GroupsExploreApp', () => {
|
|||
|
||||
let wrapper;
|
||||
|
||||
const defaultProvide = {
|
||||
const defaultPropsData = {
|
||||
filteredSearchNamespace: EXPLORE_FILTERED_SEARCH_NAMESPACE,
|
||||
endpoint: '/explore/groups.json',
|
||||
initialSort: SORTING_ITEM_UPDATED.asc,
|
||||
};
|
||||
|
||||
const createComponent = ({ routeQuery = { [EXPLORE_FILTERED_SEARCH_TERM_KEY]: 'foo' } } = {}) => {
|
||||
wrapper = shallowMountExtended(App, {
|
||||
const createComponent = ({
|
||||
routeQuery = { [GROUPS_LIST_FILTERED_SEARCH_TERM_KEY]: 'foo' },
|
||||
} = {}) => {
|
||||
wrapper = shallowMountExtended(GroupsListWithFilteredSearchApp, {
|
||||
router,
|
||||
mocks: { $route: { path: '/', query: routeQuery }, $router: routerMock },
|
||||
provide: defaultProvide,
|
||||
propsData: defaultPropsData,
|
||||
scopedSlots: {
|
||||
'empty-state': '<div data-testid="empty-state"></div>',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -44,11 +49,11 @@ describe('GroupsExploreApp', () => {
|
|||
|
||||
expect(findFilteredSearchAndSort().props()).toMatchObject({
|
||||
filteredSearchTokens: [],
|
||||
filteredSearchQuery: { [EXPLORE_FILTERED_SEARCH_TERM_KEY]: 'foo' },
|
||||
filteredSearchTermKey: EXPLORE_FILTERED_SEARCH_TERM_KEY,
|
||||
filteredSearchQuery: { [GROUPS_LIST_FILTERED_SEARCH_TERM_KEY]: 'foo' },
|
||||
filteredSearchTermKey: GROUPS_LIST_FILTERED_SEARCH_TERM_KEY,
|
||||
filteredSearchNamespace: EXPLORE_FILTERED_SEARCH_NAMESPACE,
|
||||
filteredSearchRecentSearchesStorageKey: RECENT_SEARCHES_STORAGE_KEY_GROUPS,
|
||||
sortOptions: EXPLORE_SORTING_ITEMS.map((sortItem) => ({
|
||||
sortOptions: GROUPS_LIST_SORTING_ITEMS.map((sortItem) => ({
|
||||
value: sortItem.asc,
|
||||
text: sortItem.label,
|
||||
})),
|
||||
|
|
@ -63,14 +68,14 @@ describe('GroupsExploreApp', () => {
|
|||
it('renders `GroupsApp` and empty state', () => {
|
||||
createComponent();
|
||||
|
||||
const service = new GroupsService(defaultProvide.endpoint);
|
||||
const service = new GroupsService(defaultPropsData.endpoint);
|
||||
const store = new GroupsStore({ hideProjects: true });
|
||||
|
||||
expect(wrapper.findComponent(GroupsApp).props()).toMatchObject({
|
||||
service,
|
||||
store,
|
||||
});
|
||||
expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
|
||||
expect(wrapper.findByTestId('empty-state').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when filtered search bar is submitted', () => {
|
||||
|
|
@ -81,20 +86,20 @@ describe('GroupsExploreApp', () => {
|
|||
createComponent();
|
||||
|
||||
findFilteredSearchAndSort().vm.$emit('filter', {
|
||||
[EXPLORE_FILTERED_SEARCH_TERM_KEY]: searchTerm,
|
||||
[GROUPS_LIST_FILTERED_SEARCH_TERM_KEY]: searchTerm,
|
||||
});
|
||||
});
|
||||
|
||||
it(`updates \`${EXPLORE_FILTERED_SEARCH_TERM_KEY}\` query string`, () => {
|
||||
it(`updates \`${GROUPS_LIST_FILTERED_SEARCH_TERM_KEY}\` query string`, () => {
|
||||
expect(routerMock.push).toHaveBeenCalledWith({
|
||||
query: { [EXPLORE_FILTERED_SEARCH_TERM_KEY]: searchTerm },
|
||||
query: { [GROUPS_LIST_FILTERED_SEARCH_TERM_KEY]: searchTerm },
|
||||
});
|
||||
});
|
||||
|
||||
it('emits `fetchFilteredAndSortedGroups` with correct arguments', () => {
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('fetchFilteredAndSortedGroups', {
|
||||
filterGroupsBy: searchTerm,
|
||||
sortBy: defaultProvide.initialSort,
|
||||
sortBy: defaultPropsData.initialSort,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -104,11 +109,11 @@ describe('GroupsExploreApp', () => {
|
|||
createComponent();
|
||||
|
||||
findFilteredSearchAndSort().vm.$emit('filter', {
|
||||
[EXPLORE_FILTERED_SEARCH_TERM_KEY]: '',
|
||||
[GROUPS_LIST_FILTERED_SEARCH_TERM_KEY]: '',
|
||||
});
|
||||
});
|
||||
|
||||
it(`removes \`${EXPLORE_FILTERED_SEARCH_TERM_KEY}\` query string`, () => {
|
||||
it(`removes \`${GROUPS_LIST_FILTERED_SEARCH_TERM_KEY}\` query string`, () => {
|
||||
expect(routerMock.push).toHaveBeenCalledWith({
|
||||
query: {},
|
||||
});
|
||||
|
|
@ -119,7 +124,7 @@ describe('GroupsExploreApp', () => {
|
|||
beforeEach(() => {
|
||||
createComponent({
|
||||
routeQuery: {
|
||||
[EXPLORE_FILTERED_SEARCH_TERM_KEY]: 'foo',
|
||||
[GROUPS_LIST_FILTERED_SEARCH_TERM_KEY]: 'foo',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -130,7 +135,7 @@ describe('GroupsExploreApp', () => {
|
|||
expect(routerMock.push).toHaveBeenCalledWith({
|
||||
query: {
|
||||
sort: SORTING_ITEM_CREATED.asc,
|
||||
[EXPLORE_FILTERED_SEARCH_TERM_KEY]: 'foo',
|
||||
[GROUPS_LIST_FILTERED_SEARCH_TERM_KEY]: 'foo',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -140,7 +145,7 @@ describe('GroupsExploreApp', () => {
|
|||
beforeEach(() => {
|
||||
createComponent({
|
||||
routeQuery: {
|
||||
[EXPLORE_FILTERED_SEARCH_TERM_KEY]: 'foo',
|
||||
[GROUPS_LIST_FILTERED_SEARCH_TERM_KEY]: 'foo',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -151,7 +156,7 @@ describe('GroupsExploreApp', () => {
|
|||
expect(routerMock.push).toHaveBeenCalledWith({
|
||||
query: {
|
||||
sort: SORTING_ITEM_UPDATED.desc,
|
||||
[EXPLORE_FILTERED_SEARCH_TERM_KEY]: 'foo',
|
||||
[GROUPS_LIST_FILTERED_SEARCH_TERM_KEY]: 'foo',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -269,7 +269,7 @@ describe('Description component', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
|
||||
eventHub.$emit('convert-task-list-item', '4:4-8:19');
|
||||
eventHub.$emit('convert-task-list-item', { id: '1', sourcepos: '4:4-8:19' });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
|
|
@ -324,7 +324,7 @@ describe('Description component', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
|
||||
eventHub.$emit('convert-task-list-item', '1:1-1:11');
|
||||
eventHub.$emit('convert-task-list-item', { id: '1', sourcepos: '1:1-1:11' });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
|
|
@ -355,7 +355,7 @@ describe('Description component', () => {
|
|||
props: { descriptionText },
|
||||
});
|
||||
|
||||
eventHub.$emit('delete-task-list-item', '4:4-5:19');
|
||||
eventHub.$emit('delete-task-list-item', { id: '1', sourcepos: '4:4-5:19' });
|
||||
|
||||
expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { GlDisclosureDropdown } from '@gitlab/ui';
|
||||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { TYPE_EPIC, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
|
||||
import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
|
||||
|
|
@ -14,14 +15,18 @@ describe('TaskListItemActions component', () => {
|
|||
const findDeleteItem = () => wrapper.findByTestId('delete');
|
||||
|
||||
const mountComponent = ({ issuableType = TYPE_ISSUE } = {}) => {
|
||||
const li = document.createElement('li');
|
||||
li.dataset.sourcepos = '3:1-3:10';
|
||||
li.appendChild(document.createElement('div'));
|
||||
document.body.appendChild(li);
|
||||
setHTMLFixture(`
|
||||
<li data-sourcepos="3:1-3:10">
|
||||
<div></div>
|
||||
</li>
|
||||
`);
|
||||
|
||||
wrapper = shallowMountExtended(TaskListItemActions, {
|
||||
provide: { issuableType },
|
||||
attachTo: document.querySelector('div'),
|
||||
provide: {
|
||||
id: 'gid://gitlab/WorkItem/818',
|
||||
issuableType,
|
||||
},
|
||||
attachTo: 'div',
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -60,13 +65,19 @@ describe('TaskListItemActions component', () => {
|
|||
it('emits event when `Convert to task` dropdown item is clicked', () => {
|
||||
findConvertToTaskItem().vm.$emit('action');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', {
|
||||
id: 'gid://gitlab/WorkItem/818',
|
||||
sourcepos: '3:1-3:10',
|
||||
});
|
||||
});
|
||||
|
||||
it('emits event when `Delete` dropdown item is clicked', () => {
|
||||
findDeleteItem().vm.$emit('action');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', {
|
||||
id: 'gid://gitlab/WorkItem/818',
|
||||
sourcepos: '3:1-3:10',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
import {
|
||||
deleteTaskListItem,
|
||||
convertDescriptionWithNewSort,
|
||||
deleteTaskListItem,
|
||||
extractTaskTitleAndDescription,
|
||||
insertNextToTaskListItemText,
|
||||
} from '~/issues/show/utils';
|
||||
|
||||
describe('app/assets/javascripts/issues/show/utils.js', () => {
|
||||
|
|
@ -408,3 +410,86 @@ description`;
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertNextToTaskListItemText', () => {
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.classList.add('dropdown');
|
||||
|
||||
describe('when simple checkbox with text', () => {
|
||||
it('inserts element as sibling to checkbox, last child of ul element', () => {
|
||||
setHTMLFixture(`
|
||||
<div class="description">
|
||||
<div class="md">
|
||||
<ul data-sourcepos="1:1-1:9" class="task-list">
|
||||
<li data-sourcepos="1:1-1:9" class="task-list-item">
|
||||
<input type="checkbox" class="task-list-item-checkbox">
|
||||
one
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
const listItem = document.querySelector('.task-list-item');
|
||||
insertNextToTaskListItemText(dropdown, listItem);
|
||||
|
||||
expect(listItem.lastChild).toBe(dropdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when checkbox with nested checkbox', () => {
|
||||
it('inserts element as sibling to checkbox, before the nested checkbox', () => {
|
||||
setHTMLFixture(`
|
||||
<div class="description">
|
||||
<div class="md">
|
||||
<ul data-sourcepos="1:1-1:9" class="task-list">
|
||||
<li data-sourcepos="1:1-3:14" class="task-list-item">
|
||||
<input type="checkbox" class="task-list-item-checkbox">
|
||||
one
|
||||
<ul data-sourcepos="2:3-3:14" class="task-list">
|
||||
<li data-sourcepos="2:3-2:14" class="task-list-item">
|
||||
<input type="checkbox" class="task-list-item-checkbox">
|
||||
two
|
||||
</li>
|
||||
<li data-sourcepos="3:3-3:14" class="task-list-item enabled">
|
||||
<input type="checkbox" class="task-list-item-checkbox">
|
||||
three
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
const listItem = document.querySelector('.task-list-item');
|
||||
insertNextToTaskListItemText(dropdown, listItem);
|
||||
|
||||
expect(listItem.lastElementChild.previousSibling).toBe(dropdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when multi-paragraph checkbox', () => {
|
||||
it('inserts element as sibling to checkbox, inside p element', () => {
|
||||
setHTMLFixture(`
|
||||
<div class="description">
|
||||
<div class="md">
|
||||
<ul data-sourcepos="1:1-1:9" class="task-list">
|
||||
<li data-sourcepos="1:1-3:11" class="task-list-item">
|
||||
<p data-sourcepos="1:3-1:9">
|
||||
<input type="checkbox" class="task-list-item-checkbox">
|
||||
one
|
||||
</p>
|
||||
<p data-sourcepos="3:3-3:11">
|
||||
paragraph
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
const listItem = document.querySelector('.task-list-item');
|
||||
insertNextToTaskListItemText(dropdown, listItem);
|
||||
|
||||
expect(listItem.firstElementChild.lastElementChild).toBe(dropdown);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
|
||||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import { descriptionTextWithCheckboxes, descriptionHtmlWithCheckboxes } from '../mock_data';
|
||||
import eventHub from '~/issues/show/event_hub';
|
||||
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
|
||||
import { descriptionHtmlWithCheckboxes, descriptionTextWithCheckboxes } from '../mock_data';
|
||||
|
||||
jest.mock('~/behaviors/markdown/render_gfm');
|
||||
|
||||
describe('WorkItemDescription', () => {
|
||||
describe('WorkItemDescriptionRendered', () => {
|
||||
let wrapper;
|
||||
|
||||
const findCheckboxAtIndex = (index) => wrapper.findAll('input[type="checkbox"]').at(index);
|
||||
|
|
@ -24,6 +25,7 @@ describe('WorkItemDescription', () => {
|
|||
} = {}) => {
|
||||
wrapper = shallowMount(WorkItemDescriptionRendered, {
|
||||
propsData: {
|
||||
workItemId: 'gid://gitlab/WorkItem/818',
|
||||
workItemDescription,
|
||||
canEdit,
|
||||
},
|
||||
|
|
@ -124,4 +126,30 @@ describe('WorkItemDescription', () => {
|
|||
expect(wrapper.find('[data-test-id="description-read-more"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('task list item actions', () => {
|
||||
describe('deleting the task list item', () => {
|
||||
it('emits an event to update the description with the deleted task list item', () => {
|
||||
const description = `Tasks
|
||||
|
||||
1. [ ] item 1
|
||||
1. [ ] item 2
|
||||
1. [ ] item 3
|
||||
1. [ ] item 4;`;
|
||||
const newDescription = `Tasks
|
||||
|
||||
1. [ ] item 1
|
||||
1. [ ] item 3
|
||||
1. [ ] item 4;`;
|
||||
createComponent({ workItemDescription: { description } });
|
||||
|
||||
eventHub.$emit('delete-task-list-item', {
|
||||
id: 'gid://gitlab/WorkItem/818',
|
||||
sourcepos: '4:4-5:19',
|
||||
});
|
||||
|
||||
expect(wrapper.emitted('descriptionUpdated')).toEqual([[newDescription]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,18 +4,29 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe GitlabSchema.types['MlModel'], feature_category: :mlops do
|
||||
let_it_be(:model) { create(:ml_models, :with_latest_version_and_package, description: 'A description') }
|
||||
let_it_be(:model_markdown) { create(:ml_models, :with_latest_version_and_package, description: 'A **description**') }
|
||||
let_it_be(:project) { model.project }
|
||||
let_it_be(:project_markdown) { model_markdown.project }
|
||||
let_it_be(:candidates) { Array.new(2) { create(:ml_candidates, experiment: model.default_experiment) } }
|
||||
let_it_be(:candidates_markdown) do
|
||||
Array.new(2) do
|
||||
create(:ml_candidates, experiment: model_markdown.default_experiment)
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:model_id) { GitlabSchema.id_from_object(model).to_s }
|
||||
let_it_be(:model_version_id) { GitlabSchema.id_from_object(model.latest_version).to_s }
|
||||
|
||||
let_it_be(:model_id_markdown) { GitlabSchema.id_from_object(model_markdown).to_s }
|
||||
let_it_be(:model_version_id_markdown) { GitlabSchema.id_from_object(model_markdown.latest_version).to_s }
|
||||
|
||||
let(:query) do
|
||||
%(
|
||||
query {
|
||||
mlModel(id: "#{model_id}") {
|
||||
id
|
||||
description
|
||||
descriptionHtml
|
||||
name
|
||||
versionCount
|
||||
candidateCount
|
||||
|
|
@ -33,13 +44,39 @@ RSpec.describe GitlabSchema.types['MlModel'], feature_category: :mlops do
|
|||
)
|
||||
end
|
||||
|
||||
let(:query_markdown) do
|
||||
%(
|
||||
query {
|
||||
mlModel(id: "#{model_id_markdown}") {
|
||||
id
|
||||
description
|
||||
descriptionHtml
|
||||
name
|
||||
versionCount
|
||||
candidateCount
|
||||
latestVersion {
|
||||
id
|
||||
}
|
||||
version(modelVersionId: "#{model_version_id_markdown}") {
|
||||
id
|
||||
}
|
||||
_links {
|
||||
showPath
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
let(:data) { GitlabSchema.execute(query, context: { current_user: project.owner }).as_json }
|
||||
|
||||
specify { expect(described_class.description).to eq('Machine learning model in the model registry') }
|
||||
|
||||
subject(:data) { GitlabSchema.execute(query, context: { current_user: project.owner }).as_json }
|
||||
subject(:data_markdown) { GitlabSchema.execute(query_markdown, context: { current_user: project.owner }).as_json }
|
||||
|
||||
it 'includes all the fields' do
|
||||
expected_fields = %w[id name versions candidates version_count _links created_at latest_version description
|
||||
candidate_count description version]
|
||||
candidate_count description version description_html]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
end
|
||||
|
|
@ -51,6 +88,8 @@ RSpec.describe GitlabSchema.types['MlModel'], feature_category: :mlops do
|
|||
'id' => model_id,
|
||||
'name' => model.name,
|
||||
'description' => 'A description',
|
||||
'descriptionHtml' =>
|
||||
'<p data-sourcepos="1:1-1:13" dir="auto">A description</p>',
|
||||
'latestVersion' => {
|
||||
'id' => model_version_id
|
||||
},
|
||||
|
|
@ -64,4 +103,27 @@ RSpec.describe GitlabSchema.types['MlModel'], feature_category: :mlops do
|
|||
}
|
||||
})
|
||||
end
|
||||
|
||||
it 'computes the correct properties with markdown' do
|
||||
model_data = data_markdown.dig('data', 'mlModel')
|
||||
|
||||
expect(model_data).to eq({
|
||||
'id' => model_id_markdown,
|
||||
'name' => model_markdown.name,
|
||||
'description' => model_markdown.description,
|
||||
'descriptionHtml' =>
|
||||
'<p data-sourcepos="1:1-1:17" dir="auto">A <strong data-sourcepos="1:3-1:17">description</strong></p>',
|
||||
'latestVersion' => {
|
||||
'id' => model_version_id_markdown
|
||||
},
|
||||
'version' => {
|
||||
'id' => model_version_id_markdown
|
||||
},
|
||||
'versionCount' => 1,
|
||||
'candidateCount' => 2,
|
||||
'_links' => {
|
||||
'showPath' => "/#{project_markdown.full_path}/-/ml/models/#{model_markdown.id}"
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ RSpec.describe Projects::Ml::ModelRegistryHelper, feature_category: :mlops do
|
|||
'createModelPath' => "/#{project.full_path}/-/ml/models/new",
|
||||
'canWriteModelRegistry' => true,
|
||||
'maxAllowedFileSize' => 10737418240,
|
||||
'mlflowTrackingUrl' => "http://localhost/api/v4/projects/#{project.id}/ml/mlflow/"
|
||||
'mlflowTrackingUrl' => "http://localhost/api/v4/projects/#{project.id}/ml/mlflow/",
|
||||
'markdownPreviewPath' => "http://localhost/#{project.full_path}/-/ml/preview_markdown"
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -47,22 +47,4 @@ RSpec.describe Gitlab::SidekiqMiddleware::SetIpAddress, feature_category: :syste
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#call with sidekiq_ip_address disabled' do
|
||||
before do
|
||||
stub_feature_flags(sidekiq_ip_address: false)
|
||||
end
|
||||
|
||||
context 'when the IP address is present' do
|
||||
it 'does not set the IP address' do
|
||||
expect(::Gitlab::IpAddressState).not_to receive(:with).with(ip_address)
|
||||
|
||||
described_class.new.call(worker, job, queue) do
|
||||
expect(::Gitlab::IpAddressState.current).to eq(nil)
|
||||
end
|
||||
|
||||
expect(::Gitlab::IpAddressState.current).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -199,21 +199,33 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo
|
|||
end
|
||||
end
|
||||
|
||||
describe '.with_running_builds' do
|
||||
subject(:scope) { described_class.with_running_builds }
|
||||
describe '.with_executing_builds' do
|
||||
subject(:scope) { described_class.with_executing_builds }
|
||||
|
||||
let_it_be(:runner) { create(:ci_runner) }
|
||||
let_it_be(:runner_manager1) { create(:ci_runner_machine, runner: runner) }
|
||||
let_it_be(:runner_manager2) { create(:ci_runner_machine, runner: runner) }
|
||||
|
||||
before_all do
|
||||
create(:ci_runner_machine_build, runner_manager: runner_manager1,
|
||||
build: create(:ci_build, :success, runner: runner))
|
||||
create(:ci_runner_machine_build, runner_manager: runner_manager2,
|
||||
build: create(:ci_build, :running, runner: runner))
|
||||
let_it_be(:runner_managers_by_status) do
|
||||
Ci::HasStatus::AVAILABLE_STATUSES.index_with { |_status| create(:ci_runner_machine, runner: runner) }
|
||||
end
|
||||
|
||||
it { is_expected.to contain_exactly runner_manager2 }
|
||||
let_it_be(:busy_runner_managers) do
|
||||
Ci::HasStatus::EXECUTING_STATUSES.map { |status| runner_managers_by_status[status] }
|
||||
end
|
||||
|
||||
context 'with no builds running' do
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'with builds' do
|
||||
before_all do
|
||||
Ci::HasStatus::AVAILABLE_STATUSES.each do |status|
|
||||
runner_manager = runner_managers_by_status[status]
|
||||
build = create(:ci_build, status, runner: runner)
|
||||
create(:ci_runner_machine_build, runner_manager: runner_manager, build: build)
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to match_array(busy_runner_managers) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.order_id_desc' do
|
||||
|
|
|
|||
|
|
@ -650,27 +650,31 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.with_running_builds' do
|
||||
subject { described_class.with_running_builds }
|
||||
describe '.with_executing_builds' do
|
||||
subject(:scope) { described_class.with_executing_builds }
|
||||
|
||||
let_it_be(:runner1) { create(:ci_runner) }
|
||||
let_it_be(:runners_by_status) do
|
||||
Ci::HasStatus::AVAILABLE_STATUSES.index_with { |_status| create(:ci_runner) }
|
||||
end
|
||||
|
||||
let_it_be(:busy_runners) do
|
||||
Ci::HasStatus::EXECUTING_STATUSES.map { |status| runners_by_status[status] }
|
||||
end
|
||||
|
||||
context 'with no builds running' do
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'with single build running on runner2' do
|
||||
let(:runner2) { create(:ci_runner) }
|
||||
let(:runner3) { create(:ci_runner) }
|
||||
context 'with builds' do
|
||||
before_all do
|
||||
pipeline = create(:ci_pipeline, :running)
|
||||
|
||||
before do
|
||||
project = create(:project, :repository)
|
||||
pipeline = create(:ci_pipeline, project: project)
|
||||
create(:ci_build, :running, runner: runner2, pipeline: pipeline)
|
||||
create(:ci_build, :running, runner: runner3, pipeline: pipeline)
|
||||
Ci::HasStatus::AVAILABLE_STATUSES.each do |status|
|
||||
create(:ci_build, status, runner: runners_by_status[status], pipeline: pipeline)
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to contain_exactly(runner2, runner3) }
|
||||
it { is_expected.to match_array(busy_runners) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -284,14 +284,26 @@ RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.alive' do
|
||||
subject { CommitStatus.alive }
|
||||
describe '.executing' do
|
||||
subject { CommitStatus.executing }
|
||||
|
||||
%i[running pending waiting_for_callback waiting_for_resource preparing created].each do |status|
|
||||
%i[running canceling].each do |status|
|
||||
it_behaves_like 'containing the job', status
|
||||
end
|
||||
|
||||
%i[failed success].each do |status|
|
||||
%i[created failed success canceled].each do |status|
|
||||
it_behaves_like 'not containing the job', status
|
||||
end
|
||||
end
|
||||
|
||||
describe '.alive' do
|
||||
subject { CommitStatus.alive }
|
||||
|
||||
%i[running pending waiting_for_callback waiting_for_resource preparing created canceling].each do |status|
|
||||
it_behaves_like 'containing the job', status
|
||||
end
|
||||
|
||||
%i[canceled failed success].each do |status|
|
||||
it_behaves_like 'not containing the job', status
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :fleet_visibi
|
|||
active: runner.active,
|
||||
paused: !runner.active,
|
||||
status: runner.status.to_s.upcase,
|
||||
job_execution_status: runner.builds.running.any? ? 'RUNNING' : 'IDLE',
|
||||
job_execution_status: runner.builds.executing.any? ? 'RUNNING' : 'IDLE',
|
||||
maximum_timeout: runner.maximum_timeout,
|
||||
access_level: runner.access_level.to_s.upcase,
|
||||
run_untagged: runner.run_untagged,
|
||||
|
|
@ -122,7 +122,7 @@ RSpec.describe 'Query.runner(id)', :freeze_time, feature_category: :fleet_visibi
|
|||
architecture_name: runner_manager.architecture,
|
||||
platform_name: runner_manager.platform,
|
||||
status: runner_manager.status.to_s.upcase,
|
||||
job_execution_status: runner_manager.builds.running.any? ? 'RUNNING' : 'IDLE'
|
||||
job_execution_status: runner_manager.builds.executing.any? ? 'RUNNING' : 'IDLE'
|
||||
)
|
||||
end,
|
||||
"pageInfo" => anything
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::MlController, feature_category: :mlops do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { project.first_owner }
|
||||
let_it_be(:model1) { create(:ml_models, :with_versions, project: project) }
|
||||
|
||||
let(:read_model_registry) { true }
|
||||
let(:write_model_registry) { true }
|
||||
|
||||
let(:params) { {} }
|
||||
|
||||
before do
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
allow(Ability).to receive(:allowed?)
|
||||
.with(user, :read_model_registry, project)
|
||||
.and_return(read_model_registry)
|
||||
allow(Ability).to receive(:allowed?)
|
||||
.with(user, :write_model_registry, project)
|
||||
.and_return(write_model_registry)
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'POST #preview_markdown' do
|
||||
it 'renders json in a correct format' do
|
||||
preview_markdown
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.content_type).to eq('application/json; charset=utf-8')
|
||||
expect(json_response.keys).to match_array(%w[body references])
|
||||
expect(json_response['body']).to eq('<p data-sourcepos="1:1-1:4" dir="auto">test</p>')
|
||||
expect(json_response['references']).to eq({ "commands" => "", "suggestions" => [], "users" => [] })
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preview_markdown
|
||||
post project_ml_preview_markdown_path(project, params: { text: 'test' })
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue