Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-30 18:08:56 +00:00
parent 038366a093
commit 98d7cc758f
300 changed files with 3315 additions and 382 deletions

View File

@ -226,7 +226,7 @@ export default {
<a
ref="titleWrapper"
:v-once="!viewDiffsFileByFile"
class="gl-mr-2 gl-text-decoration-none!"
class="gl-mr-2 gl-text-decoration-none! gl-text-truncate"
:href="titleLink"
@click="handleFileNameClick"
>

View File

@ -1,33 +1,29 @@
<script>
import { isEmpty } from 'lodash';
import { mapActions, mapState } from 'vuex';
import { GlLink, GlButton, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { GlButton, GlIcon, GlLink } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import DetailRow from './sidebar_detail_row.vue';
import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
import SidebarJobDetailsContainer from './sidebar_job_details_container.vue';
export default {
name: 'JobSidebar',
components: {
ArtifactsBlock,
CommitBlock,
DetailRow,
GlIcon,
TriggerBlock,
StagesDropdown,
JobsContainer,
GlLink,
GlButton,
SidebarJobDetailsContainer,
TooltipOnTruncate,
},
mixins: [timeagoMixin],
props: {
artifactHelpUrl: {
type: String,
@ -42,53 +38,12 @@ export default {
},
computed: {
...mapState(['job', 'stages', 'jobs', 'selectedStage']),
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
let className = 'js-retry-button btn btn-retry';
className +=
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
return '';
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += sprintf(__(` (from %{timeoutSource})`), {
timeoutSource: this.job.metadata.timeout_source,
});
}
return t;
},
renderBlock() {
return (
this.job.duration ||
this.job.finished_at ||
this.job.erased_at ||
this.job.queued ||
this.hasTimeout ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length
);
},
hasArtifact() {
return !isEmpty(this.job.artifact);
},
@ -96,16 +51,10 @@ export default {
return !isEmpty(this.job.trigger);
},
hasStages() {
return (
(this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0) ||
false
);
return this.job?.pipeline?.stages?.length > 0;
},
commit() {
return this.job.pipeline && this.job.pipeline.commit ? this.job.pipeline.commit : {};
return this.job?.pipeline?.commit || {};
},
},
methods: {
@ -131,22 +80,22 @@ export default {
data-method="post"
data-qa-selector="retry_button"
rel="nofollow"
>{{ __('Retry') }}</gl-link
>
>{{ __('Retry') }}
</gl-link>
<gl-link
v-if="job.cancel_path"
:href="job.cancel_path"
class="js-cancel-job btn btn-default"
data-method="post"
rel="nofollow"
>{{ __('Cancel') }}</gl-link
>
>{{ __('Cancel') }}
</gl-link>
</div>
<gl-button
:aria-label="__('Toggle Sidebar')"
class="d-md-none gl-ml-2 js-sidebar-build-toggle"
category="tertiary"
class="gl-display-md-none gl-ml-2 js-sidebar-build-toggle"
icon="chevron-double-lg-right"
@click="toggleSidebar"
/>
@ -158,77 +107,37 @@ export default {
:href="job.new_issue_path"
class="btn btn-success btn-inverted float-left mr-2"
data-testid="job-new-issue"
>{{ __('New issue') }}</gl-link
>
>{{ __('New issue') }}
</gl-link>
<gl-link
v-if="job.terminal_path"
:href="job.terminal_path"
class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left"
target="_blank"
>
{{ __('Debug') }} <gl-icon name="external-link" :size="14" />
{{ __('Debug') }}
<gl-icon :size="14" name="external-link" />
</gl-link>
</div>
<div v-if="renderBlock" class="block">
<detail-row
v-if="job.duration"
:value="duration"
class="js-job-duration"
title="Duration"
/>
<detail-row
v-if="job.finished_at"
:value="timeFormatted(job.finished_at)"
class="js-job-finished"
title="Finished"
/>
<detail-row
v-if="job.erased_at"
:value="timeFormatted(job.erased_at)"
class="js-job-erased"
title="Erased"
/>
<detail-row v-if="job.queued" :value="queued" class="js-job-queued" title="Queued" />
<detail-row
v-if="hasTimeout"
:help-url="runnerHelpUrl"
:value="timeout"
class="js-job-timeout"
title="Timeout"
/>
<detail-row v-if="job.runner" :value="runnerId" class="js-job-runner" title="Runner" />
<detail-row
v-if="job.coverage"
:value="coverage"
class="js-job-coverage"
title="Coverage"
/>
<p v-if="job.tags.length" class="build-detail-row js-job-tags">
<span class="font-weight-bold">{{ __('Tags:') }}</span>
<span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{
tag
}}</span>
</p>
</div>
<sidebar-job-details-container :runner-help-url="runnerHelpUrl" />
<artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" />
<trigger-block v-if="hasTriggers" :trigger="job.trigger" />
<commit-block
:is-last-block="hasStages"
:commit="commit"
:is-last-block="hasStages"
:merge-request="job.merge_request"
/>
<stages-dropdown
:stages="stages"
v-if="job.pipeline"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
:stages="stages"
@requestSidebarStageDropdown="fetchJobsForStage"
/>
</div>
<jobs-container v-if="jobs.length" :jobs="jobs" :job-id="job.id" />
<jobs-container v-if="jobs.length" :job-id="job.id" :jobs="jobs" />
</div>
</aside>
</template>

View File

@ -0,0 +1,102 @@
<script>
import { mapState } from 'vuex';
import DetailRow from './sidebar_detail_row.vue';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
export default {
name: 'SidebarJobDetailsContainer',
components: {
DetailRow,
},
mixins: [timeagoMixin],
props: {
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState(['job']),
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
erasedAt() {
return this.timeFormatted(this.job.erased_at);
},
finishedAt() {
return this.timeFormatted(this.job.finished_at);
},
hasTags() {
return this.job?.tags?.length;
},
hasTimeout() {
return this.job?.metadata?.timeout_human_readable ?? false;
},
hasAnyDetail() {
return Boolean(
this.job.duration ||
this.job.finished_at ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage,
);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
shouldRenderBlock() {
return Boolean(this.hasAnyDetail || this.hasTimeout || this.hasTags);
},
timeout() {
return `${this.job?.metadata?.timeout_human_readable}${this.timeoutSource}`;
},
timeoutSource() {
if (!this.job?.metadata?.timeout_source) {
return '';
}
return sprintf(__(` (from %{timeoutSource})`), {
timeoutSource: this.job.metadata.timeout_source,
});
},
},
};
</script>
<template>
<div v-if="shouldRenderBlock" class="block">
<detail-row v-if="job.duration" :value="duration" title="Duration" />
<detail-row
v-if="job.finished_at"
:value="finishedAt"
data-testid="job-finished"
title="Finished"
/>
<detail-row v-if="job.erased_at" :value="erasedAt" title="Erased" />
<detail-row v-if="job.queued" :value="queued" title="Queued" />
<detail-row
v-if="hasTimeout"
:help-url="runnerHelpUrl"
:value="timeout"
data-testid="job-timeout"
title="Timeout"
/>
<detail-row v-if="job.runner" :value="runnerId" title="Runner" />
<detail-row v-if="job.coverage" :value="coverage" title="Coverage" />
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
<span class="font-weight-bold">{{ __('Tags:') }}</span>
<span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{ tag }}</span>
</p>
</div>
</template>

View File

@ -4,7 +4,7 @@ import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at);
export const hasUnmetPrerequisitesFailure = state =>
state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites';
state?.job?.failure_reason === 'unmet_prerequisites';
export const shouldRenderCalloutMessage = state =>
!isEmpty(state.job.status) && !isEmpty(state.job.callout_message);

View File

@ -3,5 +3,5 @@ import initSearchApp from '~/search';
document.addEventListener('DOMContentLoaded', () => {
initSearchApp();
return new Search();
return new Search(); // Deprecated Dropdown (Projects)
});

View File

@ -5,48 +5,22 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
import Api from '~/api';
import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import { visitUrl } from '~/lib/utils/url_utility';
import { visitUrl, queryToObject } from '~/lib/utils/url_utility';
import refreshCounts from './refresh_counts';
export default class Search {
constructor() {
setHighlightClass();
const $groupDropdown = $('.js-search-group-dropdown');
setHighlightClass(); // Code Highlighting
const $projectDropdown = $('.js-search-project-dropdown');
this.searchInput = '.js-search-input';
this.searchClear = '.js-search-clear';
this.groupId = $groupDropdown.data('groupId');
const query = queryToObject(window.location.search);
this.groupId = query?.group_id;
this.eventListeners();
refreshCounts();
initDeprecatedJQueryDropdown($groupDropdown, {
selectable: true,
filterable: true,
filterRemote: true,
fieldName: 'group_id',
search: {
fields: ['full_name'],
},
data(term, callback) {
return Api.groups(term, {}, data => {
data.unshift({
full_name: __('Any'),
});
data.splice(1, 0, { type: 'divider' });
return callback(data);
});
},
id(obj) {
return obj.id;
},
text(obj) {
return obj.full_name;
},
clicked: () => Search.submitSearch(),
});
initDeprecatedJQueryDropdown($projectDropdown, {
selectable: true,
filterable: true,

View File

@ -0,0 +1,124 @@
<script>
import {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
GlIcon,
GlSkeletonLoader,
GlTooltipDirective,
} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { ANY_GROUP, GROUP_QUERY_PARAM, PROJECT_QUERY_PARAM } from '../constants';
export default {
name: 'GroupFilter',
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
GlIcon,
GlSkeletonLoader,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
initialGroup: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
groupSearch: '',
};
},
computed: {
...mapState(['groups', 'fetchingGroups']),
selectedGroup: {
get() {
return isEmpty(this.initialGroup) ? ANY_GROUP : this.initialGroup;
},
set(group) {
visitUrl(setUrlParams({ [GROUP_QUERY_PARAM]: group.id, [PROJECT_QUERY_PARAM]: null }));
},
},
},
methods: {
...mapActions(['fetchGroups']),
isGroupSelected(group) {
return group.id === this.selectedGroup.id;
},
handleGroupChange(group) {
this.selectedGroup = group;
},
},
ANY_GROUP,
};
</script>
<template>
<gl-dropdown
ref="groupFilter"
class="gl-w-full"
menu-class="gl-w-full!"
toggle-class="gl-text-truncate gl-reset-line-height!"
:header-text="__('Filter results by group')"
@show="fetchGroups(groupSearch)"
>
<template #button-content>
<span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate">
{{ selectedGroup.name }}
</span>
<gl-loading-icon v-if="fetchingGroups" inline class="mr-2" />
<gl-icon
v-if="!isGroupSelected($options.ANY_GROUP)"
v-gl-tooltip
name="clear"
:title="__('Clear')"
class="gl-text-gray-200! gl-hover-text-blue-800!"
@click.stop="handleGroupChange($options.ANY_GROUP)"
/>
<gl-icon name="chevron-down" />
</template>
<div class="gl-sticky gl-top-0 gl-z-index-1 gl-bg-white">
<gl-search-box-by-type
v-model="groupSearch"
class="m-2"
:debounce="500"
@input="fetchGroups"
/>
<gl-dropdown-item
class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2"
:is-check-item="true"
:is-checked="isGroupSelected($options.ANY_GROUP)"
@click="handleGroupChange($options.ANY_GROUP)"
>
{{ $options.ANY_GROUP.name }}
</gl-dropdown-item>
</div>
<div v-if="!fetchingGroups">
<gl-dropdown-item
v-for="group in groups"
:key="group.id"
:is-check-item="true"
:is-checked="isGroupSelected(group)"
@click="handleGroupChange(group)"
>
{{ group.full_name }}
</gl-dropdown-item>
</div>
<div v-if="fetchingGroups" class="mx-3 mt-2">
<gl-skeleton-loader :height="100">
<rect y="0" width="90%" height="20" rx="4" />
<rect y="40" width="70%" height="20" rx="4" />
<rect y="80" width="80%" height="20" rx="4" />
</gl-skeleton-loader>
</div>
</gl-dropdown>
</template>

View File

@ -0,0 +1,10 @@
import { __ } from '~/locale';
export const ANY_GROUP = Object.freeze({
id: null,
name: __('Any'),
});
export const GROUP_QUERY_PARAM = 'group_id';
export const PROJECT_QUERY_PARAM = 'project_id';

View File

@ -0,0 +1,28 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import GroupFilter from './components/group_filter.vue';
Vue.use(Translate);
export default store => {
let initialGroup;
const el = document.getElementById('js-search-group-dropdown');
const { initialGroupData } = el.dataset;
initialGroup = JSON.parse(initialGroupData);
initialGroup = convertObjectPropsToCamelCase(initialGroup, { deep: true });
return new Vue({
el,
store,
render(createElement) {
return createElement(GroupFilter, {
props: {
initialGroup,
},
});
},
});
};

View File

@ -1,9 +1,11 @@
import { queryToObject } from '~/lib/utils/url_utility';
import createStore from './store';
import initDropdownFilters from './dropdown_filter';
import initGroupFilter from './group_filter';
export default () => {
const store = createStore({ query: queryToObject(window.location.search) });
initDropdownFilters(store);
initGroupFilter(store);
};

View File

@ -0,0 +1,16 @@
import Api from '~/api';
import createFlash from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
export const fetchGroups = ({ commit }, search) => {
commit(types.REQUEST_GROUPS);
Api.groups(search)
.then(data => {
commit(types.RECEIVE_GROUPS_SUCCESS, data);
})
.catch(() => {
createFlash({ message: __('There was a problem fetching groups.') });
commit(types.RECEIVE_GROUPS_ERROR);
});
};

View File

@ -1,10 +1,14 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export const getStoreConfig = ({ query }) => ({
actions,
mutations,
state: createState({ query }),
});

View File

@ -0,0 +1,3 @@
export const REQUEST_GROUPS = 'REQUEST_GROUPS';
export const RECEIVE_GROUPS_SUCCESS = 'RECEIVE_GROUPS_SUCCESS';
export const RECEIVE_GROUPS_ERROR = 'RECEIVE_GROUPS_ERROR';

View File

@ -0,0 +1,15 @@
import * as types from './mutation_types';
export default {
[types.REQUEST_GROUPS](state) {
state.fetchingGroups = true;
},
[types.RECEIVE_GROUPS_SUCCESS](state, data) {
state.fetchingGroups = false;
state.groups = data;
},
[types.RECEIVE_GROUPS_ERROR](state) {
state.fetchingGroups = false;
state.groups = [];
},
};

View File

@ -1,4 +1,6 @@
const createState = ({ query }) => ({
query,
groups: [],
fetchingGroups: false,
});
export default createState;

View File

@ -10,6 +10,7 @@ import { DEFAULT_IMAGE_UPLOAD_PATH } from '../constants';
import imageRepository from '../image_repository';
import formatter from '../services/formatter';
import templater from '../services/templater';
import renderImage from '../services/renderers/render_image';
export default {
components: {
@ -41,6 +42,10 @@ export default {
type: Array,
required: true,
},
project: {
type: String,
required: true,
},
imageRoot: {
type: String,
required: false,
@ -72,6 +77,12 @@ export default {
isWysiwygMode() {
return this.editorMode === EDITOR_TYPES.wysiwyg;
},
customRenderers() {
const imageRenderer = renderImage.build(this.mounts, this.project);
return {
image: [imageRenderer],
};
},
},
created() {
this.refreshEditHelpers();
@ -140,6 +151,7 @@ export default {
:content="editableContent"
:initial-edit-type="editorMode"
:image-root="imageRoot"
:options="{ customRenderers }"
class="mb-9 pb-6 h-100"
@modeChange="onModeChange"
@input="onInputChange"

View File

@ -139,6 +139,7 @@ export default {
:saving-changes="isSavingChanges"
:return-url="appData.returnUrl"
:mounts="appData.mounts"
:project="appData.project"
@submit="onPrepareSubmit"
/>
<edit-meta-modal

View File

@ -0,0 +1,30 @@
const canRender = ({ type }) => type === 'image';
// NOTE: the `metadata` is not used yet, but will be used in a follow-up iteration
// To be removed with the next iteration of https://gitlab.com/gitlab-org/gitlab/-/issues/218531
// eslint-disable-next-line no-unused-vars
let metadata;
const render = (node, { skipChildren }) => {
skipChildren();
// To be removed with the next iteration of https://gitlab.com/gitlab-org/gitlab/-/issues/218531
// TODO resolve relative paths
return {
type: 'openTag',
tagName: 'img',
selfClose: true,
attributes: {
src: node.destination,
alt: node.firstChild.literal,
},
};
};
const build = (mounts, project) => {
metadata = { mounts, project };
return { canRender, render };
};
export default { build };

View File

@ -270,7 +270,8 @@ input[type='checkbox']:hover {
width: 100%;
}
.dropdown-menu-toggle {
.dropdown-menu-toggle,
.gl-new-dropdown {
@include media-breakpoint-up(lg) {
width: 240px;
}

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class UserGroupsCounter
def initialize(user_ids)
@user_ids = user_ids
end
def execute
Namespace.unscoped do
Namespace.from_union([
groups,
project_groups
]).group(:user_id).count # rubocop: disable CodeReuse/ActiveRecord
end
end
private
attr_reader :user_ids
def groups
Group.for_authorized_group_members(user_ids)
.select('namespaces.*, members.user_id as user_id')
end
def project_groups
Group.for_authorized_project_members(user_ids)
.select('namespaces.*, project_authorizations.user_id as user_id')
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module Mutations
module Todos
class Create < ::Mutations::Todos::Base
graphql_name 'TodoCreate'
authorize :create_todo
argument :target_id,
Types::GlobalIDType[Todoable],
required: true,
description: "The global ID of the to-do item's parent. Issues, merge requests, designs and epics are supported"
field :todo, Types::TodoType,
null: true,
description: 'The to-do created'
def resolve(target_id:)
id = ::Types::GlobalIDType[Todoable].coerce_isolated_input(target_id)
target = authorized_find!(id)
todo = TodoService.new.mark_todo(target, current_user)&.first
errors = errors_on_object(todo) if todo
{
todo: todo,
errors: errors
}
end
private
def find_object(id)
GitlabSchema.find_by_gid(id)
end
end
end
end

View File

@ -23,7 +23,6 @@ module Resolvers
# The namespace could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` or the `full_path` of the namespace
# to query for projects, so make sure it's loaded and not `nil` before continuing.
namespace = object.respond_to?(:sync) ? object.sync : object
return Project.none if namespace.nil?
query = include_subgroups ? namespace.all_projects.with_route : namespace.projects.with_route
@ -41,6 +40,14 @@ module Resolvers
complexity = super
complexity + 10
end
private
def namespace
strong_memoize(:namespace) do
object.respond_to?(:sync) ? object.sync : object
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Resolvers
module Users
class GroupCountResolver < BaseResolver
alias_method :user, :object
def resolve(**args)
return unless can_read_group_count?
BatchLoader::GraphQL.for(user.id).batch do |user_ids, loader|
results = UserGroupsCounter.new(user_ids).execute
results.each do |user_id, count|
loader.call(user_id, count)
end
end
end
def can_read_group_count?
current_user&.can?(:read_group_count, user)
end
end
end
end

View File

@ -63,6 +63,7 @@ module Types
mount_mutation Mutations::Terraform::State::Delete
mount_mutation Mutations::Terraform::State::Lock
mount_mutation Mutations::Terraform::State::Unlock
mount_mutation Mutations::Todos::Create
mount_mutation Mutations::Todos::MarkDone
mount_mutation Mutations::Todos::Restore
mount_mutation Mutations::Todos::MarkAllDone

View File

@ -7,6 +7,7 @@ module Types
description 'Values for sorting projects'
value 'SIMILARITY', 'Most similar to the search query', value: :similarity
value 'STORAGE', 'Sort by storage size', value: :storage
end
end
end

View File

@ -32,6 +32,10 @@ module Types
field :group_memberships, Types::GroupMemberType.connection_type, null: true,
description: 'Group memberships of the user',
method: :group_members
field :group_count, GraphQL::INT_TYPE, null: true,
resolver: Resolvers::Users::GroupCountResolver,
description: 'Group count for the user',
feature_flag: :user_group_counts
field :status, Types::UserStatusType, null: true,
description: 'User status'
field :project_memberships, Types::ProjectMemberType.connection_type, null: true,

View File

@ -80,7 +80,7 @@ module Ci
end
def fog_store_class
if Feature.enabled?(:ci_trace_new_fog_store)
if Feature.enabled?(:ci_trace_new_fog_store, default_enabled: true)
Ci::BuildTraceChunks::Fog
else
Ci::BuildTraceChunks::LegacyFog

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
# == Todoable concern
#
# Specify object types that supports todos.
#
# Used by Issue, MergeRequest, Design and Epic.
#
module Todoable
end

View File

@ -10,6 +10,7 @@ module DesignManagement
include Mentionable
include WhereComposite
include RelativePositioning
include Todoable
belongs_to :project, inverse_of: :designs
belongs_to :issue

View File

@ -98,6 +98,17 @@ class Group < Namespace
scope :by_id, ->(groups) { where(id: groups) }
scope :for_authorized_group_members, -> (user_ids) do
joins(:group_members)
.where("members.user_id IN (?)", user_ids)
.where("access_level >= ?", Gitlab::Access::GUEST)
end
scope :for_authorized_project_members, -> (user_ids) do
joins(projects: :project_authorizations)
.where("project_authorizations.user_id IN (?)", user_ids)
end
class << self
def sort_by_attribute(method)
if method == 'storage_size_desc'

View File

@ -21,6 +21,7 @@ class Issue < ApplicationRecord
include IdInOrdered
include Presentable
include IssueAvailableFeatures
include Todoable
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze

View File

@ -22,6 +22,7 @@ class MergeRequest < ApplicationRecord
include StateEventable
include ApprovableBase
include IdInOrdered
include Todoable
extend ::Gitlab::Utils::Override

View File

@ -35,6 +35,10 @@ class IssuePolicy < IssuablePolicy
rule { ~can?(:read_design) }.policy do
prevent :move_design
end
rule { ~anonymous & can?(:read_issue) }.policy do
enable :create_todo
end
end
IssuePolicy.prepend_if_ee('EE::IssuePolicy')

View File

@ -14,6 +14,10 @@ class MergeRequestPolicy < IssuablePolicy
rule { can?(:update_merge_request) }.policy do
enable :approve_merge_request
end
rule { ~anonymous & can?(:read_merge_request) }.policy do
enable :create_todo
end
end
MergeRequestPolicy.prepend_if_ee('EE::MergeRequestPolicy')

View File

@ -21,6 +21,7 @@ class UserPolicy < BasePolicy
enable :update_user
enable :update_user_status
enable :read_user_personal_access_tokens
enable :read_group_count
end
rule { default }.enable :read_user_profile

View File

@ -33,15 +33,17 @@ class BulkCreateIntegrationService
klass.insert_all(items_to_insert, returning: [:id])
end
# rubocop: disable CodeReuse/ActiveRecord
def run_callbacks(batch)
if integration.external_issue_tracker?
batch.update_all(has_external_issue_tracker: true)
Project.where(id: batch.select(:id)).update_all(has_external_issue_tracker: true)
end
if integration.external_wiki?
batch.update_all(has_external_wiki: true)
Project.where(id: batch.select(:id)).update_all(has_external_wiki: true)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def service_hash
if integration.template?

View File

@ -1,6 +1,7 @@
- add_to_breadcrumbs _("Groups"), admin_groups_path
- breadcrumb_title @group.name
- page_title @group.name, _("Groups")
- current_user_is_group_owner = @group && @group.has_owner?(current_user)
.js-remove-member-modal
%h3.page-title
@ -116,7 +117,7 @@
= select_tag :access_level, options_for_select(@group.access_level_roles), class: "project-access-select select2"
%hr
= button_tag _('Add users to group'), class: "gl-button btn btn-success"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
= render 'shared/members/requests', membership_source: @group, group: @group, requesters: @requesters, force_mobile_view: true
.card
.card-header
@ -127,6 +128,11 @@
= sprite_icon('pencil-square', css_class: 'gl-icon')
= _('Manage access')
%ul.content-list.group-users-list.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false }
= render partial: 'shared/members/member',
collection: @members, as: :member,
locals: { membership_source: @group,
group: @group,
show_controls: false,
current_user_is_group_owner: current_user_is_group_owner }
.card-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab'

View File

@ -2,6 +2,7 @@
- breadcrumb_title @project.full_name
- page_title @project.full_name, _("Projects")
- @content_class = "admin-projects"
- current_user_is_group_owner = @group && @group.has_owner?(current_user)
.js-remove-member-modal
%h3.page-title
@ -183,11 +184,16 @@
= sprite_icon('pencil-square', css_class: 'gl-icon')
= _('Manage access')
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false }
= render partial: 'shared/members/member',
collection: @group_members, as: :member,
locals: { membership_source: @project,
group: @group,
show_controls: false,
current_user_is_group_owner: current_user_is_group_owner }
.card-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
= render 'shared/members/requests', membership_source: @project, requesters: @requesters, force_mobile_view: true
= render 'shared/members/requests', membership_source: @project, group: @group, requesters: @requesters, force_mobile_view: true
.card
.card-header
@ -199,6 +205,11 @@
= sprite_icon('pencil-square', css_class: 'gl-icon')
= _('Manage access')
%ul.content-list.project_members.members-list
= render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false }
= render partial: 'shared/members/member',
collection: @project_members, as: :member,
locals: { membership_source: @project,
group: @group,
show_controls: false,
current_user_is_group_owner: current_user_is_group_owner }
.card-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'

View File

@ -4,6 +4,7 @@
- show_access_requests = can_manage_members && @requesters.exists?
- invited_active = params[:search_invited].present? || params[:invited_members_page].present?
- vue_members_list_enabled = Feature.enabled?(:vue_group_members_list, @group)
- current_user_is_group_owner = @group && @group.has_owner?(current_user)
- form_item_label_css_class = 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center'
@ -71,7 +72,11 @@
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
- else
%ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
= render partial: 'shared/members/member', collection: @members, as: :member
= render partial: 'shared/members/member',
collection: @members, as: :member,
locals: { membership_source: @group,
group: @group,
current_user_is_group_owner: current_user_is_group_owner }
= paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil }
- if @group.shared_with_group_links.any?
#tab-groups.tab-pane
@ -97,7 +102,11 @@
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) }
- else
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @invited_members, as: :member
= render partial: 'shared/members/member',
collection: @invited_members, as: :member,
locals: { membership_source: @group,
group: @group,
current_user_is_group_owner: current_user_is_group_owner }
= paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab', params: { page: nil }
- if show_access_requests
#tab-access-requests.tab-pane
@ -109,4 +118,8 @@
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
- else
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @requesters, as: :member
= render partial: 'shared/members/member',
collection: @requesters, as: :member,
locals: { membership_source: @group,
group: @group,
current_user_is_group_owner: current_user_is_group_owner }

View File

@ -1,5 +1,7 @@
- project = local_assigns.fetch(:project)
- members = local_assigns.fetch(:members)
- group = local_assigns.fetch(:group)
- current_user_is_group_owner = group && group.has_owner?(current_user)
.card
.card-header.flex-project-members-panel
@ -15,4 +17,8 @@
= label_tag :sort_by, _('Sort by'), class: 'col-form-label label-bold px-2'
= render 'shared/members/sort_dropdown'
%ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
= render partial: 'shared/members/member', collection: members, as: :member
= render partial: 'shared/members/member',
collection: members, as: :member,
locals: { membership_source: project,
group: group,
current_user_is_group_owner: current_user_is_group_owner }

View File

@ -1,5 +1,6 @@
- page_title _("Members")
- can_admin_project_members = can?(current_user, :admin_project_member, @project)
- group = @project.group
.js-remove-member-modal
.row.gl-mt-3
@ -32,12 +33,12 @@
- elsif @project.allowed_to_share_with_group?
.invite-group= render 'shared/members/invite_group', access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, submit_url: project_group_links_path(@project), group_link_field: 'link_group_id', group_access_field: 'link_group_access'
= render 'shared/members/requests', membership_source: @project, requesters: @requesters
= render 'shared/members/requests', membership_source: @project, group: group, requesters: @requesters
.clearfix
%h5.member.existing-title
= _("Existing members and groups")
- if @group_links.any?
= render 'projects/project_members/groups', group_links: @group_links
= render 'projects/project_members/team', project: @project, members: @project_members
= render 'projects/project_members/team', project: @project, group: group, members: @project_members
= paginate @project_members, theme: "gitlab"

View File

@ -2,21 +2,10 @@
= hidden_field_tag :group_id, params[:group_id]
- if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id]
.dropdown.form-group.mb-lg-0.mx-lg-1{ data: { testid: "group-filter" } }
.dropdown.form-group.mb-lg-0.mx-lg-1.gl-p-0{ data: { testid: "group-filter" } }
%label.d-block{ for: "dashboard_search_group" }
= _("Group")
%button.dropdown-menu-toggle.gl-display-inline-flex.js-search-group-dropdown.gl-mt-0{ type: "button", id: "dashboard_search_group", data: { toggle: "dropdown", group_id: params[:group_id] } }
%span.dropdown-toggle-text.gl-flex-grow-1.str-truncated-100
= @group&.name || _("Any")
- if @group.present?
= link_to sprite_icon("clear"), url_for(safe_params.except(:project_id, :group_id)), class: 'search-clear js-search-clear has-tooltip', title: _('Clear')
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right
= dropdown_title(_("Filter results by group"))
= dropdown_filter(_("Search groups"))
= dropdown_content
= dropdown_loading
%input#js-search-group-dropdown.dropdown-menu-toggle{ value: "Loading...", data: { "initial-group-data": @group.to_json } }
.dropdown.form-group.mb-lg-0.mx-lg-1{ data: { testid: "project-filter" } }
%label.d-block{ for: "dashboard_search_project" }
= _("Project")

View File

@ -6,7 +6,7 @@
- if issuable_mr > 0
%li.issuable-mr.gl-display-none.gl-display-sm-block.has-tooltip{ title: _('Related merge requests') }
= image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged')
= sprite_icon('merge-request', css_class: "gl-vertical-align-middle")
= issuable_mr
- if upvotes > 0

View File

@ -2,6 +2,9 @@
- show_controls = local_assigns.fetch(:show_controls, true)
- force_mobile_view = local_assigns.fetch(:force_mobile_view, false)
- member = local_assigns.fetch(:member)
- current_user_is_group_owner = local_assigns.fetch(:current_user_is_group_owner, false)
- membership_source = local_assigns.fetch(:membership_source)
- group = local_assigns.fetch(:group)
- user = local_assigns.fetch(:user, member.user)
- source = member.source
- override = member.try(:override)
@ -25,13 +28,13 @@
= render 'shared/members/its_you_badge', user: user, current_user: current_user
= render_if_exists 'shared/members/ee/license_badge', user: user, group: @group
= render_if_exists 'shared/members/ee/license_badge', user: user, group: group, current_user_is_group_owner: current_user_is_group_owner
= render 'shared/members/blocked_badge', user: user
= render 'shared/members/two_factor_auth_badge', user: user
- if source.instance_of?(Group) && source != @group
- if source.instance_of?(Group) && source != membership_source
&middot;
= link_to source.full_name, source, class: "gl-display-inline-block inline-link"
@ -57,10 +60,9 @@
= link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at)
- if show_roles
- current_resource = @project || @group
.controls.member-controls.align-items-center
= render_if_exists 'shared/members/ee/ldap_tag', can_override: member.can_override?
- if show_controls && member.source == current_resource
- if show_controls && member.source == membership_source
- if member.can_resend_invite?
= link_to sprite_icon('paper-airplane'), polymorphic_path([:resend_invite, member]),
@ -88,7 +90,7 @@
class: ("is-active" if member.access_level == role_id),
data: { id: role_id, el_id: dom_id(member), qa_selector: "#{role.downcase}_access_level_link" }
= render_if_exists 'shared/members/ee/revert_ldap_group_sync_option',
group: @group,
group: group,
member: member,
can_override: member.can_override?
.clearable-input.member-form-control{ class: [("d-sm-inline-block" unless force_mobile_view)] }
@ -125,8 +127,8 @@
= _("Delete")
- unless force_mobile_view
= sprite_icon('remove', css_class: 'd-none d-sm-block gl-icon')
= render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :edit, can_override: member.can_override?
= render_if_exists 'shared/members/ee/override_member_buttons', group: group, member: member, user: user, action: :edit, can_override: member.can_override?
- else
%span.member-access-text.user-access-role= member.human_access
= render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :confirm, can_override: member.can_override?
= render_if_exists 'shared/members/ee/override_member_buttons', group: group, member: member, user: user, action: :confirm, can_override: member.can_override?

View File

@ -1,6 +1,8 @@
- membership_source = local_assigns.fetch(:membership_source)
- requesters = local_assigns.fetch(:requesters)
- force_mobile_view = local_assigns.fetch(:force_mobile_view, false)
- group = local_assigns.fetch(:group)
- current_user_is_group_owner = group && group.has_owner?(current_user)
- return if requesters.empty?
@ -10,4 +12,9 @@
%strong= membership_source.name
%span.badge.badge-pill= requesters.size
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: requesters, as: :member, locals: { force_mobile_view: force_mobile_view }
= render partial: 'shared/members/member',
collection: requesters, as: :member,
locals: { membership_source: membership_source,
group: group,
force_mobile_view: force_mobile_view,
current_user_is_group_owner: current_user_is_group_owner }

View File

@ -0,0 +1,5 @@
---
title: Updated list view MR icon
merge_request: 46059
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Ensure that copy to clipboard button is visible
merge_request: 46466
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Allow to create todo on GraphQL
merge_request: 46029
author:
type: added

View File

@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46209
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273405
type: development
group: group::testing
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,7 @@
---
name: user_group_counts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44069/
rollout_issue_url:
type: development
group: group::compliance
default_enabled: false

View File

@ -462,6 +462,34 @@ are stored.
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
Alternatively, if you have existing Pages deployed you can follow
the below steps to do a no downtime transfer to a new storage location.
1. Pause Pages deployments by setting the following in `/etc/gitlab/gitlab.rb`:
```ruby
sidekiq['experimental_queue_selector'] = true
sidekiq['queue_groups'] = [
"feature_category!=pages"
]
```
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
1. `rsync` contents from the current storage location to the new storage location: `sudo rsync -avzh --progress /var/opt/gitlab/gitlab-rails/shared/pages/ /mnt/storage/pages`
1. Set the new storage location in `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['pages_path'] = "/mnt/storage/pages"
```
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
1. Verify Pages are still being served up as expected.
1. Unpause Pages deployments by removing from `/etc/gitlab/gitlab.rb` the `sidekiq` setting set above.
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
1. Trigger a new Pages deployment and verify it's working as expected.
1. Remove the old Pages storage location: `sudo rm -rf /var/opt/gitlab/gitlab-rails/shared/pages`
1. Verify Pages are still being served up as expected.
## Configure listener for reverse proxy requests
Follow the steps below to configure the proxy listener of GitLab Pages. [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/2533) in

View File

@ -12992,6 +12992,7 @@ type Mutation {
terraformStateDelete(input: TerraformStateDeleteInput!): TerraformStateDeletePayload
terraformStateLock(input: TerraformStateLockInput!): TerraformStateLockPayload
terraformStateUnlock(input: TerraformStateUnlockInput!): TerraformStateUnlockPayload
todoCreate(input: TodoCreateInput!): TodoCreatePayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
todoRestore(input: TodoRestoreInput!): TodoRestorePayload
todoRestoreMany(input: TodoRestoreManyInput!): TodoRestoreManyPayload
@ -13276,6 +13277,11 @@ enum NamespaceProjectSort {
Most similar to the search query
"""
SIMILARITY
"""
Sort by storage size
"""
STORAGE
}
input NegatedBoardIssueInput {
@ -20172,6 +20178,41 @@ type TodoConnection {
pageInfo: PageInfo!
}
"""
Autogenerated input type of TodoCreate
"""
input TodoCreateInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global ID of the to-do item's parent. Issues, merge requests, designs and epics are supported
"""
targetId: TodoableID!
}
"""
Autogenerated return type of TodoCreate
"""
type TodoCreatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The to-do created
"""
todo: Todo
}
"""
An edge in a connection.
"""
@ -20339,6 +20380,11 @@ enum TodoTargetEnum {
MERGEREQUEST
}
"""
Identifier of Todoable
"""
scalar TodoableID
"""
Autogenerated input type of TodosMarkAllDone
"""
@ -21541,6 +21587,11 @@ type User {
"""
email: String
"""
Group count for the user. Available only when feature flag `user_group_counts` is enabled
"""
groupCount: Int
"""
Group memberships of the user
"""

View File

@ -37870,6 +37870,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "todoCreate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "TodoCreateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "TodoCreatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "todoMarkDone",
"description": null,
@ -39133,6 +39160,12 @@
"description": "Most similar to the search query",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "STORAGE",
"description": "Sort by storage size",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@ -58681,6 +58714,108 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "TodoCreateInput",
"description": "Autogenerated input type of TodoCreate",
"fields": null,
"inputFields": [
{
"name": "targetId",
"description": "The global ID of the to-do item's parent. Issues, merge requests, designs and epics are supported",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "TodoableID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TodoCreatePayload",
"description": "Autogenerated return type of TodoCreate",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "todo",
"description": "The to-do created",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Todo",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TodoEdge",
@ -59166,6 +59301,16 @@
],
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "TodoableID",
"description": "Identifier of Todoable",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "TodosMarkAllDoneInput",
@ -62400,6 +62545,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "groupCount",
"description": "Group count for the user. Available only when feature flag `user_group_counts` is enabled",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "groupMemberships",
"description": "Group memberships of the user",

View File

@ -2838,6 +2838,16 @@ Representing a todo entry.
| `state` | TodoStateEnum! | State of the todo |
| `targetType` | TodoTargetEnum! | Target type of the todo |
### TodoCreatePayload
Autogenerated return type of TodoCreate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `todo` | Todo | The to-do created |
### TodoMarkDonePayload
Autogenerated return type of TodoMarkDone.
@ -3041,6 +3051,7 @@ Autogenerated return type of UpdateSnippet.
| ----- | ---- | ----------- |
| `avatarUrl` | String | URL of the user's avatar |
| `email` | String | User email |
| `groupCount` | Int | Group count for the user. Available only when feature flag `user_group_counts` is enabled |
| `id` | ID! | ID of the user |
| `name` | String! | Human-readable name of the user |
| `state` | UserState! | State of the user |
@ -3770,6 +3781,7 @@ Values for sorting projects.
| Value | Description |
| ----- | ----------- |
| `SIMILARITY` | Most similar to the search query |
| `STORAGE` | Sort by storage size |
### PackageTypeEnum

View File

@ -1,25 +1,23 @@
---
stage: none
group: unassigned
type: reference, howto
stage: Manage
group: Access
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# SCIM API **(SILVER ONLY)**
# SCIM API (SYSTEM ONLY) **(SILVER ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in [GitLab.com Silver](https://about.gitlab.com/pricing/) 11.10.
The SCIM API implements the [RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
The SCIM API implements the [RFC7644 protocol](https://tools.ietf.org/html/rfc7644). As this API is for
**system** use for SCIM provider integration, it is subject to change without notice.
CAUTION: **Caution:**
This API is for internal system use for connecting with a SCIM provider. While it can be used directly, it is subject to change without notice.
NOTE: **Note:**
[Group SSO](../user/group/saml_sso/index.md) must be enabled for the group. For more information, see [SCIM setup documentation](../user/group/saml_sso/scim_setup.md#requirements).
To use this API, [Group SSO](../user/group/saml_sso/index.md) must be enabled for the group.
This API is only in use where [SCIM for Group SSO](../user/group/saml_sso/scim_setup.md) is enabled. It's a prerequisite to the creation of SCIM identities.
## Get a list of SAML users
NOTE: **Note:**
This endpoint is used as part of the SCIM syncing mechanism and it only returns
This endpoint is used as part of the SCIM syncing mechanism. It only returns
a single user based on a unique ID which should match the `extern_uid` of the user.
```plaintext

View File

@ -282,10 +282,10 @@ When running your project pipeline at this point:
on the related JSON object's content. The deployment job finishes whenever the deployment to EC2
is done or has failed.
#### Deploy Amazon EKS
### Deploy to Amazon EKS
- [How to deploy your application to a GitLab-managed Amazon EKS cluster with Auto DevOps](https://about.gitlab.com/blog/2020/05/05/deploying-application-eks/)
#### Deploy to Google Cloud
## Deploy to Google Cloud
- [Deploying with GitLab on Google Cloud](https://about.gitlab.com/solutions/google-cloud-platform/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -45,6 +45,61 @@ To protect an environment:
The protected environment will now appear in the list of protected environments.
### Use the API to protect an environment
Alternatively, you can use the API to protect an environment:
1. Use a project with a CI that creates an environment. For example:
```yaml
stages:
- test
- deploy
test:
stage: test
script:
- 'echo "Testing Application: ${CI_PROJECT_NAME}"'
production:
stage: deploy
when: manual
script:
- 'echo "Deploying to ${CI_ENVIRONMENT_NAME}"'
environment:
name: ${CI_JOB_NAME}
```
1. Use the UI to [create a new group](../../user/group/index.md#create-a-new-group).
For example, this group is called `protected-access-group` and has the group ID `9899826`. Note
that the rest of the examples in these steps use this group.
![Group Access](img/protected_access_group_v13_6.png)
1. Use the API to add a user to the group as a reporter:
```shell
$ curl --request POST --header "PRIVATE-TOKEN: xxxxxxxxxxxx" --data "user_id=3222377&access_level=20" "https://gitlab.com/api/v4/groups/9899826/members"
{"id":3222377,"name":"Sean Carroll","username":"sfcarroll","state":"active","avatar_url":"https://assets.gitlab-static.net/uploads/-/system/user/avatar/3222377/avatar.png","web_url":"https://gitlab.com/sfcarroll","access_level":20,"created_at":"2020-10-26T17:37:50.309Z","expires_at":null}
```
1. Use the API to add the group to the project as a reporter:
```shell
$ curl --request POST --header "PRIVATE-TOKEN: xxxxxxxxxxxx" --request POST "https://gitlab.com/api/v4/projects/22034114/share?group_id=9899826&group_access=20"
{"id":1233335,"project_id":22034114,"group_id":9899826,"group_access":20,"expires_at":null}
```
1. Use the API to add the group with protected environment access:
```shell
curl --header 'Content-Type: application/json' --request POST --data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}]}' --header "PRIVATE-TOKEN: xxxxxxxxxxx" "https://gitlab.com/api/v4/projects/22034114/protected_environments"
```
The group now has access and can be seen in the UI.
## Environment access by group membership
A user may be granted access to protected environments as part of

View File

@ -128,6 +128,12 @@ This helps you avoid having to add the `only:` rule to all of your jobs to make
them always run. You can use this format to set up a Review App, helping to
save resources.
### Using SAST, DAST, and other Secure Templates with Pipelines for Merge Requests
To use [Secure templates](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Security)
with pipelines for merge requests, you may need to apply a `rules: if: merge_request_event` for the
Secure scans to run in the same pipeline as the commit.
#### Excluding certain branches
Pipelines for merge requests require special treatment when

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Adding a new Service Component to GitLab
The GitLab product is made up of several service components that run as independent system processes in communication with each other. These services can be run on the same instance, or spread across different instances. A list of the existing components can be found in the [GitLab architecture overview](architecture.md).

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# API style guide
This style guide recommends best practices for API development.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Application limits development
This document provides a development guide for contributors to add application

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Application secrets
This page is a development guide for application secrets.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Approval Rules **(STARTER)**
This document explains the backend design and flow of all related functionality

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab architecture overview
## Software delivery

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Generating chaos in a test GitLab instance
As [Werner Vogels](https://twitter.com/Werner), the CTO at Amazon Web Services, famously put it, **Everything fails, all the time**.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Code comments
Whenever you add comment to the code that is expected to be addressed at any time

View File

@ -1,3 +1,9 @@
---
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Code Intelligence
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/1576) in GitLab 13.1.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Creating enums
When creating a new enum, it should use the database type `SMALLINT`.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Danger bot
The GitLab CI/CD pipeline includes a `danger-review` job that uses [Danger](https://github.com/danger/danger)

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Setting Multiple Values
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32921) in GitLab 13.5.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Delete existing migrations
When removing existing migrations from the GitLab project, you have to take into account

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Deprecation guidelines
This page includes information about how and when to remove or make breaking

View File

@ -1,4 +1,7 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
description: "Learn how GitLab docs' global navigation works and how to add new items."
---

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab Docs monthly release process
When a new GitLab version is released on the 22nd, we need to create the respective

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Guidelines for implementing Enterprise Edition features
- **Write the code and the tests.**: As with any code, EE features should have

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Dealing with email in development
## Ensuring compatibility with mailer Sidekiq jobs

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Experiment Guide
Experiments can be conducted by any GitLab team, most often the teams from the [Growth Sub-department](https://about.gitlab.com/handbook/engineering/development/growth/). Experiments are not tied to releases because they will primarily target GitLab.com.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Accessibility & Readability
## Resources

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Architecture
When you are developing a new feature that requires architectural design, or if

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Axios
We use [Axios](https://github.com/axios/axios) to communicate with the server in Vue applications and most new code.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Frontend dependencies
## Package manager

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Design Patterns
## Singletons

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Frontend Development Process
You can find more about the organization of the frontend team in the [handbook](https://about.gitlab.com/handbook/engineering/frontend/).

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# DropLab
A generic dropdown for all of your custom dropdown needs.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Ajax
`Ajax` is a droplab plugin that allows for retrieving and rendering list data from a server.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Filter
`Filter` is a plugin that allows for filtering data that has been added

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# InputSetter
`InputSetter` is a plugin that allows for updating DOM out of the scope of droplab when a list item is clicked.

View File

@ -0,0 +1,218 @@
# Editor Lite
## Background
**Editor Lite** is a technological product driving [Web Editor](../../user/project/repository/web_editor.md), [Snippets](../../user/snippets.md), [CI Linter](../../ci/lint.md), etc. Editor Lite is the driving technology for any single-file editing experience across the product.
Editor Lite is a thin wrapper around [the Monaco editor](https://microsoft.github.io/monaco-editor/index.html) that provides the necessary helpers and abstractions and extends Monaco using extensions.
## How to use Editor Lite
Editor Lite is framework-agnostic and can be used in any application, whether it's Rails or Vue. For the convenience of integration, we have [the dedicated `<editor-lite>` Vue component](#vue-component), but in general, the integration of Editor Lite is pretty straightforward:
1. Import Editor Lite:
```javascript
import EditorLite from '~/editor/editor_lite';
```
1. Initialize global editor for the view:
```javascript
const editor = new EditorLite({
// Editor Options.
// The list of all accepted options can be found at
// https://microsoft.github.io/monaco-editor/api/enums/monaco.editor.editoroption.html
});
```
1. Create an editor's instance:
```javascript
editor.createInstance({
// Editor Lite configuration options.
})
```
An instance of Editor Lite accepts the following configuration options:
| Option | Required? | Description |
| ---- | ---- | ---- |
| `el` | `true` | `HTML Node`: element on which to render the editor |
| `blobPath` | `false` | `String`: the name of a file to render in the editor. It is used to identify the correct syntax highlighter to use with that or another file type. Can accept wildcard as in `*.js` when the actual filename isn't known or doesn't play any role |
| `blobContent` | `false` | `String`: the initial content to be rendered in the editor |
| `extensions` | `false` | `Array`: extensions to use in this instance |
| `blobGlobalId` | `false` | `String`: auto-generated property.</br>**Note:** this prop might go away in the future. Do not pass `blobGlobalId` unless you know what you're doing.|
| [Editor Options](https://microsoft.github.io/monaco-editor/api/enums/monaco.editor.editoroption.html) | `false` | `Object(s)`: any prop outside of the list above is treated as an Editor Option for this particular instance. This way, one can override global Editor Options on the instance level. |
## API
The editor follows the same public API as [provided by Monaco editor](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandalonecodeeditor.html) with just a few additional functions on the instance level:
| Function | Arguments | Description
| ----- | ----- | ----- |
| `updateModelLanguage` | `path`: String | Updates the instance's syntax highlighting to follow the extension of the passed `path`. Available only on _instance_ level|
| `use` | Array of objects | Array of **extensions** to apply to the instance. Note: `use` accepts only the array of _objects_, which means that the extensions' ES6 modules should be fetched and resolved in your views/components before being passed to `use`. This prop is available on _instance_ (applies extension to this particular instance) and _global edtor_ (applies the same extension to all instances) levels. |
| Monaco Editor options | See [documentation](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandalonecodeeditor.html) | Default Monaco editor options |
## Tips
1. Editor's loading state.
Editor Lite comes with the loading state built-in, making spinners and loaders rarely needed in HTML. To benefit the built-in loading state, set the `data-editor-loading` property on the HTML element that is supposed to contain the editor. Editor Lite will show the loader automatically while it's bootstrapping.
![Editor Lite: loading state](img/editor_lite_loading.png)
1. Update syntax highlighting if the file name changes.
```javascript
// fileNameEl here is the HTML input element that contains the file name
fileNameEl.addEventListener('change', () => {
this.editor.updateModelLanguage(fileNameEl.value);
});
```
1. Get the editor's content.
We might set up listeners on the editor for every change but it rapidly can become an expensive operation. Instead , we can get editor's content when it's needed. For example on a form's submission:
```javascript
form.addEventListener('submit', () => {
my_content_variable = this.editor.getValue();
});
```
1. Performance
Even though Editor Lite itself is extremely slim, it still depends on Monaco editor. Monaco is not an easily tree-shakeable module. Hence, every time you add Editor Lite to a view, the JavaScript bundle's size significantly increases, affecting your view's loading performance. To avoid that, it is recommended to import the editor on demand on those views where it is not 100% certain that the editor will be used. Or if the editor is a secondary element of the view. Loading Editor Lite on demand is no different from loading any other module:
```javascript
someActionFunction() {
import(/* webpackChunkName: 'EditorLite' */ '~/editor/editor_lite').
then(({ default: EditorLite }) => {
const editor = new EditorLite();
...
});
...
}
```
## Extensions
Editor Lite has been built to provide a universal, extensible editing tool to the whole product, which would not depend on any particular group. Even though the Editor Lite's core is owned by [Create::Editor FE Team](https://about.gitlab.com/handbook/engineering/development/dev/create-editor-fe/), the main functional elements — extensions — can be owned by any group. Editor Lite extensions' main idea is that the core of the editor remains very slim and stable. At the same time, whatever new functionality is needed can be added as an extension to this core, without touching the core itself. It allows any group to build and own any new editing functionality without being afraid of it being broken or overridden with the Editor Lite changes.
Structurally, the complete implementation of Editor Lite could be presented as the following diagram:
```mermaid
graph TD;
B[Extension 1]---A[Editor Lite]
C[Extension 2]---A[Editor Lite]
D[Extension 3]---A[Editor Lite]
E[...]---A[Editor Lite]
F[Extension N]---A[Editor Lite]
A[Editor Lite]---Z[Monaco]
```
Technically, an extension is just an ES6 module that exports a JavaScript object:
```javascript
import { Position } from 'monaco-editor';
export default {
navigateFileStart() {
this.setPosition(new Position(1, 1));
},
};
```
Important things to note here:
- We can depend on other modules in our extensions. This organization helps keep the size of Editor Lite's core at bay by importing dependencies only when needed.
- `this` in extension's functions refers to the current Editor Lite instance. Using `this`, you get access to the complete instance's API, such as the `setPosition()` method in this particular case.
### Using an existing extension
Adding an extension to Editor Lite's instance is simple:
```javascript
import EditorLite from '~/editor/editor_lite';
import MyExtension from '~/my_extension';
const editor = new EditorLite().createInstance({
...
});
editor.use(MyExtension);
```
### Creating an extension
Let's create our first Editor Lite extension. As aforementioned, extensions are ES6 modules exporting the simple `Object` that is used to extend Editor Lite's functionality. As the most straightforward test, let's create an extension that extends Editor Lite with a new function that, when called, will output editor's content in `alert`.
`~/my_folder/my_fancy_extension.js:`
```javascript
export default {
throwContentAtMe() {
alert(this.getValue());
},
};
```
And that's it with our extension! Note that we're using `this` as a reference to the instance. And through it, we get access to the complete underlying [Monaco editor API](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandalonecodeeditor.html) like `getValue()` in this case.
Now let's use our extension:
`~/my_folder/component_bundle.js`:
```javascript
import EditorLite from '~/editor/editor_lite';
import MyFancyExtension from './my_fancy_extension';
const editor = new EditorLite().createInstance({
...
});
editor.use(MyFancyExtension);
...
someButton.addEventListener('click', () => {
editor.throwContentAtMe();
});
```
First of all, we import Editor Lite and our new extension. Then we create the editor and its instance. By default Editor Lite has no `throwContentAtMe` method. But the `editor.use(MyFancyExtension)` line brings that method to our instance. After that, we can use it any time we need it. In this case, we call it when some theoretical button has been clicked.
This script would result in an alert containing the editor's content when `someButton` is clicked.
![Editor Lite new extension's result](img/editor_lite_create_ext.png)
### Tips
1. Performance
Just like Editor Lite itself, any extension can be loaded on demand to not harm loading performance of the views:
```javascript
const EditorPromise = import(
/* webpackChunkName: 'EditorLite' */ '~/editor/editor_lite'
);
const MarkdownExtensionPromise = import('~/editor/editor_markdown_ext');
Promise.all([EditorPromise, MarkdownExtensionPromise])
.then(([{ default: EditorLite }, { default: MarkdownExtension }]) => {
const editor = new EditorLite().createInstance({
...
});
editor.use(MarkdownExtension);
});
```
1. Using multiple extensions
Just pass the array of extensions to your `use` method:
```javascript
editor.use([FileTemplateExtension, MyFancyExtension]);
```
## <a id="vue-component"></a>`<editor-lite>` Vue component
TBD

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Emojis
GitLab supports native Unicode emojis and falls back to image-based emojis selectively

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Frontend FAQ
## Rules of Frontend FAQ

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Icons and SVG Illustrations
We manage our own icon and illustration library in the [`gitlab-svgs`](https://gitlab.com/gitlab-org/gitlab-svgs)

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Frontend Development Guidelines
This document describes various guidelines to ensure consistency and quality

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Implementing keyboard shortcuts
We use [Mousetrap](https://craig.is/killing/mice) to implement keyboard

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Performance
## Best Practices

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Principles
These principles will ensure that your frontend contribution starts off in the right direction.

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Security
## Resources

View File

@ -1,3 +1,9 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# HTML style guide
## Buttons

Some files were not shown because too many files have changed in this diff Show More