Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-25 06:11:18 +00:00
parent 65de487500
commit fd5a9d4a57
30 changed files with 472 additions and 98 deletions

View file

@ -21,6 +21,8 @@ import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_ite
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_CREATING,
TRACKING_CATEGORY_SHOW,
TASK_TYPE_NAME,
WIDGET_TYPE_DESCRIPTION,
@ -466,7 +468,7 @@ export default {
this.openWorkItemDetailModal(el);
} catch (error) {
createFlash({
message: s__('WorkItem|Something went wrong when creating a work item. Please try again'),
message: sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemTypes.TASK),
error,
captureError: true,
});

View file

@ -90,9 +90,8 @@ export default {
</script>
<template>
<span>
<span ref="userAvatar">
<gl-avatar
ref="userAvatar"
:class="{
lazy: lazy,
[cssClasses]: true,
@ -108,7 +107,7 @@ export default {
tooltipText ||
$slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
"
:target="() => $refs.userAvatar.$el"
:target="() => $refs.userAvatar"
:placement="tooltipPlacement"
boundary="window"
>

View file

@ -39,14 +39,14 @@ export default {
:class="{ 'gl-cursor-text': disabled }"
aria-labelledby="item-title"
>
<div
<span
id="item-title"
ref="titleEl"
role="textbox"
:aria-label="__('Title')"
:data-placeholder="placeholder"
:contenteditable="!disabled"
class="gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-rounded-base"
class="gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-rounded-base gl-display-block"
:class="{ 'gl-hover-border-gray-200 gl-pseudo-placeholder': !disabled }"
@blur="handleBlur"
@keyup="handleInput"
@ -55,8 +55,7 @@ export default {
@keydown.meta.u.prevent
@keydown.ctrl.b.prevent
@keydown.meta.b.prevent
>{{ title }}</span
>
{{ title }}
</div>
</h2>
</template>

View file

@ -8,10 +8,14 @@ import {
} from '@gitlab/ui';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import {
sprintfWorkItem,
I18N_WORK_ITEM_DELETE,
I18N_WORK_ITEM_ARE_YOU_SURE_DELETE,
} from '../constants';
export default {
i18n: {
deleteTask: s__('WorkItem|Delete task'),
enableTaskConfidentiality: s__('WorkItem|Turn on confidentiality'),
disableTaskConfidentiality: s__('WorkItem|Turn off confidentiality'),
},
@ -31,6 +35,11 @@ export default {
required: false,
default: null,
},
workItemType: {
type: String,
required: false,
default: null,
},
canUpdate: {
type: Boolean,
required: false,
@ -53,6 +62,14 @@ export default {
},
},
emits: ['deleteWorkItem', 'toggleWorkItemConfidentiality'],
computed: {
i18n() {
return {
deleteWorkItem: sprintfWorkItem(I18N_WORK_ITEM_DELETE, this.workItemType),
areYouSureDelete: sprintfWorkItem(I18N_WORK_ITEM_ARE_YOU_SURE_DELETE, this.workItemType),
};
},
},
methods: {
handleToggleWorkItemConfidentiality() {
this.track('click_toggle_work_item_confidentiality');
@ -75,6 +92,7 @@ export default {
<div>
<gl-dropdown
icon="ellipsis_v"
data-testid="work-item-actions-dropdown"
text-sr-only
:text="__('More actions')"
category="tertiary"
@ -97,20 +115,18 @@ export default {
v-if="canDelete"
v-gl-modal="'work-item-confirm-delete'"
data-testid="delete-action"
>{{ $options.i18n.deleteTask }}</gl-dropdown-item
>{{ i18n.deleteWorkItem }}</gl-dropdown-item
>
</gl-dropdown>
<gl-modal
modal-id="work-item-confirm-delete"
:title="$options.i18n.deleteWorkItem"
:ok-title="$options.i18n.deleteWorkItem"
:title="i18n.deleteWorkItem"
:ok-title="i18n.deleteWorkItem"
ok-variant="danger"
@ok="handleDeleteWorkItem"
@hide="handleCancelDeleteWorkItem"
>
{{
s__('WorkItem|Are you sure you want to delete the task? This action cannot be reversed.')
}}
{{ i18n.areYouSureDelete }}
</gl-modal>
</div>
</template>

View file

@ -279,11 +279,12 @@ export default {
<work-item-actions
v-if="canUpdate || canDelete"
:work-item-id="workItem.id"
:work-item-type="workItemType"
:can-delete="canDelete"
:can-update="canUpdate"
:is-confidential="workItem.confidential"
:is-parent-confidential="parentWorkItemConfidentiality"
@deleteWorkItem="$emit('deleteWorkItem')"
@deleteWorkItem="$emit('deleteWorkItem', workItemType)"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="error = $event"
/>

View file

@ -2,7 +2,8 @@
import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
import {
i18n,
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
STATE_OPEN,
STATE_CLOSED,
STATE_EVENT_CLOSE,
@ -93,7 +94,9 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
this.$emit('error', i18n.updateError);
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
this.$emit('error', msg);
Sentry.captureException(error);
}

View file

@ -1,7 +1,11 @@
<script>
import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
TRACKING_CATEGORY_SHOW,
} from '../constants';
import { getUpdateWorkItemMutation } from './update_work_item';
import ItemTitle from './item_title.vue';
@ -78,7 +82,8 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
this.$emit('error', i18n.updateError);
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
this.$emit('error', msg);
Sentry.captureException(error);
}

View file

@ -3,7 +3,11 @@ import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { __ } from '~/locale';
import Tracking from '~/tracking';
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
TRACKING_CATEGORY_SHOW,
} from '../constants';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
/* eslint-disable @gitlab/require-i18n-strings */
@ -125,7 +129,8 @@ export default {
}
})
.catch((error) => {
this.$emit('error', i18n.updateError);
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
this.$emit('error', msg);
Sentry.captureException(error);
});
},

View file

@ -1,4 +1,5 @@
import { s__ } from '~/locale';
import { s__, sprintf } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export const STATE_OPEN = 'OPEN';
export const STATE_CLOSED = 'CLOSED';
@ -31,6 +32,30 @@ export const i18n = {
),
};
export const I18N_WORK_ITEM_ERROR_CREATING = s__(
'WorkItem|Something went wrong when creating %{workItemType}. Please try again.',
);
export const I18N_WORK_ITEM_ERROR_UPDATING = s__(
'WorkItem|Something went wrong while updating the %{workItemType}. Please try again.',
);
export const I18N_WORK_ITEM_ERROR_DELETING = s__(
'WorkItem|Something went wrong when deleting the %{workItemType}. Please try again.',
);
export const I18N_WORK_ITEM_DELETE = s__('WorkItem|Delete %{workItemType}');
export const I18N_WORK_ITEM_ARE_YOU_SURE_DELETE = s__(
'WorkItem|Are you sure you want to delete the %{workItemType}? This action cannot be reversed.',
);
export const I18N_WORK_ITEM_DELETED = s__('WorkItem|%{workItemType} deleted');
export const sprintfWorkItem = (msg, workItemTypeArg) => {
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
return capitalizeFirstCharacter(
sprintf(msg, {
workItemType: workItemType.toLocaleLowerCase(),
}),
);
};
export const WIDGET_ICONS = {
TASK: 'issue-type-task',
};

View file

@ -1,7 +1,9 @@
<script>
import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
import { s__ } from '~/locale';
import { getPreferredLocales, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import createWorkItemFromTaskMutation from '../graphql/create_work_item_from_task.mutation.graphql';
@ -10,7 +12,6 @@ import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.
import ItemTitle from '../components/item_title.vue';
export default {
createErrorText: s__('WorkItem|Something went wrong when creating a task. Please try again'),
fetchTypesErrorText: s__(
'WorkItem|Something went wrong when fetching work item types. Please try again',
),
@ -69,7 +70,7 @@ export default {
update(data) {
return data.workspace?.workItemTypes?.nodes.map((node) => ({
value: node.id,
text: node.name,
text: capitalizeFirstCharacter(node.name.toLocaleLowerCase(getPreferredLocales()[0])),
}));
},
error() {
@ -78,15 +79,19 @@ export default {
},
},
computed: {
dropdownButtonText() {
return this.selectedWorkItemType?.name || s__('WorkItem|Type');
},
formOptions() {
return [{ value: null, text: s__('WorkItem|Select type') }, ...this.workItemTypes];
},
isButtonDisabled() {
return this.title.trim().length === 0 || !this.selectedWorkItemType;
},
createErrorText() {
const workItemType = this.workItemTypes.find(
(item) => item.value === this.selectedWorkItemType,
)?.text;
return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemType);
},
},
methods: {
async createWorkItem() {
@ -128,7 +133,7 @@ export default {
} = response;
this.$router.push({ name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } });
} catch {
this.error = this.$options.createErrorText;
this.error = this.createErrorText;
}
},
async createWorkItemFromTask() {
@ -150,7 +155,7 @@ export default {
});
this.$emit('onCreate', data.workItemCreateFromTask.workItem.descriptionHtml);
} catch {
this.error = this.$options.createErrorText;
this.error = this.createErrorText;
}
},
handleTitleInput(title) {

View file

@ -3,9 +3,13 @@ import { GlAlert } from '@gitlab/ui';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import ZenMode from '~/zen_mode';
import WorkItemDetail from '../components/work_item_detail.vue';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_DELETING,
I18N_WORK_ITEM_DELETED,
} from '../constants';
import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
export default {
@ -34,7 +38,7 @@ export default {
this.ZenMode = new ZenMode();
},
methods: {
deleteWorkItem() {
deleteWorkItem(workItemType) {
this.$apollo
.mutate({
mutation: deleteWorkItemMutation,
@ -53,13 +57,12 @@ export default {
throw new Error(workItemDelete.errors[0]);
}
this.$toast.show(s__('WorkItem|Work item deleted'));
const msg = sprintfWorkItem(I18N_WORK_ITEM_DELETED, workItemType);
this.$toast.show(msg);
visitUrl(this.issuesListPath);
})
.catch((e) => {
this.error =
e.message ||
s__('WorkItem|Something went wrong when deleting the work item. Please try again.');
this.error = e.message || sprintfWorkItem(I18N_WORK_ITEM_ERROR_DELETING, workItemType);
});
},
},
@ -69,6 +72,6 @@ export default {
<template>
<div>
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert>
<work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem" />
<work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem($event)" />
</div>
</template>

View file

@ -25,8 +25,8 @@
= s_("Profiles|This email will be displayed on your public profile.")
.form-group.gl-form-group
- commit_email_link_url = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits', target: '_blank')
- commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url }
- commit_email_link_url = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits')
- commit_email_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: commit_email_link_url }
- commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more.%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe }
= form.label :commit_email, s_('Profiles|Commit email')
.gl-md-form-input-lg

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
class ScheduleBackfillClusterAgentsHasVulnerabilities < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
MIGRATION = 'BackfillClusterAgentsHasVulnerabilities'
DELAY_INTERVAL = 2.minutes
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'BackfillVulnerabilityReadsClusterAgent',
table_name: :vulnerability_reads,
column_name: :id,
job_arguments: []
)
queue_batched_background_migration(
MIGRATION,
:cluster_agents,
:id,
job_interval: DELAY_INTERVAL
)
end
def down
delete_batched_background_migration(MIGRATION, :cluster_agents, :id, [])
end
end

View file

@ -0,0 +1 @@
fd138239f6970b892fdb8190fb65b3364bb9ba5396100ba3d5d695eef6436dcf

View file

@ -46,7 +46,8 @@ Read more about [author responsibilities](#the-responsibility-of-the-merge-reque
Domain experts are team members who have substantial experience with a specific technology,
product feature, or area of the codebase. Team members are encouraged to self-identify as
domain experts and add it to their [team profiles](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/team_members/person/README.md).
domain experts and add it to their
[team profiles](https://about.gitlab.com/handbook/engineering/workflow/code-review/#how-to-self-identify-as-a-domain-expert).
When self-identifying as a domain expert, it is recommended to assign the MR changing the `.yml` file to be merged by an already established Domain Expert or a corresponding Engineering Manager.

View file

@ -124,17 +124,16 @@ This table shows available scopes per token. Scopes can be limited further on to
## Security considerations
Access tokens should be treated like passwords and kept secure.
Adding them to URLs is a security risk. This is especially true when cloning or adding a remote, as Git then writes the URL to its `.git/config` file in plain text. URLs are also generally logged by proxies and application servers, which makes those credentials visible to system administrators.
Instead, API calls can be passed an access token using headers, like [the `Private-Token` header](../api/index.md#personalprojectgroup-access-tokens).
Tokens can also be stored using a [Git credential storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
Tokens should not be committed to your source code. Instead, consider an approach such as [using external secrets in CI](../ci/secrets/index.md).
When creating a scoped token, consider using the most limited scope possible to reduce the impact of accidentally leaking the token.
When creating a token, consider setting a token that expires when your task is complete. For example, if performing a one-off import, set the
token to expire after a few hours or a day. This reduces the impact of a token that is accidentally leaked because it is useless when it expires.
- Access tokens should be treated like passwords and kept secure.
- Adding access tokens to URLs is a security risk, especially when cloning or adding a remote because Git then writes the URL to its `.git/config` file in plain text. URLs are
also generally logged by proxies and application servers, which makes those credentials visible to system administrators. Instead, pass API calls an access token using
headers like [the `Private-Token` header](../api/index.md#personalprojectgroup-access-tokens).
- Tokens can also be stored using a [Git credential storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
- Tokens should not be committed to your source code. Instead, consider an approach such as [using external secrets in CI](../ci/secrets/index.md).
- When creating a scoped token, consider using the most limited scope possible to reduce the impact of accidentally leaking the token.
- When creating a token, consider setting a token that expires when your task is complete. For example, if performing a one-off import, set the
token to expire after a few hours or a day. This reduces the impact of a token that is accidentally leaked because it is useless when it expires.
- Be careful not to include tokens when pasting code, console commands, or log outputs into an issue or MR description or comment.
- Dont log credentials in the console logs. Consider [protecting](../ci/variables/index.md#protected-cicd-variables) and
[masking](../ci/variables/index.md#mask-a-cicd-variable) your credentials.
- Review all currently active access tokens of all types on a regular basis and revoke any that are no longer needed.

View file

@ -29,14 +29,40 @@ For more details about the agent's purpose and architecture, see the [architectu
## Workflows
You can choose from two primary workflows.
You can choose from two primary workflows. The GitOps workflow is recommended.
In a [**GitOps** workflow](gitops.md), you keep your Kubernetes manifests in GitLab. You install a GitLab agent in your cluster, and
any time you update your manifests, the agent updates the cluster. This workflow is fully driven with Git and is considered pull-based,
### GitOps workflow
In a [**GitOps** workflow](gitops.md):
- You keep your Kubernetes manifests in GitLab.
- You install a GitLab agent in your cluster.
- Any time you update your manifests, the agent updates the cluster.
- The cluster automatically cleans up unexpected changes. It uses
[server-side applies](https://kubernetes.io/docs/reference/using-api/server-side-apply/)
to fix any configuration inconsistencies that third parties introduce.
This workflow is fully driven with Git and is considered **pull-based**,
because the cluster is pulling updates from your GitLab repository.
In a [**CI/CD** workflow](ci_cd_workflow.md), you use GitLab CI/CD to query and update your cluster by using the Kubernetes API.
This workflow is considered push-based, because GitLab is pushing requests from GitLab CI/CD to your cluster.
GitLab recommends this workflow. We are actively investing in this workflow
so we can provide a first-class experience.
### GitLab CI/CD workflow
In a [**CI/CD** workflow](ci_cd_workflow.md):
- You configure GitLab CI/CD to use the Kubernetes API to query and update your cluster.
This workflow is considered **push-based**, because GitLab is pushing requests
from GitLab CI/CD to your cluster.
Use this workflow:
- When you have a heavily pipeline-oriented processes.
- When you need to migrate to the agent but the GitOps workflow cannot support the use case you need.
This workflow has a weaker security model and is not recommended for production deployments.
## Supported cluster versions

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Backfills the `vulnerability_reads.casted_cluster_agent_id` column
class BackfillClusterAgentsHasVulnerabilities < Gitlab::BackgroundMigration::BatchedMigrationJob
VULNERABILITY_READS_JOIN = <<~SQL
INNER JOIN vulnerability_reads
ON vulnerability_reads.casted_cluster_agent_id = cluster_agents.id AND
vulnerability_reads.project_id = cluster_agents.project_id AND
vulnerability_reads.report_type = 7
SQL
RELATION = ->(relation) do
relation
.where(has_vulnerabilities: false)
end
def perform
each_sub_batch(
operation_name: :update_all,
batching_scope: RELATION
) do |sub_batch|
sub_batch
.joins(VULNERABILITY_READS_JOIN)
.update_all(has_vulnerabilities: true)
end
end
end
end
end

View file

@ -22,7 +22,7 @@ module Gitlab
ProjectFeature.connection.execute(
<<~SQL
UPDATE project_features pf
SET package_registry_access_level = (CASE p.packages_enabled
SET package_registry_access_level = (CASE p.packages_enabled
WHEN true THEN (CASE p.visibility_level
WHEN #{PROJECT_PUBLIC} THEN #{FEATURE_PUBLIC}
WHEN #{PROJECT_INTERNAL} THEN #{FEATURE_ENABLED}

View file

@ -44420,6 +44420,9 @@ msgstr ""
msgid "Work in progress Limit"
msgstr ""
msgid "WorkItem|%{workItemType} deleted"
msgstr ""
msgid "WorkItem|Add"
msgstr ""
@ -44438,7 +44441,7 @@ msgstr ""
msgid "WorkItem|Are you sure you want to cancel editing?"
msgstr ""
msgid "WorkItem|Are you sure you want to delete the task? This action cannot be reversed."
msgid "WorkItem|Are you sure you want to delete the %{workItemType}? This action cannot be reversed."
msgstr ""
msgid "WorkItem|Assignee"
@ -44464,7 +44467,7 @@ msgstr ""
msgid "WorkItem|Create work item"
msgstr ""
msgid "WorkItem|Delete task"
msgid "WorkItem|Delete %{workItemType}"
msgstr ""
msgid "WorkItem|Expand tasks"
@ -44503,18 +44506,15 @@ msgstr ""
msgid "WorkItem|Select type"
msgstr ""
msgid "WorkItem|Something went wrong when creating a task. Please try again"
msgid "WorkItem|Something went wrong when creating %{workItemType}. Please try again."
msgstr ""
msgid "WorkItem|Something went wrong when creating a work item. Please try again"
msgid "WorkItem|Something went wrong when deleting the %{workItemType}. Please try again."
msgstr ""
msgid "WorkItem|Something went wrong when deleting the task. Please try again."
msgstr ""
msgid "WorkItem|Something went wrong when deleting the work item. Please try again."
msgstr ""
msgid "WorkItem|Something went wrong when fetching tasks. Please refresh this page."
msgstr ""
@ -44530,6 +44530,9 @@ msgstr ""
msgid "WorkItem|Something went wrong when trying to create a child. Please try again."
msgstr ""
msgid "WorkItem|Something went wrong while updating the %{workItemType}. Please try again."
msgstr ""
msgid "WorkItem|Something went wrong while updating the work item. Please try again."
msgstr ""
@ -44551,9 +44554,6 @@ msgstr ""
msgid "WorkItem|Turn on confidentiality"
msgstr ""
msgid "WorkItem|Type"
msgstr ""
msgid "WorkItem|Undo"
msgstr ""
@ -44563,7 +44563,7 @@ msgstr ""
msgid "WorkItem|Work Items"
msgstr ""
msgid "WorkItem|Work item deleted"
msgid "WorkItem|Work item"
msgstr ""
msgid "Would you like to create a new branch?"

View file

@ -12,6 +12,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createFlash from '~/flash';
import Description from '~/issues/show/components/description.vue';
import { updateHistory } from '~/lib/utils/url_utility';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@ -71,7 +72,11 @@ describe('Description component', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
function createComponent({ props = {}, provide } = {}) {
function createComponent({
props = {},
provide,
createWorkItemFromTaskHandler = createWorkItemFromTaskSuccessHandler,
} = {}) {
wrapper = shallowMountExtended(Description, {
propsData: {
issueId: 1,
@ -85,7 +90,7 @@ describe('Description component', () => {
apolloProvider: createMockApollo([
[workItemQuery, queryHandler],
[workItemTypesQuery, workItemTypesQueryHandler],
[createWorkItemFromTaskMutation, createWorkItemFromTaskSuccessHandler],
[createWorkItemFromTaskMutation, createWorkItemFromTaskHandler],
]),
mocks: {
$toast,
@ -317,7 +322,28 @@ describe('Description component', () => {
expect(findModal().exists()).toBe(false);
});
it('shows toast after delete success', async () => {
const newDesc = 'description';
findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
expect($toast.show).toHaveBeenCalledWith('Task deleted');
});
});
describe('creating work item from checklist item', () => {
it('emits `updateDescription` after creating new work item', async () => {
createComponent({
props: {
descriptionHtml: descriptionHtmlWithCheckboxes,
},
provide: {
glFeatures: {
workItemsCreateFromMarkdown: true,
},
},
});
const newDescription = `<p>New description</p>`;
await findConvertToTaskButton().trigger('click');
@ -327,12 +353,28 @@ describe('Description component', () => {
expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]);
});
it('shows toast after delete success', async () => {
const newDesc = 'description';
findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
it('shows flash message when creating task fails', async () => {
createComponent({
props: {
descriptionHtml: descriptionHtmlWithCheckboxes,
},
provide: {
glFeatures: {
workItemsCreateFromMarkdown: true,
},
},
createWorkItemFromTaskHandler: jest.fn().mockRejectedValue({}),
});
expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
expect($toast.show).toHaveBeenCalledWith('Task deleted');
await findConvertToTaskButton().trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Something went wrong when creating task. Please try again.',
}),
);
});
});

View file

@ -1,15 +1,30 @@
import { GlModal } from '@gitlab/ui';
import { GlDropdownDivider, GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
const TEST_ID_DELETE_ACTION = 'delete-action';
describe('WorkItemActions component', () => {
let wrapper;
let glModalDirective;
const findModal = () => wrapper.findComponent(GlModal);
const findConfidentialityToggleButton = () =>
wrapper.findByTestId('confidentiality-toggle-action');
const findDeleteButton = () => wrapper.findByTestId('delete-action');
wrapper.findByTestId(TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION);
const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION);
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
const findDropdownItemsActual = () =>
findDropdownItems().wrappers.map((x) => {
if (x.is(GlDropdownDivider)) {
return { divider: true };
}
return {
testId: x.attributes('data-testid'),
text: x.text(),
};
});
const createComponent = ({
canUpdate = true,
@ -19,7 +34,14 @@ describe('WorkItemActions component', () => {
} = {}) => {
glModalDirective = jest.fn();
wrapper = shallowMountExtended(WorkItemActions, {
propsData: { workItemId: '123', canUpdate, canDelete, isConfidential, isParentConfidential },
propsData: {
workItemId: '123',
canUpdate,
canDelete,
isConfidential,
isParentConfidential,
workItemType: 'Task',
},
directives: {
glModal: {
bind(_, { value }) {
@ -44,8 +66,19 @@ describe('WorkItemActions component', () => {
it('renders dropdown actions', () => {
createComponent();
expect(findConfidentialityToggleButton().exists()).toBe(true);
expect(findDeleteButton().exists()).toBe(true);
expect(findDropdownItemsActual()).toEqual([
{
testId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
text: 'Turn on confidentiality',
},
{
divider: true,
},
{
testId: TEST_ID_DELETE_ACTION,
text: 'Delete task',
},
]);
});
describe('toggle confidentiality action', () => {
@ -103,7 +136,8 @@ describe('WorkItemActions component', () => {
canDelete: false,
});
expect(wrapper.findByTestId('delete-action').exists()).toBe(false);
expect(findDeleteButton().exists()).toBe(false);
expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
});
});
});

View file

@ -7,7 +7,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import ItemState from '~/work_items/components/item_state.vue';
import WorkItemState from '~/work_items/components/work_item_state.vue';
import {
i18n,
STATE_OPEN,
STATE_CLOSED,
STATE_EVENT_CLOSE,
@ -104,7 +103,9 @@ describe('WorkItemState component', () => {
findItemState().vm.$emit('changed', STATE_CLOSED);
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
expect(wrapper.emitted('error')).toEqual([
['Something went wrong while updating the task. Please try again.'],
]);
});
it('tracks editing the state', async () => {

View file

@ -6,7 +6,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ItemTitle from '~/work_items/components/item_title.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
@ -116,7 +116,9 @@ describe('WorkItemTitle component', () => {
findItemTitle().vm.$emit('title-changed', 'new title');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
expect(wrapper.emitted('error')).toEqual([
['Something went wrong while updating the task. Please try again.'],
]);
});
it('tracks editing the title', async () => {

View file

@ -7,7 +7,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { __ } from '~/locale';
import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import { updateWorkItemMutationResponse } from 'jest/work_items/mock_data';
@ -181,7 +181,9 @@ describe('WorkItemWeight component', () => {
findInput().trigger('blur');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
expect(wrapper.emitted('error')).toEqual([
['Something went wrong while updating the task. Please try again.'],
]);
});
it('emits an error when there is a network error', async () => {
@ -194,7 +196,9 @@ describe('WorkItemWeight component', () => {
findInput().trigger('blur');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
expect(wrapper.emitted('error')).toEqual([
['Something went wrong while updating the task. Please try again.'],
]);
});
it('tracks updating the weight', () => {

View file

@ -193,6 +193,8 @@ describe('Create work item component', () => {
wrapper.find('form').trigger('submit');
await waitForPromises();
expect(findAlert().text()).toBe(CreateWorkItem.createErrorText);
expect(findAlert().text()).toBe(
'Something went wrong when creating work item. Please try again.',
);
});
});

View file

@ -379,11 +379,12 @@ describe('WorkItemDetail component', () => {
it('shows an error message when WorkItemTitle emits an `error` event', async () => {
createComponent();
await waitForPromises();
const updateError = 'Failed to update';
findWorkItemTitle().vm.$emit('error', i18n.updateError);
findWorkItemTitle().vm.$emit('error', updateError);
await waitForPromises();
expect(findAlert().text()).toBe(i18n.updateError);
expect(findAlert().text()).toBe(updateError);
});
it('calls the subscription', () => {
@ -456,11 +457,12 @@ describe('WorkItemDetail component', () => {
it('shows an error message when it emits an `error` event', async () => {
createComponent({ workItemsMvc2Enabled: true });
await waitForPromises();
const updateError = 'Failed to update';
findWorkItemWeight().vm.$emit('error', i18n.updateError);
findWorkItemWeight().vm.$emit('error', updateError);
await waitForPromises();
expect(findAlert().text()).toBe(i18n.updateError);
expect(findAlert().text()).toBe(updateError);
});
});

View file

@ -0,0 +1,107 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillClusterAgentsHasVulnerabilities, :migration do # rubocop:disable Layout/LineLength
let(:migration) do
described_class.new(start_id: 1, end_id: 10,
batch_table: table_name, batch_column: batch_column,
sub_batch_size: sub_batch_size, pause_ms: pause_ms,
connection: ApplicationRecord.connection)
end
let(:users_table) { table(:users) }
let(:vulnerability_reads_table) { table(:vulnerability_reads) }
let(:vulnerability_scanners_table) { table(:vulnerability_scanners) }
let(:vulnerabilities_table) { table(:vulnerabilities) }
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:cluster_agents_table) { table(:cluster_agents) }
let(:table_name) { 'cluster_agents' }
let(:batch_column) { :id }
let(:sub_batch_size) { 100 }
let(:pause_ms) { 0 }
subject(:perform_migration) { migration.perform }
before do
users_table.create!(id: 1, name: 'John Doe', email: 'test@example.com', projects_limit: 5)
namespaces_table.create!(id: 1, name: 'Namespace 1', path: 'namespace-1')
namespaces_table.create!(id: 2, name: 'Namespace 2', path: 'namespace-2')
namespaces_table.create!(id: 3, name: 'Namespace 3', path: 'namespace-3')
projects_table.create!(id: 1, namespace_id: 1, name: 'Project 1', path: 'project-1', project_namespace_id: 1)
projects_table.create!(id: 2, namespace_id: 2, name: 'Project 2', path: 'project-2', project_namespace_id: 2)
projects_table.create!(id: 3, namespace_id: 2, name: 'Project 3', path: 'project-3', project_namespace_id: 3)
cluster_agents_table.create!(id: 1, name: 'Agent 1', project_id: 1)
cluster_agents_table.create!(id: 2, name: 'Agent 2', project_id: 2)
cluster_agents_table.create!(id: 3, name: 'Agent 3', project_id: 1)
cluster_agents_table.create!(id: 4, name: 'Agent 4', project_id: 1)
cluster_agents_table.create!(id: 5, name: 'Agent 5', project_id: 1)
cluster_agents_table.create!(id: 6, name: 'Agent 6', project_id: 1)
cluster_agents_table.create!(id: 7, name: 'Agent 7', project_id: 3)
cluster_agents_table.create!(id: 8, name: 'Agent 8', project_id: 1)
cluster_agents_table.create!(id: 9, name: 'Agent 9', project_id: 1)
cluster_agents_table.create!(id: 10, name: 'Agent 10', project_id: 3)
cluster_agents_table.create!(id: 11, name: 'Agent 11', project_id: 1)
vulnerability_scanners_table.create!(id: 1, project_id: 1, external_id: 'starboard', name: 'Starboard')
vulnerability_scanners_table.create!(id: 2, project_id: 2, external_id: 'starboard', name: 'Starboard')
vulnerability_scanners_table.create!(id: 3, project_id: 3, external_id: 'starboard', name: 'Starboard')
add_vulnerability_read!(1, project_id: 1, cluster_agent_id: 1, report_type: 7)
add_vulnerability_read!(2, project_id: 1, cluster_agent_id: nil, report_type: 7)
add_vulnerability_read!(3, project_id: 1, cluster_agent_id: 3, report_type: 7)
add_vulnerability_read!(4, project_id: 1, cluster_agent_id: nil, report_type: 7)
add_vulnerability_read!(5, project_id: 2, cluster_agent_id: 5, report_type: 5)
add_vulnerability_read!(7, project_id: 2, cluster_agent_id: 7, report_type: 7)
add_vulnerability_read!(9, project_id: 3, cluster_agent_id: 9, report_type: 7)
add_vulnerability_read!(10, project_id: 1, cluster_agent_id: 10, report_type: 7)
add_vulnerability_read!(11, project_id: 2, cluster_agent_id: 11, report_type: 7)
end
it 'backfills `has_vulnerabilities` for the selected records', :aggregate_failures do
queries = ActiveRecord::QueryRecorder.new do
perform_migration
end
expect(queries.count).to eq(3)
expect(cluster_agents_table.where(has_vulnerabilities: true).count).to eq 2
expect(cluster_agents_table.where(has_vulnerabilities: true).pluck(:id)).to match_array([1, 3])
end
it 'tracks timings of queries' do
expect(migration.batch_metrics.timings).to be_empty
expect { perform_migration }.to change { migration.batch_metrics.timings }
end
private
def add_vulnerability_read!(id, project_id:, cluster_agent_id:, report_type:)
vulnerabilities_table.create!(
id: id,
project_id: project_id,
author_id: 1,
title: "Vulnerability #{id}",
severity: 5,
confidence: 5,
report_type: report_type
)
vulnerability_reads_table.create!(
id: id,
uuid: SecureRandom.uuid,
severity: 5,
state: 1,
vulnerability_id: id,
scanner_id: project_id,
casted_cluster_agent_id: cluster_agent_id,
project_id: project_id,
report_type: report_type
)
end
end

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe ScheduleBackfillClusterAgentsHasVulnerabilities do
let_it_be(:batched_migration) { described_class::MIGRATION }
it 'schedules background jobs for each batch of cluster agents' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
table_name: :cluster_agents,
column_name: :id,
interval: described_class::DELAY_INTERVAL
)
}
end
end
end

View file

@ -17,6 +17,11 @@ RSpec.describe 'profiles/show' do
expect(rendered).to have_field('user_name', with: user.name)
expect(rendered).to have_field('user_id', with: user.id)
expectd_link = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits')
expected_link_html = "<a href=\"#{expectd_link}\" target=\"_blank\" " \
"rel=\"noopener noreferrer\">#{_('Learn more.')}</a>"
expect(rendered.include?(expected_link_html)).to eq(true)
end
end
end