Commit e64320b1 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'tz-defer-frequent-item-init' into 'master'

Only inits frequent Item Apps when dropdown

See merge request gitlab-org/gitlab!45047
parents 3209f7a8 5e2d483e
......@@ -5,6 +5,14 @@ import getUnicodeSupportMap from './unicode_support_map';
let browserUnicodeSupportMap;
export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) {
// Skipping the map creation for Bots + RSPec
if (
navigator.userAgent.indexOf('HeadlessChrome') > -1 ||
navigator.userAgent.indexOf('Lighthouse') > -1 ||
navigator.userAgent.indexOf('Speedindex') > -1
) {
return true;
}
browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap();
return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion);
}
<script>
import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
import { GlButton, GlModalDirective, GlTabs } from '@gitlab/ui';
import { GlAlert, GlButton, GlModalDirective, GlSprintf, GlTabs } from '@gitlab/ui';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants';
import FeatureFlagsTab from './feature_flags_tab.vue';
import FeatureFlagsTable from './feature_flags_table.vue';
......@@ -9,9 +10,9 @@ import UserListsTable from './user_lists_table.vue';
import { s__ } from '~/locale';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import {
buildUrlWithCurrentLocation,
getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils';
import ConfigureFeatureFlagsModal from './configure_feature_flags_modal.vue';
......@@ -20,13 +21,15 @@ const SCOPES = { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE };
export default {
components: {
ConfigureFeatureFlagsModal,
FeatureFlagsTab,
FeatureFlagsTable,
UserListsTable,
TablePagination,
GlAlert,
GlButton,
GlSprintf,
GlTabs,
FeatureFlagsTab,
ConfigureFeatureFlagsModal,
TablePagination,
UserListsTable,
},
directives: {
GlModal: GlModalDirective,
......@@ -44,6 +47,20 @@ export default {
type: String,
required: true,
},
featureFlagsLimit: {
type: String,
required: true,
},
featureFlagsLimitExceeded: {
type: Boolean,
required: false,
default: false,
},
rotateInstanceIdPath: {
type: String,
required: false,
default: '',
},
unleashApiUrl: {
type: String,
required: true,
......@@ -69,6 +86,7 @@ export default {
scope,
page: getParameterByName('page') || '1',
isUserListAlertDismissed: false,
shouldShowFeatureFlagsLimitWarning: this.featureFlagsLimitExceeded,
selectedTab: Object.values(SCOPES).indexOf(scope),
};
},
......@@ -184,11 +202,36 @@ export default {
dataForScope(scope) {
return this[scope];
},
onDismissFeatureFlagsLimitWarning() {
this.shouldShowFeatureFlagsLimitWarning = false;
},
onNewFeatureFlagCLick() {
if (this.featureFlagsLimitExceeded) {
this.shouldShowFeatureFlagsLimitWarning = true;
}
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="shouldShowFeatureFlagsLimitWarning"
variant="warning"
@dismiss="onDismissFeatureFlagsLimitWarning"
>
<gl-sprintf
:message="
s__(
'FeatureFlags|Feature flags limit reached (%{featureFlagsLimit}). Delete one or more feature flags before adding new ones.',
)
"
>
<template #featureFlagsLimit>
<span>{{ featureFlagsLimit }}</span>
</template>
</gl-sprintf>
</gl-alert>
<configure-feature-flags-modal
v-if="canUserConfigure"
:help-client-libraries-path="featureFlagsClientLibrariesHelpPagePath"
......@@ -228,9 +271,10 @@ export default {
<gl-button
v-if="hasNewPath"
:href="newFeatureFlagPath"
:href="featureFlagsLimitExceeded ? '' : newFeatureFlagPath"
variant="success"
data-testid="ff-new-button"
@click="onNewFeatureFlagCLick"
>
{{ s__('FeatureFlags|New feature flag') }}
</gl-button>
......@@ -306,9 +350,10 @@ export default {
<gl-button
v-if="hasNewPath"
:href="newFeatureFlagPath"
:href="featureFlagsLimitExceeded ? '' : newFeatureFlagPath"
variant="success"
data-testid="ff-new-button"
@click="onNewFeatureFlagCLick"
>
{{ s__('FeatureFlags|New feature flag') }}
</gl-button>
......
......@@ -36,6 +36,8 @@ export default () => {
el.dataset.featureFlagsClientLibrariesHelpPagePath,
featureFlagsClientExampleHelpPagePath: el.dataset.featureFlagsClientExampleHelpPagePath,
unleashApiUrl: el.dataset.unleashApiUrl,
featureFlagsLimitExceeded: el.dataset.featureFlagsLimitExceeded,
featureFlagsLimit: el.dataset.featureFlagsLimit,
csrfToken: csrf.token,
canUserConfigure: el.dataset.canUserAdminFeatureFlag,
newFeatureFlagPath: el.dataset.newFeatureFlagPath,
......
......@@ -5,6 +5,7 @@ import { formatDate } from '~/lib/utils/datetime_utility';
export default {
components: {
GlLink,
IncidentSla: () => import('ee_component/issue_show/components/incidents/incident_sla.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -12,36 +13,51 @@ export default {
props: {
alert: {
type: Object,
required: true,
required: false,
default: null,
},
},
data() {
return { childHasData: false };
},
computed: {
startTime() {
return formatDate(this.alert.startedAt, 'yyyy-mm-dd Z');
},
showHighlightBar() {
return this.alert || this.childHasData;
},
},
methods: {
update(hasData) {
this.childHasData = hasData;
},
},
};
</script>
<template>
<div
v-show="showHighlightBar"
class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
>
<div class="gl-pr-3">
<div v-if="alert" class="gl-mr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
<gl-link v-gl-tooltip :title="alert.title" :href="alert.detailsUrl">
#{{ alert.iid }}
</gl-link>
</div>
<div class="gl-pr-3">
<div v-if="alert" class="gl-mr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert start time:') }}</span>
{{ startTime }}
</div>
<div>
<div v-if="alert" class="gl-mr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span>
<span>{{ alert.eventCount }}</span>
</div>
<incident-sla @update="update" />
</div>
</template>
......@@ -53,7 +53,7 @@ export default {
<div>
<gl-tabs content-class="gl-reset-line-height" class="gl-mt-n3" data-testid="incident-tabs">
<gl-tab :title="s__('Incident|Summary')">
<highlight-bar v-if="alert" :alert="alert" />
<highlight-bar :alert="alert" />
<description-component v-bind="$attrs" />
</gl-tab>
<gl-tab v-if="alert" class="alert-management-details" :title="s__('Incident|Alert details')">
......
......@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import issuableApp from './components/app.vue';
import incidentTabs from './components/incidents/incident_tabs.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(VueApollo);
......@@ -11,7 +12,7 @@ export default function initIssuableApp(issuableData = {}) {
defaultClient: createDefaultClient(),
});
const { projectNamespace, projectPath, iid } = issuableData;
const { iid, projectNamespace, projectPath, slaFeatureAvailable } = issuableData;
return new Vue({
el: document.getElementById('js-issuable-app'),
......@@ -22,6 +23,7 @@ export default function initIssuableApp(issuableData = {}) {
provide: {
fullPath: `${projectNamespace}/${projectPath}`,
iid,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
},
render(createElement) {
return createElement('issuable-app', {
......
......@@ -11,3 +11,5 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
issue.subscribed?(current_user, issue.project)
end
end
IssuePresenter.prepend_if_ee('EE::IssuePresenter')
......@@ -7,6 +7,8 @@
"feature-flags-help-page-path" => help_page_path("operations/feature_flags"),
"feature-flags-client-libraries-help-page-path" => help_page_path("operations/feature_flags", anchor: "choose-a-client-library"),
"feature-flags-client-example-help-page-path" => help_page_path("operations/feature_flags", anchor: "golang-application-example"),
"feature-flags-limit-exceeded" => @project.actual_limits.exceeded?(:project_feature_flags, @project.operations_feature_flags.count),
"feature-flags-limit" => @project.actual_limits.project_feature_flags,
"unleash-api-url" => (unleash_api_url(@project) if can?(current_user, :admin_feature_flag, @project)),
"unleash-api-instance-id" => (unleash_api_instance_id(@project) if can?(current_user, :admin_feature_flag, @project)),
"can-user-admin-feature-flag" => can?(current_user, :admin_feature_flag, @project),
......
---
title: Add Issuable Service Level Agreement (SLA) table
merge_request: 44253
author:
type: added
---
title: Feature Flags limits UX and documentation
merge_request: 44089
author:
type: added
# frozen_string_literal: true
class AddIssuableSlaTable < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :issuable_slas do |t|
t.references :issue, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
t.datetime_with_timezone :due_at, null: false
end
end
end
8a12c3c4f674d2a36df56a89bfd32e0f3945e73605460bdf2a8b0aa1308f5b19
\ No newline at end of file
......@@ -12739,6 +12739,21 @@ CREATE SEQUENCE issuable_severities_id_seq
ALTER SEQUENCE issuable_severities_id_seq OWNED BY issuable_severities.id;
CREATE TABLE issuable_slas (
id bigint NOT NULL,
issue_id bigint NOT NULL,
due_at timestamp with time zone NOT NULL
);
CREATE SEQUENCE issuable_slas_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE issuable_slas_id_seq OWNED BY issuable_slas.id;
CREATE TABLE issue_assignees (
user_id integer NOT NULL,
issue_id integer NOT NULL
......@@ -17561,6 +17576,8 @@ ALTER TABLE ONLY ip_restrictions ALTER COLUMN id SET DEFAULT nextval('ip_restric
ALTER TABLE ONLY issuable_severities ALTER COLUMN id SET DEFAULT nextval('issuable_severities_id_seq'::regclass);
ALTER TABLE ONLY issuable_slas ALTER COLUMN id SET DEFAULT nextval('issuable_slas_id_seq'::regclass);
ALTER TABLE ONLY issue_email_participants ALTER COLUMN id SET DEFAULT nextval('issue_email_participants_id_seq'::regclass);
ALTER TABLE ONLY issue_links ALTER COLUMN id SET DEFAULT nextval('issue_links_id_seq'::regclass);
......@@ -18689,6 +18706,9 @@ ALTER TABLE ONLY ip_restrictions
ALTER TABLE ONLY issuable_severities
ADD CONSTRAINT issuable_severities_pkey PRIMARY KEY (id);
ALTER TABLE ONLY issuable_slas
ADD CONSTRAINT issuable_slas_pkey PRIMARY KEY (id);
ALTER TABLE ONLY issue_email_participants
ADD CONSTRAINT issue_email_participants_pkey PRIMARY KEY (id);
......@@ -20485,6 +20505,8 @@ CREATE INDEX index_ip_restrictions_on_group_id ON ip_restrictions USING btree (g
CREATE UNIQUE INDEX index_issuable_severities_on_issue_id ON issuable_severities USING btree (issue_id);
CREATE UNIQUE INDEX index_issuable_slas_on_issue_id ON issuable_slas USING btree (issue_id);
CREATE UNIQUE INDEX index_issue_assignees_on_issue_id_and_user_id ON issue_assignees USING btree (issue_id, user_id);
CREATE INDEX index_issue_assignees_on_user_id ON issue_assignees USING btree (user_id);
......@@ -20541,8 +20563,6 @@ CREATE INDEX index_issues_on_updated_at ON issues USING btree (updated_at);
CREATE INDEX index_issues_on_updated_by_id ON issues USING btree (updated_by_id) WHERE (updated_by_id IS NOT NULL);
CREATE INDEX index_issues_project_id_issue_type_incident ON issues USING btree (project_id) WHERE (issue_type = 1);
CREATE UNIQUE INDEX index_jira_connect_installations_on_client_key ON jira_connect_installations USING btree (client_key);
CREATE INDEX index_jira_connect_subscriptions_on_namespace_id ON jira_connect_subscriptions USING btree (namespace_id);
......@@ -22852,6 +22872,9 @@ ALTER TABLE ONLY gpg_signatures
ALTER TABLE ONLY vulnerability_user_mentions
ADD CONSTRAINT fk_rails_1a41c485cd FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE;
ALTER TABLE ONLY issuable_slas
ADD CONSTRAINT fk_rails_1b8768cd63 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
ALTER TABLE ONLY board_assignees
ADD CONSTRAINT fk_rails_1c0ff59e82 FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE CASCADE;
......
......@@ -23,8 +23,8 @@ GET /projects/:id/registry/repositories
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) accessible by the authenticated user. |
| `tags` | boolean | no | If the parameter is included as true, each repository will include an array of `"tags"` in the response. |
| `tags_count` | boolean | no | If the parameter is included as true, each repository will include `"tags_count"` in the response ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/32141) in GitLab 13.1). |
| `tags` | boolean | no | If the parameter is included as true, each repository includes an array of `"tags"` in the response. |
| `tags_count` | boolean | no | If the parameter is included as true, each repository includes `"tags_count"` in the response ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/32141) in GitLab 13.1). |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories"
......@@ -66,8 +66,8 @@ GET /groups/:id/registry/repositories
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) accessible by the authenticated user. |
| `tags` | boolean | no | If the parameter is included as true, each repository will include an array of `"tags"` in the response. |
| `tags_count` | boolean | no | If the parameter is included as true, each repository will include `"tags_count"` in the response ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/32141) in GitLab 13.1). |
| `tags` | boolean | no | If the parameter is included as true, each repository includes an array of `"tags"` in the response. |
| `tags_count` | boolean | no | If the parameter is included as true, each repository includes `"tags_count"` in the response ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/32141) in GitLab 13.1). |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/2/registry/repositories?tags=1&tags_count=true"
......@@ -250,7 +250,7 @@ DELETE /projects/:id/registry/repositories/:repository_id/tags
| `repository_id` | integer | yes | The ID of registry repository. |
| `name_regex` | string | no | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to delete. To delete all tags specify `.*`. **Note:** `name_regex` is deprecated in favor of `name_regex_delete`. This field is validated. |
| `name_regex_delete` | string | yes | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to delete. To delete all tags specify `.*`. This field is validated. |
| `name_regex_keep` | string | no | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to keep. This value will override any matches from `name_regex_delete`. This field is validated. Note: setting to `.*` will result in a no-op. |
| `name_regex_keep` | string | no | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to keep. This value overrides any matches from `name_regex_delete`. This field is validated. Note: setting to `.*` results in a no-op. |
| `keep_n` | integer | no | The amount of latest tags of given name to keep. |
| `older_than` | string | no | Tags to delete that are older than the given time, written in human readable form `1h`, `1d`, `1month`. |
......
......@@ -6725,6 +6725,11 @@ type EpicIssue implements CurrentUserTodos & Noteable {
"""
severity: IssuableSeverity
"""
Timestamp of when the issue SLA expires. Returns null if `incident_sla_dev` feature flag is disabled.
"""
slaDueAt: Time
"""
State of the issue
"""
......@@ -8880,6 +8885,11 @@ type Issue implements CurrentUserTodos & Noteable {
"""
severity: IssuableSeverity
"""
Timestamp of when the issue SLA expires. Returns null if `incident_sla_dev` feature flag is disabled.
"""
slaDueAt: Time
"""
State of the issue
"""
......
......@@ -18532,6 +18532,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "slaDueAt",
"description": "Timestamp of when the issue SLA expires. Returns null if `incident_sla_dev` feature flag is disabled.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state",
"description": "State of the issue",
......@@ -24220,6 +24234,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "slaDueAt",
"description": "Timestamp of when the issue SLA expires. Returns null if `incident_sla_dev` feature flag is disabled.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state",
"description": "State of the issue",
......@@ -1066,6 +1066,7 @@ Relationship between an epic and an issue.
| `relationPath` | String | URI path of the epic-issue relation |
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
| `severity` | IssuableSeverity | Severity level of the incident |
| `slaDueAt` | Time | Timestamp of when the issue SLA expires. Returns null if `incident_sla_dev` feature flag is disabled. |
| `state` | IssueState! | State of the issue |
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page |
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the issue |
......@@ -1256,6 +1257,7 @@ Represents a recorded measurement (object count) for the Admins.
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
| `severity` | IssuableSeverity | Severity level of the incident |
| `slaDueAt` | Time | Timestamp of when the issue SLA expires. Returns null if `incident_sla_dev` feature flag is disabled. |
| `state` | IssueState! | State of the issue |
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page |
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the issue |
......
......@@ -56,6 +56,20 @@ To create and enable a feature flag:
You can change these settings by clicking the **{pencil}** (edit) button
next to any feature flag in the list.
## Maximum number of feature flags
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/254379) in GitLab 13.5.
The maximum number of feature flags per project on self-managed GitLab instances
is 200. On GitLab.com, the maximum number is determined by [GitLab.com tier](https://about.gitlab.com/pricing/):
| Tier | Number of feature flags per project |
|----------|-------------------------------------|
| Free | 50 |
| Bronze | 100 |
| Silver | 150 |
| Gold | 200 |
## Feature flag strategies
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35555) in GitLab 13.0.
......
......@@ -253,20 +253,24 @@ production: &base
## Upgrade your subscription tier
To upgrade your [GitLab tier](https://about.gitlab.com/pricing/), contact our sales team as this
can't be done in the Customers Portal. You can either send an email to `renewals@gitlab.com`, or
complete the [**Contact Sales**](https://about.gitlab.com/sales/) form. Include details of which subscription you want to upgrade and the desired tier in your message.
To upgrade your [GitLab tier](https://about.gitlab.com/pricing/):
After messaging the sales team, the workflow is as follows:
1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
1. Select the **Upgrade** button on the relevant subscription card on the
[Manage purchases](https://customers.gitlab.com/subscriptions) page.
1. Select the desired upgrade.
1. Confirm the active form of payment, or add a new form of payment.
1. Select the **I accept the Privacy Policy and Terms of Service** checkbox.
1. Select **Purchase**.
1. Receive a reply from the sales team, asking for confirmation of the upgrade.
1. Reply to the sales team, confirming details of the upgrade.
1. Receive a quote from the sales team.
1. Sign and return the quote.
1. Receive the new license.
1. Upload the new license. For details, see [Uploading your license](../../user/admin_area/license.md#uploading-your-license).
The following is emailed to you:
The new subscription tier is active when the license file is uploaded.
- A payment receipt. You can also access this information in the Customers Portal under
[**View invoices**](https://customers.gitlab.com/receipts).
- A new license.
[Upload the new license](../../user/admin_area/license.md#uploading-your-license) to your instance.
The new tier takes effect when the new license is uploaded.
## Subscription expiry
......
......@@ -223,6 +223,14 @@ in an offline environment if you prefer using only locally available Docker imag
recommend keeping the pull policy setting to `always` if not in an offline environment, as this
enables the use of updated scanners in your CI/CD pipelines.
##### Support for Custom Certificate Authorities
Support for custom certificate authorities was introduced in the following versions:
| Analyzer | Version |
| -------- | ------- |
| `klar` | [v2.3.0](https://gitlab.com/gitlab-org/security-products/analyzers/klar/-/releases/v2.3.0) |
#### Make GitLab container scanning analyzer images available inside your Docker registry
For container scanning, import the following default images from `registry.gitlab.com` into your
......
......@@ -392,6 +392,18 @@ For details on saving and transporting Docker images as a file, see Docker's doc
[`docker save`](https://docs.docker.com/engine/reference/commandline/save/), [`docker load`](https://docs.docker.com/engine/reference/commandline/load/),
[`docker export`](https://docs.docker.com/engine/reference/commandline/export/), and [`docker import`](https://docs.docker.com/engine/reference/commandline/import/).
#### Support for Custom Certificate Authorities
Support for custom certificate authorities was introduced in the following versions.
| Analyzer | Version |
| -------- | ------- |
| `gemnasium` | [v2.8.0](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/releases/v2.8.0) |
| `gemnasium-maven` | [v2.9.0](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/releases/v2.9.0) |
| `gemnasium-python` | [v2.7.0](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/releases/v2.7.0) |
| `retire.js` | [v2.4.0](https://gitlab.com/gitlab-org/security-products/analyzers/retire.js/-/releases/v2.4.0) |
| `bundler-audit` | [v2.4.0](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit/-/releases/v2.4.0) |
### Set dependency scanning CI job variables to use local dependency scanning analyzers
Add the following configuration to your `.gitlab-ci.yml` file. You must change the value of
......
......@@ -519,6 +519,25 @@ For details on saving and transporting Docker images as a file, see Docker's doc
[`docker save`](https://docs.docker.com/engine/reference/commandline/save/), [`docker load`](https://docs.docker.com/engine/reference/commandline/load/),
[`docker export`](https://docs.docker.com/engine/reference/commandline/export/), and [`docker import`](https://docs.docker.com/engine/reference/commandline/import/).
#### If support for Custom Certificate Authorities are needed
Support for custom certificate authorities was introduced in the following versions.
| Analyzer | Version |
| -------- | ------- |
| `bandit` | [v2.3.0](https://gitlab.com/gitlab-org/security-products/analyzers/bandit/-/releases/v2.3.0) |
| `brakeman` | [v2.1.0](https://gitlab.com/gitlab-org/security-products/analyzers/brakeman/-/releases/v2.1.0) |
| `eslint` | [v2.9.2](https://gitlab.com/gitlab-org/security-products/analyzers/eslint/-/releases/v2.9.2) |
| `flawfinder` | [v2.3.0](https://gitlab.com/gitlab-org/security-products/analyzers/flawfinder/-/releases/v2.3.0) |
| `gosec` | [v2.5.0](https://gitlab.com/gitlab-org/security-products/analyzers/gosec/-/releases/v2.5.0) |
| `kubesec` | [v2.1.0](https://gitlab.com/gitlab-org/security-products/analyzers/kubesec/-/releases/v2.1.0) |
| `nodejs-scan` | [v2.9.5](https://gitlab.com/gitlab-org/security-products/analyzers/nodejs-scan/-/releases/v2.9.5) |
| `phpcs-security-audit` | [v2.8.2](https://gitlab.com/gitlab-org/security-products/analyzers/phpcs-security-audit/-/releases/v2.8.2) |
| `pmd-apex` | [v2.1.0](https://gitlab.com/gitlab-org/security-products/analyzers/pmd-apex/-/releases/v2.1.0) |
| `security-code-scan` | [v2.7.3](https://gitlab.com/gitlab-org/security-products/analyzers/security-code-scan/-/releases/v2.7.3) |
| `sobelow` | [v2.2.0](https://gitlab.com/gitlab-org/security-products/analyzers/sobelow/-/releases/v2.2.0) |
| `spotbugs` | [v2.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs/-/releases/v2.7.1) |
### Set SAST CI job variables to use local SAST analyzers
Add the following configuration to your `.gitlab-ci.yml` file. You must replace
......
......@@ -173,6 +173,32 @@ We have created a [short video walkthrough](https://youtu.be/wDtc_K00Y0A) showca
<iframe src="https://www.youtube.com/embed/wDtc_K00Y0A" frameborder="0" allowfullscreen="true"> </iframe>
</figure>
### Make GitLab Secret Detection analyzer image available inside your Docker registry
Import the following default Secret Detection analyzer images from `registry.gitlab.com` into your
[local Docker container registry](../../packages/container_registry/index.md):
```plaintext
registry.gitlab.com/gitlab-org/security-products/analyzers/secrets:3
```
The process for importing Docker images into a local offline Docker registry depends on
**your network security policy**. Please consult your IT staff to find an accepted and approved
process by which external resources can be imported or temporarily accessed. Note that these scanners are [updated periodically](../index.md#maintenance-and-update-of-the-vulnerabilities-database)
with new definitions, so consider if you're able to make periodic updates yourself.
For details on saving and transporting Docker images as a file, see Docker's documentation on
[`docker save`](https://docs.docker.com/engine/reference/commandline/save/), [`docker load`](https://docs.docker.com/engine/reference/commandline/load/),
[`docker export`](https://docs.docker.com/engine/reference/commandline/export/), and [`docker import`](https://docs.docker.com/engine/reference/commandline/import/).
#### If support for Custom Certificate Authorities are needed
Support for custom certificate authorities was introduced in the following versions.
| Analyzer | Version |
| -------- | ------- |
| secrets | [v3.0.0](https://gitlab.com/gitlab-org/security-products/analyzers/secrets/-/releases/v3.0.0) |
## Troubleshooting
### Getting warning message `gl-secret-detection-report.json: no matching files`
......
query getSlaDueAt($iid: String!, $fullPath: ID!) {
project(fullPath: $fullPath) {
issue(iid: $iid) {
slaDueAt
}
}
}
<script>
import { GlIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { formatTime, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import getSlaDueAt from './graphql/queries/get_sla_due_at.graphql';
export default {
components: { GlIcon, TimeAgoTooltip },
inject: ['fullPath', 'iid', 'slaFeatureAvailable'],
apollo: {
slaDueAt: {
query: getSlaDueAt,
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
update(data) {
return data?.project?.issue?.slaDueAt;
},
result({ data } = {}) {
const slaDueAt = data?.project?.issue?.slaDueAt;
this.$emit('update', Boolean(slaDueAt));
},
error() {
createFlash({
message: s__('Incident|There was an issue loading incident data. Please try again.'),
});
},
},
},
data() {
return {
slaDueAt: null,
};
},
computed: {
displayValue() {
const time = formatTime(calculateRemainingMilliseconds(this.slaDueAt));
// remove the seconds portion of the string
return time.substring(0, time.length - 3);
},
},
};
</script>
<template>
<div v-if="slaFeatureAvailable && slaDueAt">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Time to SLA:') }}</span>
<span class="gl-white-space-nowrap">
<gl-icon name="timer" />
<time-ago-tooltip :time="slaDueAt">
{{ displayValue }}
</time-ago-tooltip>
</span>
</div>
</template>
......@@ -21,7 +21,7 @@ module EE
end
def sla_feature_available?
::Feature.enabled?(:incident_sla_dev, @project) && @project.feature_available?(:incident_sla, current_user)
::IncidentManagement::IncidentSla.available_for?(@project)
end
def track_tracing_external_url
......
......@@ -4,12 +4,24 @@ module EE
module Resolvers
module IssuesResolver
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
argument :iteration_id, ::GraphQL::ID_TYPE.to_list_type,
required: false,
description: 'Iterations applied to the issue'
end
private
override :preloads
def preloads
super.merge(
{
sla_due_at: [:issuable_sla]
}
)
end
end
end
end
......@@ -23,14 +23,15 @@ module EE
::Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate.new(ctx, obj.id)
}
field :health_status,
::Types::HealthStatusEnum,
null: true,
field :health_status, ::Types::HealthStatusEnum, null: true,
description: 'Current health status. Returns null if `save_issuable_health_status` feature flag is disabled.',
resolve: -> (obj, _, _) { obj.supports_health_status? ? obj.health_status : nil }
field :status_page_published_incident, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates whether an issue is published to the status page'
field :sla_due_at, ::Types::TimeType, null: true,
description: 'Timestamp of when the issue SLA expires. Returns null if `incident_sla_dev` feature flag is disabled.'
end
end
end
......
......@@ -34,7 +34,8 @@ module EE
return {} unless issuable.is_a?(Issue)
super.merge(
publishedIncidentUrl: ::Gitlab::StatusPage::Storage.details_url(issuable)
publishedIncidentUrl: ::Gitlab::StatusPage::Storage.details_url(issuable),
slaFeatureAvailable: issuable.sla_available?.to_s
)
end
......
......@@ -58,7 +58,7 @@ module EE
end
def sla_feature_available?
::Feature.enabled?(:incident_sla_dev, @project) && @project.feature_available?(:incident_sla, current_user)
::IncidentManagement::IncidentSla.available_for?(@project)
end
def opsgenie_mvc_data
......
......@@ -20,5 +20,15 @@ module EE
def weight_available?
supports_weight? && project&.feature_available?(:issue_weights)
end
def sla_available?
return false unless ::IncidentManagement::IncidentSla.available_for?(project)
supports_sla?
end
def supports_sla?
incident?
end
end
end
......@@ -49,6 +49,7 @@ module EE
belongs_to :promoted_to_epic, class_name: 'Epic'
has_one :status_page_published_incident, class_name: 'StatusPage::PublishedIncident', inverse_of: :issue
has_one :issuable_sla
has_many :vulnerability_links, class_name: 'Vulnerabilities::IssueLink', inverse_of: :issue
has_many :related_vulnerabilities, through: :vulnerability_links, source: :vulnerability
......
# frozen_string_literal: true
class IssuableSla < ApplicationRecord
belongs_to :issue, optional: false
validates :due_at, presence: true
end
# frozen_string_literal: true
module EE
module IssuePresenter
extend ActiveSupport::Concern
def sla_due_at
return unless sla_available?
issuable_sla&.due_at
end
end
end
......@@ -20,6 +20,21 @@ module EE
end
end
end
override :after_create
def after_create(issue)
super
add_issue_sla(issue)
end
private
def add_issue_sla(issue)
return unless issue.sla_available?
::IncidentManagement::Incidents::CreateSlaService.new(issue, current_user).execute
end
end
end
end
# frozen_string_literal: true
module IncidentManagement
module Incidents
class CreateSlaService < BaseService
def initialize(issuable, current_user)
super(issuable.project, current_user)
@issuable = issuable
end
def execute
return not_enabled_success unless issuable.sla_available?
return not_enabled_success unless incident_setting&.sla_timer?
sla = issuable.build_issuable_sla(
due_at: issuable.created_at + incident_setting.sla_timer_minutes.minutes
)
return success(sla: sla) if sla.save
error(sla.errors&.full_messages)
end
attr_reader :issuable
private
def not_enabled_success
ServiceResponse.success(message: 'SLA not enabled')
end
def success(payload)
ServiceResponse.success(payload: payload)
end
def error(message)
ServiceResponse.error(message: message)
end
def incident_setting
@incident_setting ||= project.incident_management_setting
end
end
end
end
# frozen_string_literal: true
module IncidentManagement
module IncidentSla
class << self
def available_for?(project)
::Feature.enabled?(:incident_sla_dev, project) && project.feature_available?(:incident_sla)
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :issuable_sla do
issue
due_at { 1.hour.from_now }
end
end
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import IncidentSla from 'ee/issue_show/components/incidents/incident_sla.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatTime } from '~/lib/utils/datetime_utility';
jest.mock('~/lib/utils/datetime_utility');
const defaultProvide = { fullPath: 'test', iid: 1, slaFeatureAvailable: true };
describe('Incident SLA', () => {
let wrapper;
const mountComponent = options => {
wrapper = shallowMount(
IncidentSla,
merge(
{
data() {
return { slaDueAt: '2020-01-01T00:00:00.000Z' };
},
provide: { ...defaultProvide },
},
options,
),
);
};
beforeEach(() => {
formatTime.mockImplementation(() => '12:34:56');
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
const findTimer = () => wrapper.find(TimeAgoTooltip);
it('does not render an SLA when no sla is present', () => {
mountComponent({
data() {
return { slaDueAt: null };
},
});
expect(findTimer().exists()).toBe(false);
});
it('renders an incident SLA when sla is present', () => {
mountComponent();
expect(findTimer().text()).toBe('12:34');
});
it('renders a component when feature is available', () => {
mountComponent();
expect(wrapper.exists()).toBe(true);
});
it('renders a blank component when feature is not available', () => {
mountComponent({
provide: {
...defaultProvide,
slaFeatureAvailable: false,
},
});
expect(wrapper.html()).toBe('');
});
});
......@@ -4,14 +4,11 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Issue'] do
it { expect(described_class).to have_graphql_field(:epic) }
it { expect(described_class).to have_graphql_field(:iteration) }
it { expect(described_class).to have_graphql_field(:weight) }
it { expect(described_class).to have_graphql_field(:health_status) }
it { expect(described_class).to have_graphql_field(:blocked) }
it { expect(described_class).to have_graphql_field(:sla_due_at) }
context 'N+1 queries' do
let_it_be(:user) { create(:user) }
......
......@@ -6,7 +6,7 @@ RSpec.describe EE::IncidentManagement::ProjectIncidentManagementSetting do
let_it_be(:project) { create(:project, :repository, create_templates: :issue) }
describe 'Validations' do
describe 'validate incident SLA settings' do
describe 'validate SLA settings' do
subject { build(:project_incident_management_setting, sla_timer: sla_timer) }
describe '#sla_timer_minutes' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssuableSla do
describe 'associations' do
it { is_expected.to belong_to(:issue).required }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:due_at) }
end
end
......@@ -12,6 +12,7 @@ RSpec.describe Issue do
it { is_expected.to have_many(:resource_weight_events) }
it { is_expected.to have_many(:resource_iteration_events) }
it { is_expected.to have_one(:issuable_sla) }
end
describe 'modules' do
......@@ -802,4 +803,31 @@ RSpec.describe Issue do
expect(incident.issue_type_supports?(:epics)).to be(false)
end
end
describe '#sla_available?' do
let_it_be(:project) { create(:project) }
let_it_be_with_refind(:issue) { create(:incident, project: project) }
subject { issue.sla_available? }
where(:feature_enabled, :incident_type, :license_available, :sla_available) do
false | true | true | false
true | false | true | false
true | true | false | false
true | true | true | true
end
with_them do
before do
stub_feature_flags(incident_sla_dev: feature_enabled)
stub_licensed_features(incident_sla: license_available)
issue_type = incident_type ? 'incident' : 'issue'
issue.update(issue_type: issue_type)
end
it 'returns the expected value' do
expect(subject).to eq(sla_available)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssuePresenter do
describe '#sla_due_at' do
let_it_be(:incident) { create(:incident) }
let_it_be(:issuable_sla) { create(:issuable_sla, issue: incident) }
subject { described_class.new(incident).present.sla_due_at }
before do
allow(incident).to receive(:sla_available?).and_return(available)
end
context 'issue sla available' do
let(:available) { true }
it { is_expected.to eq(issuable_sla.due_at) }
end
context 'issue sla not available' do
let(:available) { false }
it { is_expected.to eq(nil) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::Incidents::CreateSlaService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be_with_refind(:incident) { create(:incident, project: project) }
describe '#execute' do
subject(:create_issuable_sla_response) { described_class.new(incident, user).execute }
let(:response_payload_sla) { create_issuable_sla_response.payload[:sla] }
let(:response_payload_message) { create_issuable_sla_response.message }
before_all do
project.add_maintainer(user)
end
before do
stub_licensed_features(incident_sla: true)
end
shared_examples 'no issuable sla created' do
it 'does not create the issuable sla' do
expect { subject }.not_to change(IssuableSla, :count)
end
it 'does not return a sla' do
expect(response_payload_sla).to eq(nil)
end
end
context 'incident setting not created' do
it_behaves_like 'no issuable sla created'
end
context 'incident setting exists' do
let(:sla_timer) { true }
let(:sla_timer_minutes) { 30 }
let!(:setting) { create(:project_incident_management_setting, project: project, sla_timer: sla_timer, sla_timer_minutes: sla_timer_minutes) }
context 'project does not have incident_sla feature' do
before do
stub_licensed_features(incident_sla: false)
end
it_behaves_like 'no issuable sla created'
end
context 'sla timer setting is disabled' do
let(:sla_timer) { false }
it_behaves_like 'no issuable sla created'
end
it 'creates the issuable sla with the given offset', :aggregate_failures do
expect { subject }.to change(IssuableSla, :count)
offset_time = incident.created_at + setting.sla_timer_minutes.minutes
expect(response_payload_sla.due_at).to eq(offset_time)
end
it 'returns a success with the sla', :aggregate_failures do
expect(create_issuable_sla_response.success?).to eq(true)
expect(response_payload_sla).to be_a(IssuableSla)
end
context 'errors when saving' do
before do
allow_next_instance_of(IssuableSla) do |issuable_sla|
allow(issuable_sla).to receive(:save).and_return(false)
errors = ActiveModel::Errors.new(issuable_sla).tap { |e| e.add(:issue_id, 'error message') }
allow(issuable_sla).to receive(:errors).and_return(errors)
end
end
it 'does not create the issuable sla' do
expect { subject }.not_to change(IssuableSla, :count)
end
it 'returns an error', :aggregate_failures do
expect(create_issuable_sla_response.error?).to eq(true)
expect(response_payload_message).to include('Issue error message')
end
end
end
end
end
......@@ -403,9 +403,15 @@ ee:
- issues:
- epic_issue:
- :epic
- :issuable_sla
- protected_branches:
- :unprotect_access_levels
- protected_environments:
- :deploy_access_levels
- :service_desk_setting
- :security_setting
included_attributes:
issuable_sla:
- :issue
- :due_at
......@@ -11152,6 +11152,9 @@ msgstr ""
msgid "FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality."
msgstr ""
msgid "FeatureFlags|Feature flags limit reached (%{featureFlagsLimit}). Delete one or more feature flags before adding new ones."
msgstr ""
msgid "FeatureFlags|Flag becomes read only soon"
msgstr ""
......@@ -13280,6 +13283,9 @@ msgstr ""
msgid "HighlightBar|Original alert:"
msgstr ""
msgid "HighlightBar|Time to SLA:"
msgstr ""
msgid "History"
msgstr ""
......@@ -13836,6 +13842,9 @@ msgstr ""
msgid "Incident|There was an issue loading alert data. Please try again."
msgstr ""
msgid "Incident|There was an issue loading incident data. Please try again."
msgstr ""
msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import MockAdapter from 'axios-mock-adapter';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { GlAlert, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
import createStore from '~/feature_flags/store/index';
......@@ -20,14 +20,17 @@ localVue.use(Vuex);
describe('Feature flags', () => {
const mockData = {
canUserConfigure: true,
// canUserRotateToken: true,
csrfToken: 'testToken',
featureFlagsClientLibrariesHelpPagePath: '/help/feature-flags#unleash-clients',
featureFlagsClientExampleHelpPagePath: '/help/feature-flags#client-example',
unleashApiUrl: `${TEST_HOST}/api/unleash`,
canUserConfigure: true,
canUserRotateToken: true,
featureFlagsClientLibrariesHelpPagePath: '/help/feature-flags#unleash-clients',
featureFlagsHelpPagePath: '/help/feature-flags',
featureFlagsLimit: '200',
featureFlagsLimitExceeded: false,
newFeatureFlagPath: 'feature-flags/new',
newUserListPath: '/user-list/new',
unleashApiUrl: `${TEST_HOST}/api/unleash`,
};
const mockState = {
......@@ -60,6 +63,7 @@ describe('Feature flags', () => {
const configureButton = () => wrapper.find('[data-testid="ff-configure-button"]');
const newButton = () => wrapper.find('[data-testid="ff-new-button"]');
const newUserListButton = () => wrapper.find('[data-testid="ff-new-list-button"]');
const limitAlert = () => wrapper.find(GlAlert);
beforeEach(() => {
mock = new MockAdapter(axios);
......@@ -82,28 +86,64 @@ describe('Feature flags', () => {
wrapper = null;
});
describe('when limit exceeded', () => {
const propsData = { ...mockData, featureFlagsLimitExceeded: true };
beforeEach(done => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
.reply(200, getRequestData, {});
factory(propsData);
setImmediate(done);
});
it('makes the new feature flag button do nothing if clicked', () => {
expect(newButton().exists()).toBe(true);
expect(newButton().props('disabled')).toBe(false);
expect(newButton().props('href')).toBe(undefined);
});
it('shows a feature flags limit reached alert', () => {
expect(limitAlert().exists()).toBe(true);
expect(
limitAlert()
.find(GlSprintf)
.attributes('message'),
).toContain('Feature flags limit reached');
});
describe('when the alert is dismissed', () => {
beforeEach(async () => {
await limitAlert().vm.$emit('dismiss');
});
it('hides the alert', async () => {
expect(limitAlert().exists()).toBe(false);
});
it('re-shows the alert if the new feature flag button is clicked', async () => {
await newButton().vm.$emit('click');
expect(limitAlert().exists()).toBe(true);
});
});
});
describe('without permissions', () => {
const propsData = {
csrfToken: 'testToken',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags',
...mockData,
canUserConfigure: false,
canUserRotateToken: false,
featureFlagsClientLibrariesHelpPagePath: '/help/feature-flags#unleash-clients',
featureFlagsClientExampleHelpPagePath: '/help/feature-flags#client-example',
unleashApiUrl: `${TEST_HOST}/api/unleash`,
newFeatureFlagPath: null,
newUserListPath: null,
};
beforeEach(done => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
.reply(200, getRequestData, {});
factory(propsData);
setImmediate(() => {
done();
});
setImmediate(done);
});
it('does not render configure button', () => {
......@@ -197,9 +237,7 @@ describe('Feature flags', () => {
factory();
jest.spyOn(store, 'dispatch');
setImmediate(() => {
done();
});
setImmediate(done);
});
it('should render a table with feature flags', () => {
......@@ -267,10 +305,7 @@ describe('Feature flags', () => {
describe('in user lists tab', () => {
beforeEach(done => {
factory();
setImmediate(() => {
done();
});
setImmediate(done);
});
beforeEach(() => {
wrapper.find('[data-testid="user-lists-tab"]').vm.$emit('changeTab');
......@@ -295,10 +330,7 @@ describe('Feature flags', () => {
Api.fetchFeatureFlagUserLists.mockRejectedValueOnce();
factory();
setImmediate(() => {
done();
});
setImmediate(done);
});
it('should render error state', () => {
......@@ -329,10 +361,7 @@ describe('Feature flags', () => {
.onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
.reply(200, getRequestData, {});
factory();
setImmediate(() => {
done();
});
setImmediate(done);
});
it('should fire the rotate action when a `token` event is received', () => {
......
import { shallowMount } from '@vue/test-utils';
import merge from 'lodash/merge';
import { GlLink } from '@gitlab/ui';
import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
......@@ -16,12 +17,17 @@ describe('Highlight Bar', () => {
title: 'Alert 1',
};
const mountComponent = () => {
wrapper = shallowMount(HighlightBar, {
propsData: {
alert,
},
});
const mountComponent = options => {
wrapper = shallowMount(
HighlightBar,
merge(
{
propsData: { alert },
provide: { fullPath: 'test', iid: 1, slaFeatureAvailable: true },
},
options,
),
);
};
beforeEach(() => {
......@@ -37,22 +43,52 @@ describe('Highlight Bar', () => {
const findLink = () => wrapper.find(GlLink);
it('renders a link to the alert page', () => {
expect(findLink().exists()).toBe(true);
expect(findLink().attributes('href')).toBe(alert.detailsUrl);
expect(findLink().attributes('title')).toBe(alert.title);
expect(findLink().text()).toBe(`#${alert.iid}`);
describe('empty state', () => {
beforeEach(() => {
mountComponent({ propsData: { alert: null } });
});
it('renders a empty component', () => {
expect(wrapper.isVisible()).toBe(false);
});
});
it('renders formatted start time of the alert', () => {
const formattedDate = '2020-05-29 UTC';
formatDate.mockReturnValueOnce(formattedDate);
mountComponent();
expect(formatDate).toHaveBeenCalledWith(alert.startedAt, 'yyyy-mm-dd Z');
expect(wrapper.text()).toContain(formattedDate);
describe('alert present', () => {
beforeEach(() => {
mountComponent();
});
it('renders a link to the alert page', () => {
expect(findLink().exists()).toBe(true);
expect(findLink().attributes('href')).toBe(alert.detailsUrl);
expect(findLink().attributes('title')).toBe(alert.title);
expect(findLink().text()).toBe(`#${alert.iid}`);
});
it('renders formatted start time of the alert', () => {
const formattedDate = '2020-05-29 UTC';
formatDate.mockReturnValueOnce(formattedDate);
mountComponent();
expect(formatDate).toHaveBeenCalledWith(alert.startedAt, 'yyyy-mm-dd Z');
expect(wrapper.text()).toContain(formattedDate);
});
it('renders a number of alert events', () => {
expect(wrapper.text()).toContain(alert.eventCount);
});
});
it('renders a number of alert events', () => {
expect(wrapper.text()).toContain(alert.eventCount);
describe('when child data is present', () => {
beforeEach(() => {
mountComponent({
data() {
return { hasChildData: true };
},
});
});
it('renders the highlight bar component', () => {
expect(wrapper.isVisible()).toBe(true);
});
});
});
......@@ -57,7 +57,6 @@ describe('Incident Tabs component', () => {
it('does not show the alert details tab', () => {
expect(findAlertDetailsComponent().exists()).toBe(false);
expect(findHighlightBarComponent().exists()).toBe(false);
});
});
......
......@@ -30,6 +30,7 @@ issues:
- metrics
- timelogs
- issuable_severity
- issuable_sla
- issue_assignees
- closed_by
- epic_issue
......@@ -713,3 +714,5 @@ system_note_metadata:
- description_version
status_page_published_incident:
- issue
issuable_sla:
- issue
......@@ -23,6 +23,7 @@ RSpec.describe 'Test coverage of the Project Import' do
project.issues.notes.events
project.issues.notes.events.push_event_payload
project.issues.milestone.events.push_event_payload
project.issues.issuable_sla
project.issues.issue_milestones
project.issues.issue_milestones.milestone
project.issues.resource_label_events.label.priorities
......
......@@ -855,3 +855,6 @@ ProjectSecuritySetting:
- auto_fix_sast
- created_at
- updated_at
IssuableSla:
- issue_id
- due_at
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment