Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d27e35761a
commit
a2b477802d
30 changed files with 353 additions and 109 deletions
|
@ -1,13 +1,10 @@
|
|||
import dateFormat from 'dateformat';
|
||||
import { unescape } from 'lodash';
|
||||
import { dateFormats } from '~/analytics/shared/constants';
|
||||
import { hideFlash } from '~/flash';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import { roundToNearestHalf } from '~/lib/utils/common_utils';
|
||||
import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
|
||||
import { parseSeconds } from '~/lib/utils/datetime_utility';
|
||||
import { formatTimeAsSummary } from '~/lib/utils/datetime/date_format_utility';
|
||||
import { slugify } from '~/lib/utils/text_utility';
|
||||
import { s__, sprintf } from '../locale';
|
||||
|
||||
export const removeFlash = (type = 'alert') => {
|
||||
const flashEl = document.querySelector(`.flash-${type}`);
|
||||
|
@ -45,29 +42,6 @@ export const transformStagesForPathNavigation = ({
|
|||
return formattedStages;
|
||||
};
|
||||
|
||||
export const timeSummaryForPathNavigation = ({ seconds, hours, days, minutes, weeks, months }) => {
|
||||
if (months) {
|
||||
return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
|
||||
value: roundToNearestHalf(months),
|
||||
});
|
||||
} else if (weeks) {
|
||||
return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
|
||||
value: roundToNearestHalf(weeks),
|
||||
});
|
||||
} else if (days) {
|
||||
return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
|
||||
value: roundToNearestHalf(days),
|
||||
});
|
||||
} else if (hours) {
|
||||
return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
|
||||
} else if (minutes) {
|
||||
return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
|
||||
} else if (seconds) {
|
||||
return unescape(sanitize(s__('ValueStreamAnalytics|<1m'), { ALLOWED_TAGS: [] }));
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a raw median value in seconds and converts it to a string representation
|
||||
* ie. converts 172800 => 2d (2 days)
|
||||
|
@ -76,7 +50,7 @@ export const timeSummaryForPathNavigation = ({ seconds, hours, days, minutes, we
|
|||
* @returns {String} String representation ie 2w
|
||||
*/
|
||||
export const medianTimeToParsedSeconds = (value) =>
|
||||
timeSummaryForPathNavigation({
|
||||
formatTimeAsSummary({
|
||||
...parseSeconds(value, { daysPerWeek: 7, hoursPerDay: 24 }),
|
||||
seconds: value,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import dateFormat from 'dateformat';
|
||||
import { isString, mapValues, reduce, isDate } from 'lodash';
|
||||
import { s__, n__, __ } from '../../../locale';
|
||||
import { isString, mapValues, reduce, isDate, unescape } from 'lodash';
|
||||
import { roundToNearestHalf } from '~/lib/utils/common_utils';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import { s__, n__, __, sprintf } from '../../../locale';
|
||||
|
||||
/**
|
||||
* Returns i18n month names array.
|
||||
|
@ -361,3 +363,26 @@ export const dateToTimeInputValue = (date) => {
|
|||
hour12: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const formatTimeAsSummary = ({ seconds, hours, days, minutes, weeks, months }) => {
|
||||
if (months) {
|
||||
return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
|
||||
value: roundToNearestHalf(months),
|
||||
});
|
||||
} else if (weeks) {
|
||||
return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
|
||||
value: roundToNearestHalf(weeks),
|
||||
});
|
||||
} else if (days) {
|
||||
return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
|
||||
value: roundToNearestHalf(days),
|
||||
});
|
||||
} else if (hours) {
|
||||
return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
|
||||
} else if (minutes) {
|
||||
return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
|
||||
} else if (seconds) {
|
||||
return unescape(sanitize(s__('ValueStreamAnalytics|<1m'), { ALLOWED_TAGS: [] }));
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
|
|
@ -11,15 +11,26 @@ import {
|
|||
GlButton,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql';
|
||||
import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import createFlash, { FLASH_TYPES } from '~/flash';
|
||||
|
||||
import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
|
||||
|
||||
export const i18n = {
|
||||
snippetSpamSuccess: sprintf(
|
||||
s__('Snippets|%{spammable_titlecase} was submitted to Akismet successfully.'),
|
||||
{ spammable_titlecase: __('Snippet') },
|
||||
),
|
||||
snippetSpamFailure: s__('Snippets|Error with Akismet. Please check the logs for more info.'),
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAvatar,
|
||||
|
@ -54,7 +65,7 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
inject: ['reportAbusePath'],
|
||||
inject: ['reportAbusePath', 'canReportSpam'],
|
||||
props: {
|
||||
snippet: {
|
||||
type: Object,
|
||||
|
@ -63,7 +74,8 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
isDeleting: false,
|
||||
isLoading: false,
|
||||
isSubmittingSpam: false,
|
||||
errorMessage: '',
|
||||
canCreateSnippet: false,
|
||||
};
|
||||
|
@ -105,10 +117,11 @@ export default {
|
|||
category: 'secondary',
|
||||
},
|
||||
{
|
||||
condition: this.reportAbusePath,
|
||||
condition: this.canReportSpam && !isEmpty(this.reportAbusePath),
|
||||
text: __('Submit as spam'),
|
||||
href: this.reportAbusePath,
|
||||
click: this.submitAsSpam,
|
||||
title: __('Submit as spam'),
|
||||
loading: this.isSubmittingSpam,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
@ -157,7 +170,7 @@ export default {
|
|||
this.$refs.deleteModal.show();
|
||||
},
|
||||
deleteSnippet() {
|
||||
this.isDeleting = true;
|
||||
this.isLoading = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: DeleteSnippetMutation,
|
||||
|
@ -167,17 +180,34 @@ export default {
|
|||
if (data?.destroySnippet?.errors.length) {
|
||||
throw new Error(data?.destroySnippet?.errors[0]);
|
||||
}
|
||||
this.isDeleting = false;
|
||||
this.errorMessage = undefined;
|
||||
this.closeDeleteModal();
|
||||
this.redirectToSnippets();
|
||||
})
|
||||
.catch((err) => {
|
||||
this.isDeleting = false;
|
||||
this.isLoading = false;
|
||||
this.errorMessage = err.message;
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
async submitAsSpam() {
|
||||
try {
|
||||
this.isSubmittingSpam = true;
|
||||
await axios.post(this.reportAbusePath);
|
||||
createFlash({
|
||||
message: this.$options.i18n.snippetSpamSuccess,
|
||||
type: FLASH_TYPES.SUCCESS,
|
||||
});
|
||||
} catch (error) {
|
||||
createFlash({ message: this.$options.i18n.snippetSpamFailure });
|
||||
} finally {
|
||||
this.isSubmittingSpam = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
i18n,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
@ -189,9 +219,7 @@ export default {
|
|||
:title="snippetVisibilityLevelDescription"
|
||||
data-container="body"
|
||||
>
|
||||
<span class="sr-only">
|
||||
{{ s__(`VisibilityLevel|${visibility}`) }}
|
||||
</span>
|
||||
<span class="sr-only">{{ s__(`VisibilityLevel|${visibility}`) }}</span>
|
||||
<gl-icon :name="visibilityLevelIcon" :size="14" />
|
||||
</div>
|
||||
<div class="creator" data-testid="authored-message">
|
||||
|
@ -233,6 +261,7 @@ export default {
|
|||
>
|
||||
<gl-button
|
||||
:disabled="action.disabled"
|
||||
:loading="action.loading"
|
||||
:variant="action.variant"
|
||||
:category="action.category"
|
||||
:class="action.cssClass"
|
||||
|
@ -240,9 +269,8 @@ export default {
|
|||
data-qa-selector="snippet_action_button"
|
||||
:data-qa-action="action.text"
|
||||
@click="action.click ? action.click() : undefined"
|
||||
>{{ action.text }}</gl-button
|
||||
>
|
||||
{{ action.text }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -266,14 +294,14 @@ export default {
|
|||
<gl-modal ref="deleteModal" modal-id="delete-modal" title="Example title">
|
||||
<template #modal-title>{{ __('Delete snippet?') }}</template>
|
||||
|
||||
<gl-alert v-if="errorMessage" variant="danger" class="mb-2" @dismiss="errorMessage = ''">{{
|
||||
errorMessage
|
||||
}}</gl-alert>
|
||||
<gl-alert v-if="errorMessage" variant="danger" class="mb-2" @dismiss="errorMessage = ''">
|
||||
{{ errorMessage }}
|
||||
</gl-alert>
|
||||
|
||||
<gl-sprintf :message="__('Are you sure you want to delete %{name}?')">
|
||||
<template #name
|
||||
><strong>{{ snippet.title }}</strong></template
|
||||
>
|
||||
<template #name>
|
||||
<strong>{{ snippet.title }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
|
||||
<template #modal-footer>
|
||||
|
@ -281,11 +309,11 @@ export default {
|
|||
<gl-button
|
||||
variant="danger"
|
||||
category="primary"
|
||||
:disabled="isDeleting"
|
||||
:disabled="isLoading"
|
||||
data-qa-selector="delete_snippet_button"
|
||||
@click="deleteSnippet"
|
||||
>
|
||||
<gl-loading-icon v-if="isDeleting" size="sm" inline />
|
||||
<gl-loading-icon v-if="isLoading" size="sm" inline />
|
||||
{{ __('Delete snippet') }}
|
||||
</gl-button>
|
||||
</template>
|
||||
|
|
|
@ -27,6 +27,7 @@ export default function appFactory(el, Component) {
|
|||
visibilityLevels = '[]',
|
||||
selectedLevel,
|
||||
multipleLevelsRestricted,
|
||||
canReportSpam,
|
||||
reportAbusePath,
|
||||
...restDataset
|
||||
} = el.dataset;
|
||||
|
@ -39,6 +40,7 @@ export default function appFactory(el, Component) {
|
|||
selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
|
||||
multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
|
||||
reportAbusePath,
|
||||
canReportSpam,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(Component, {
|
||||
|
|
|
@ -41,6 +41,7 @@ const populateUserInfo = (user) => {
|
|||
workInformation: userData.work_information,
|
||||
websiteUrl: userData.website_url,
|
||||
pronouns: userData.pronouns,
|
||||
localTime: userData.local_time,
|
||||
loaded: true,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -93,19 +93,27 @@ export default {
|
|||
</div>
|
||||
<div class="gl-text-gray-500">
|
||||
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
|
||||
<gl-icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
|
||||
<gl-icon name="profile" class="gl-flex-shrink-0" />
|
||||
<span ref="bio" class="gl-ml-2 gl-overflow-hidden">{{ user.bio }}</span>
|
||||
</div>
|
||||
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
|
||||
<gl-icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
|
||||
<gl-icon name="work" class="gl-flex-shrink-0" />
|
||||
<span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
|
||||
</div>
|
||||
<div v-if="user.location" class="gl-display-flex gl-mb-2">
|
||||
<gl-icon name="location" class="gl-flex-shrink-0" />
|
||||
<span class="gl-ml-2">{{ user.location }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="user.localTime && !user.bot"
|
||||
class="gl-display-flex gl-mb-2"
|
||||
data-testid="user-popover-local-time"
|
||||
>
|
||||
<gl-icon name="clock" class="gl-flex-shrink-0" />
|
||||
<span class="gl-ml-2">{{ user.localTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="user.location" class="js-location gl-text-gray-500 gl-display-flex">
|
||||
<gl-icon name="location" class="gl-text-gray-400 flex-shrink-0" />
|
||||
<span class="gl-ml-2">{{ user.location }}</span>
|
||||
</div>
|
||||
<div v-if="statusHtml" class="js-user-status gl-mt-3">
|
||||
<div v-if="statusHtml" class="gl-mb-2" data-testid="user-popover-status">
|
||||
<span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span>
|
||||
</div>
|
||||
<div v-if="user.bot" class="gl-text-blue-500">
|
||||
|
|
|
@ -10,7 +10,9 @@ module Clusters
|
|||
|
||||
validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
|
||||
|
||||
delegate :project, to: :agent
|
||||
def config_project
|
||||
agent.project
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,12 +6,15 @@ module Clusters
|
|||
attr_reader :agent
|
||||
|
||||
delegate :id, to: :agent, prefix: true
|
||||
delegate :project, to: :agent
|
||||
|
||||
def initialize(agent:)
|
||||
@agent = agent
|
||||
end
|
||||
|
||||
def config_project
|
||||
agent.project
|
||||
end
|
||||
|
||||
def config
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -9,6 +9,10 @@ module Clusters
|
|||
belongs_to :project, class_name: '::Project', optional: false
|
||||
|
||||
validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
|
||||
|
||||
def config_project
|
||||
agent.project
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,5 +12,13 @@ module Users
|
|||
validates :last_digits, allow_nil: true, numericality: {
|
||||
greater_than_or_equal_to: 0, less_than_or_equal_to: 9999
|
||||
}
|
||||
|
||||
def similar_records
|
||||
self.class.where(
|
||||
expiration_date: expiration_date,
|
||||
last_digits: last_digits,
|
||||
holder_name: holder_name
|
||||
).order(credit_card_validated_at: :desc).includes(:user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -61,7 +61,6 @@
|
|||
= _('Disabled')
|
||||
|
||||
= render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace
|
||||
= render_if_exists 'admin/users/credit_card_info', user: @user
|
||||
|
||||
%li
|
||||
%span.light= _('External User:')
|
||||
|
@ -139,6 +138,8 @@
|
|||
|
||||
= render_if_exists 'namespaces/shared_runner_status', namespace: @user.namespace
|
||||
|
||||
= render_if_exists 'admin/users/credit_card_info', user: @user, link_to_match_page: true
|
||||
|
||||
= render 'shared/custom_attributes', custom_attributes: @user.custom_attributes
|
||||
|
||||
-# Rendered on desktop only so order of cards can be different on desktop vs mobile
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
- breadcrumb_title @snippet.to_reference
|
||||
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
|
||||
|
||||
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } }
|
||||
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
|
||||
|
||||
.row-content-block.top-block.content-component-block
|
||||
= render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
- content_for :prefetch_asset_tags do
|
||||
- webpack_preload_asset_tag('monaco', prefetch: true)
|
||||
|
||||
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } }
|
||||
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
|
||||
|
||||
.row-content-block.top-block.content-component-block
|
||||
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: finding_ci_pipeline_disable_joins
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70216
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338665
|
||||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::threat insights
|
||||
default_enabled: true
|
|
@ -192,6 +192,20 @@ cannot change them:
|
|||
This ensures that your job uses the settings you intend and that they are not overridden by
|
||||
project-level pipelines.
|
||||
|
||||
##### Avoid parent and child pipelines
|
||||
|
||||
Compliance pipelines start on the run of _every_ pipeline in a relevant project. This means that if a pipeline in the relevant project
|
||||
triggers a child pipeline, the compliance pipeline runs first. This can trigger the parent pipeline, instead of the child pipeline.
|
||||
|
||||
Therefore, in projects with compliance frameworks, we recommend replacing
|
||||
[parent-child pipelines](../../../ci/pipelines/parent_child_pipelines.md) with the following:
|
||||
|
||||
- Direct [`include`](../../../ci/yaml/index.md#include) statements that provide the parent pipeline with child pipeline configuration.
|
||||
- Child pipelines placed in another project that are run using the [trigger API](../../../ci/triggers/) rather than the parent-child
|
||||
pipeline feature.
|
||||
|
||||
This alternative ensures the compliance pipeline does not re-start the parent pipeline.
|
||||
|
||||
### Sharing and permissions
|
||||
|
||||
For your repository, you can set up features such as public access, repository features,
|
||||
|
|
|
@ -5,7 +5,7 @@ module API
|
|||
module Clusters
|
||||
class AgentAuthorization < Grape::Entity
|
||||
expose :agent_id, as: :id
|
||||
expose :project, with: Entities::ProjectIdentity, as: :config_project
|
||||
expose :config_project, with: Entities::ProjectIdentity
|
||||
expose :config, as: :configuration
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@ module API
|
|||
module Entities
|
||||
class User < UserBasic
|
||||
include UsersHelper
|
||||
include TimeZoneHelper
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) }
|
||||
|
@ -24,6 +25,10 @@ module API
|
|||
expose :bio_html do |user|
|
||||
strip_tags(user.bio)
|
||||
end
|
||||
|
||||
expose :local_time do |user|
|
||||
local_time(user.timezone)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1141,6 +1141,9 @@ msgstr ""
|
|||
msgid "(revoked)"
|
||||
msgstr ""
|
||||
|
||||
msgid "(target)"
|
||||
msgstr ""
|
||||
|
||||
msgid "(we need your current password to confirm your changes)"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3341,6 +3344,9 @@ msgstr ""
|
|||
msgid "All users must have a name."
|
||||
msgstr ""
|
||||
|
||||
msgid "All users with matching cards"
|
||||
msgstr ""
|
||||
|
||||
msgid "Allow \"%{group_name}\" to sign you in"
|
||||
msgstr ""
|
||||
|
||||
|
@ -6309,6 +6315,9 @@ msgstr ""
|
|||
msgid "Capacity threshold"
|
||||
msgstr ""
|
||||
|
||||
msgid "Card number:"
|
||||
msgstr ""
|
||||
|
||||
msgid "CascadingSettings|Enforce for all subgroups"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9924,10 +9933,7 @@ msgstr ""
|
|||
msgid "CredentialsInventory|SSH Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "Credit card validated at:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Credit card validated:"
|
||||
msgid "Credit card:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Critical vulnerabilities present"
|
||||
|
@ -9984,6 +9990,9 @@ msgstr ""
|
|||
msgid "Current sign-in at:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Current sign-in ip"
|
||||
msgstr ""
|
||||
|
||||
msgid "Current vulnerabilities count"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13806,6 +13815,9 @@ msgstr ""
|
|||
msgid "Expiration date (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expiration date:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expired"
|
||||
msgstr ""
|
||||
|
||||
|
@ -16853,6 +16865,9 @@ msgstr ""
|
|||
msgid "History of authentications"
|
||||
msgstr ""
|
||||
|
||||
msgid "Holder name:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Home page URL"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22939,6 +22954,9 @@ msgstr ""
|
|||
msgid "No contributions were found"
|
||||
msgstr ""
|
||||
|
||||
msgid "No credit card data for matching"
|
||||
msgstr ""
|
||||
|
||||
msgid "No credit card required."
|
||||
msgstr ""
|
||||
|
||||
|
@ -31589,6 +31607,9 @@ msgstr ""
|
|||
msgid "Smartcard authentication failed: client certificate header is missing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Snippet"
|
||||
msgstr ""
|
||||
|
||||
msgid "Snippets"
|
||||
msgstr ""
|
||||
|
||||
|
@ -31613,6 +31634,9 @@ msgstr ""
|
|||
msgid "SnippetsEmptyState|There are no snippets to show."
|
||||
msgstr ""
|
||||
|
||||
msgid "Snippets|%{spammable_titlecase} was submitted to Akismet successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Snippets|Add another file %{num}/%{total}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -31622,6 +31646,9 @@ msgstr ""
|
|||
msgid "Snippets|Description (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Snippets|Error with Akismet. Please check the logs for more info."
|
||||
msgstr ""
|
||||
|
||||
msgid "Snippets|Files"
|
||||
msgstr ""
|
||||
|
||||
|
@ -36953,6 +36980,9 @@ msgstr ""
|
|||
msgid "User and IP rate limits"
|
||||
msgstr ""
|
||||
|
||||
msgid "User created at"
|
||||
msgstr ""
|
||||
|
||||
msgid "User does not have a pending request"
|
||||
msgstr ""
|
||||
|
||||
|
@ -37313,6 +37343,15 @@ msgstr ""
|
|||
msgid "Validate your GitLab CI configuration file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Validated at"
|
||||
msgstr ""
|
||||
|
||||
msgid "Validated at:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Validated:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Validations failed."
|
||||
msgstr ""
|
||||
|
||||
|
@ -40851,6 +40890,9 @@ msgstr ""
|
|||
msgid "originating vulnerability"
|
||||
msgstr ""
|
||||
|
||||
msgid "other card matches"
|
||||
msgstr ""
|
||||
|
||||
msgid "out of %d total test"
|
||||
msgid_plural "out of %d total tests"
|
||||
msgstr[0] ""
|
||||
|
|
|
@ -98,7 +98,7 @@ function rspec_simple_job() {
|
|||
}
|
||||
|
||||
function rspec_db_library_code() {
|
||||
local db_files="spec/lib/gitlab/database/ spec/support/helpers/database/"
|
||||
local db_files="spec/lib/gitlab/database/"
|
||||
|
||||
rspec_simple_job "-- ${db_files}"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import {
|
||||
transformStagesForPathNavigation,
|
||||
timeSummaryForPathNavigation,
|
||||
medianTimeToParsedSeconds,
|
||||
formatMedianValues,
|
||||
filterStagesByHiddenStatus,
|
||||
|
@ -47,21 +46,6 @@ describe('Value stream analytics utils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('timeSummaryForPathNavigation', () => {
|
||||
it.each`
|
||||
unit | value | result
|
||||
${'months'} | ${1.5} | ${'1.5M'}
|
||||
${'weeks'} | ${1.25} | ${'1.5w'}
|
||||
${'days'} | ${2} | ${'2d'}
|
||||
${'hours'} | ${10} | ${'10h'}
|
||||
${'minutes'} | ${20} | ${'20m'}
|
||||
${'seconds'} | ${10} | ${'<1m'}
|
||||
${'seconds'} | ${0} | ${'-'}
|
||||
`('will format $value $unit to $result', ({ unit, value, result }) => {
|
||||
expect(timeSummaryForPathNavigation({ [unit]: value })).toBe(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('medianTimeToParsedSeconds', () => {
|
||||
it.each`
|
||||
value | result
|
||||
|
|
|
@ -118,3 +118,18 @@ describe('date_format_utility.js', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatTimeAsSummary', () => {
|
||||
it.each`
|
||||
unit | value | result
|
||||
${'months'} | ${1.5} | ${'1.5M'}
|
||||
${'weeks'} | ${1.25} | ${'1.5w'}
|
||||
${'days'} | ${2} | ${'2d'}
|
||||
${'hours'} | ${10} | ${'10h'}
|
||||
${'minutes'} | ${20} | ${'20m'}
|
||||
${'seconds'} | ${10} | ${'<1m'}
|
||||
${'seconds'} | ${0} | ${'-'}
|
||||
`('will format $value $unit to $result', ({ unit, value, result }) => {
|
||||
expect(utils.formatTimeAsSummary({ [unit]: value })).toBe(result);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,19 +41,23 @@ describe('Snippet view app', () => {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findEmbedDropdown = () => wrapper.findComponent(EmbedDropdown);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders loader while the query is in flight', () => {
|
||||
createComponent({ loading: true });
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders all simple components after the query is finished', () => {
|
||||
it('renders all simple components required after the query is finished', () => {
|
||||
createComponent();
|
||||
expect(wrapper.find(SnippetHeader).exists()).toBe(true);
|
||||
expect(wrapper.find(SnippetTitle).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(SnippetHeader).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(SnippetTitle).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders embed dropdown component if visibility allows', () => {
|
||||
|
@ -65,7 +69,7 @@ describe('Snippet view app', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.find(EmbedDropdown).exists()).toBe(true);
|
||||
expect(findEmbedDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correct snippet-blob components', () => {
|
||||
|
@ -98,7 +102,7 @@ describe('Snippet view app', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.find(EmbedDropdown).exists()).toBe(isRendered);
|
||||
expect(findEmbedDropdown().exists()).toBe(isRendered);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -120,7 +124,7 @@ describe('Snippet view app', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.find(CloneDropdownButton).exists()).toBe(isRendered);
|
||||
expect(wrapper.findComponent(CloneDropdownButton).exists()).toBe(isRendered);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,23 +1,30 @@
|
|||
import { GlButton, GlModal, GlDropdown } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { ApolloMutation } from 'vue-apollo';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
|
||||
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
|
||||
import SnippetHeader from '~/snippets/components/snippet_header.vue';
|
||||
import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
|
||||
import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import createFlash, { FLASH_TYPES } from '~/flash';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
describe('Snippet header component', () => {
|
||||
let wrapper;
|
||||
let snippet;
|
||||
let mutationTypes;
|
||||
let mutationVariables;
|
||||
let mock;
|
||||
|
||||
let errorMsg;
|
||||
let err;
|
||||
const originalRelativeUrlRoot = gon.relative_url_root;
|
||||
const reportAbusePath = '/-/snippets/42/mark_as_spam';
|
||||
const canReportSpam = true;
|
||||
|
||||
const GlEmoji = { template: '<img/>' };
|
||||
|
||||
|
@ -47,6 +54,7 @@ describe('Snippet header component', () => {
|
|||
mocks: { $apollo },
|
||||
provide: {
|
||||
reportAbusePath,
|
||||
canReportSpam,
|
||||
...provide,
|
||||
},
|
||||
propsData: {
|
||||
|
@ -118,10 +126,13 @@ describe('Snippet header component', () => {
|
|||
RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })),
|
||||
REJECT: jest.fn(() => Promise.reject(err)),
|
||||
};
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
mock.restore();
|
||||
gon.relative_url_root = originalRelativeUrlRoot;
|
||||
});
|
||||
|
||||
|
@ -186,7 +197,6 @@ describe('Snippet header component', () => {
|
|||
{
|
||||
category: 'primary',
|
||||
disabled: false,
|
||||
href: reportAbusePath,
|
||||
text: 'Submit as spam',
|
||||
variant: 'default',
|
||||
},
|
||||
|
@ -205,7 +215,6 @@ describe('Snippet header component', () => {
|
|||
text: 'Delete',
|
||||
},
|
||||
{
|
||||
href: reportAbusePath,
|
||||
text: 'Submit as spam',
|
||||
title: 'Submit as spam',
|
||||
},
|
||||
|
@ -249,6 +258,31 @@ describe('Snippet header component', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('submit snippet as spam', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it.each`
|
||||
request | variant | text
|
||||
${200} | ${'SUCCESS'} | ${i18n.snippetSpamSuccess}
|
||||
${500} | ${'DANGER'} | ${i18n.snippetSpamFailure}
|
||||
`(
|
||||
'renders a "$variant" flash message with "$text" message for a request with a "$request" response',
|
||||
async ({ request, variant, text }) => {
|
||||
const submitAsSpamBtn = findButtons().at(2);
|
||||
mock.onPost(reportAbusePath).reply(request);
|
||||
submitAsSpamBtn.trigger('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(createFlash).toHaveBeenLastCalledWith({
|
||||
message: expect.stringContaining(text),
|
||||
type: FLASH_TYPES[variant],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('with guest user', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
|
@ -258,6 +292,7 @@ describe('Snippet header component', () => {
|
|||
},
|
||||
provide: {
|
||||
reportAbusePath: null,
|
||||
canReportSpam: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ const DEFAULT_PROPS = {
|
|||
username: 'root',
|
||||
name: 'Administrator',
|
||||
location: 'Vienna',
|
||||
localTime: '2:30 PM',
|
||||
bot: false,
|
||||
bio: null,
|
||||
workInformation: null,
|
||||
|
@ -31,10 +32,11 @@ describe('User Popover Component', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findUserStatus = () => wrapper.find('.js-user-status');
|
||||
const findUserStatus = () => wrapper.findByTestId('user-popover-status');
|
||||
const findTarget = () => document.querySelector('.js-user-link');
|
||||
const findUserName = () => wrapper.find(UserNameWithStatus);
|
||||
const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link');
|
||||
const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time');
|
||||
|
||||
const createWrapper = (props = {}, options = {}) => {
|
||||
wrapper = mountExtended(UserPopover, {
|
||||
|
@ -71,7 +73,6 @@ describe('User Popover Component', () => {
|
|||
|
||||
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name);
|
||||
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username);
|
||||
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.location);
|
||||
});
|
||||
|
||||
it('shows icon for location', () => {
|
||||
|
@ -164,6 +165,25 @@ describe('User Popover Component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('local time', () => {
|
||||
it('should show local time when it is available', () => {
|
||||
createWrapper();
|
||||
|
||||
expect(findUserLocalTime().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show local time when it is not available', () => {
|
||||
const user = {
|
||||
...DEFAULT_PROPS.user,
|
||||
localTime: null,
|
||||
};
|
||||
|
||||
createWrapper({ user });
|
||||
|
||||
expect(findUserLocalTime().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('status data', () => {
|
||||
it('should show only message', () => {
|
||||
const user = { ...DEFAULT_PROPS.user, status: { message_html: 'Hello World' } };
|
||||
|
@ -256,5 +276,11 @@ describe('User Popover Component', () => {
|
|||
const securityBotDocsLink = findSecurityBotDocsLink();
|
||||
expect(securityBotDocsLink.text()).toBe('Learn more about %<>\';"');
|
||||
});
|
||||
|
||||
it('does not display local time', () => {
|
||||
createWrapper({ user: SECURITY_BOT_USER });
|
||||
|
||||
expect(findUserLocalTime().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,15 +3,34 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Entities::Clusters::AgentAuthorization do
|
||||
let_it_be(:authorization) { create(:agent_group_authorization) }
|
||||
|
||||
subject { described_class.new(authorization).as_json }
|
||||
|
||||
it 'includes basic fields' do
|
||||
expect(subject).to include(
|
||||
id: authorization.agent_id,
|
||||
config_project: a_hash_including(id: authorization.agent.project_id),
|
||||
configuration: authorization.config
|
||||
)
|
||||
shared_examples 'generic authorization' do
|
||||
it 'includes shared fields' do
|
||||
expect(subject).to include(
|
||||
id: authorization.agent_id,
|
||||
config_project: a_hash_including(id: authorization.agent.project_id),
|
||||
configuration: authorization.config
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'project authorization' do
|
||||
let(:authorization) { create(:agent_project_authorization) }
|
||||
|
||||
include_examples 'generic authorization'
|
||||
end
|
||||
|
||||
context 'group authorization' do
|
||||
let(:authorization) { create(:agent_group_authorization) }
|
||||
|
||||
include_examples 'generic authorization'
|
||||
end
|
||||
|
||||
context 'implicit authorization' do
|
||||
let(:agent) { create(:cluster_agent) }
|
||||
let(:authorization) { Clusters::Agents::ImplicitAuthorization.new(agent: agent) }
|
||||
|
||||
include_examples 'generic authorization'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,13 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Entities::User do
|
||||
let(:user) { create(:user) }
|
||||
let(:current_user) { create(:user) }
|
||||
let_it_be(:timezone) { 'America/Los_Angeles' }
|
||||
|
||||
subject { described_class.new(user, current_user: current_user).as_json }
|
||||
let(:user) { create(:user, timezone: timezone) }
|
||||
let(:current_user) { create(:user) }
|
||||
let(:entity) { described_class.new(user, current_user: current_user) }
|
||||
|
||||
subject { entity.as_json }
|
||||
|
||||
it 'exposes correct attributes' do
|
||||
expect(subject).to include(:bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title, :work_information, :pronouns)
|
||||
|
@ -35,4 +38,10 @@ RSpec.describe API::Entities::User do
|
|||
expect(subject[:bot]).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'exposes local_time' do
|
||||
local_time = '2:30 PM'
|
||||
expect(entity).to receive(:local_time).with(timezone).and_return(local_time)
|
||||
expect(subject[:local_time]).to eq(local_time)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,4 +7,10 @@ RSpec.describe Clusters::Agents::GroupAuthorization do
|
|||
it { is_expected.to belong_to(:group).class_name('::Group').required }
|
||||
|
||||
it { expect(described_class).to validate_jsonb_schema(['config']) }
|
||||
|
||||
describe '#config_project' do
|
||||
let(:record) { create(:agent_group_authorization) }
|
||||
|
||||
it { expect(record.config_project).to eq(record.agent.project) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,6 @@ RSpec.describe Clusters::Agents::ImplicitAuthorization do
|
|||
|
||||
it { expect(subject.agent).to eq(agent) }
|
||||
it { expect(subject.agent_id).to eq(agent.id) }
|
||||
it { expect(subject.project).to eq(agent.project) }
|
||||
it { expect(subject.config_project).to eq(agent.project) }
|
||||
it { expect(subject.config).to be_nil }
|
||||
end
|
||||
|
|
|
@ -7,4 +7,10 @@ RSpec.describe Clusters::Agents::ProjectAuthorization do
|
|||
it { is_expected.to belong_to(:project).class_name('Project').required }
|
||||
|
||||
it { expect(described_class).to validate_jsonb_schema(['config']) }
|
||||
|
||||
describe '#config_project' do
|
||||
let(:record) { create(:agent_project_authorization) }
|
||||
|
||||
it { expect(record.config_project).to eq(record.agent.project) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,4 +7,19 @@ RSpec.describe Users::CreditCardValidation do
|
|||
|
||||
it { is_expected.to validate_length_of(:holder_name).is_at_most(26) }
|
||||
it { is_expected.to validate_numericality_of(:last_digits).is_less_than_or_equal_to(9999) }
|
||||
|
||||
describe '.similar_records' do
|
||||
let(:card_details) { subject.attributes.slice(:expiration_date, :last_digits, :holder_name) }
|
||||
|
||||
subject(:credit_card_validation) { create(:credit_card_validation) }
|
||||
|
||||
let!(:match1) { create(:credit_card_validation, card_details) }
|
||||
let!(:other1) { create(:credit_card_validation, card_details.merge(last_digits: 9)) }
|
||||
let!(:match2) { create(:credit_card_validation, card_details) }
|
||||
let!(:other2) { create(:credit_card_validation, card_details.merge(holder_name: 'foo bar')) }
|
||||
|
||||
it 'returns records with matching credit card, ordered by credit_card_validated_at' do
|
||||
expect(subject.similar_records).to eq([match2, match1, subject])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue