Commit 629dc697 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents c4449495 dcfc4b0e
......@@ -25,8 +25,8 @@ export default {
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
MountingPortal,
BoardSidebarWeightInput: () =>
import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
SidebarWeightWidget: () =>
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
IterationSidebarDropdownWidget: () =>
import('ee_component/sidebar/components/iteration_sidebar_dropdown_widget.vue'),
},
......@@ -65,7 +65,12 @@ export default {
},
},
methods: {
...mapActions(['toggleBoardItem', 'setAssignees', 'setActiveItemConfidential']),
...mapActions([
'toggleBoardItem',
'setAssignees',
'setActiveItemConfidential',
'setActiveItemWeight',
]),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
......@@ -144,7 +149,13 @@ export default {
data-testid="sidebar-due-date"
/>
<board-sidebar-labels-select class="labels" />
<board-sidebar-weight-input v-if="weightFeatureAvailable" class="weight" />
<sidebar-weight-widget
v-if="weightFeatureAvailable"
:iid="activeBoardItem.iid"
:full-path="fullPath"
:issuable-type="issuableType"
@weightUpdated="setActiveItemWeight($event)"
/>
<sidebar-confidentiality-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
......
......@@ -704,4 +704,7 @@ export default {
unsetError: ({ commit }) => {
commit(types.SET_ERROR, undefined);
},
// EE action needs CE empty equivalent
setActiveItemWeight: () => {},
};
......@@ -100,9 +100,9 @@ export default {
token: BaseToken,
unique: true,
options: [
{ value: INSTANCE_TYPE, title: s__('Runners|shared') },
{ value: INSTANCE_TYPE, title: s__('Runners|instance') },
{ value: GROUP_TYPE, title: s__('Runners|group') },
{ value: PROJECT_TYPE, title: s__('Runners|specific') },
{ value: PROJECT_TYPE, title: s__('Runners|project') },
],
// TODO We should support more complex search rules,
// search for multiple states (OR) or have NOT operators
......
......@@ -26,6 +26,14 @@ module Emails
subject: subject(_("GitLab account request rejected")))
end
def user_deactivated_email(name, email)
@name = name
profile_email_with_layout(
to: email,
subject: subject(_('Your account has been deactivated')))
end
# rubocop: disable CodeReuse/ActiveRecord
def new_ssh_key_email(key_id)
@key = Key.find_by(id: key_id)
......
......@@ -375,6 +375,10 @@ class User < ApplicationRecord
Ci::DropPipelineService.new.execute_async_for_all(user.pipelines, :user_blocked, user)
Ci::DisableUserPipelineSchedulesService.new.execute(user)
end
after_transition any => :deactivated do |user|
NotificationService.new.user_deactivated(user.name, user.notification_email)
end
# rubocop: enable CodeReuse/ServiceClass
end
......
......@@ -428,6 +428,10 @@ class NotificationService
mailer.user_admin_rejection_email(name, email).deliver_later
end
def user_deactivated(name, email)
mailer.user_deactivated_email(name, email).deliver_later
end
# Members
def new_access_request(member)
return true unless member.notifiable?(:subscription)
......
......@@ -22,7 +22,7 @@
- if can_be_configured
%p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.')
- service_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
- service_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'service-ping')
- service_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: service_ping_path }
%p.mb-2= s_('%{service_ping_link_start}Learn more%{service_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { service_ping_link_start: service_ping_link_start, service_ping_link_end: '</a>'.html_safe }
......@@ -32,7 +32,7 @@
%pre.service-data-payload-container.js-syntax-highlight.code.highlight.mt-2.d-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else
= _('Service ping is disabled, and cannot be configured through this form.')
- deactivating_service_ping_path = help_page_path('development/usage_ping/index.md', anchor: 'disable-usage-ping')
- deactivating_service_ping_path = help_page_path('development/usage_ping/index.md', anchor: 'disable-service-ping')
- deactivating_service_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_service_ping_path }
= s_('For more information, see the documentation on %{deactivating_service_ping_link_start}deactivating service ping%{deactivating_service_ping_link_end}.').html_safe % { deactivating_service_ping_link_start: deactivating_service_ping_link_start, deactivating_service_ping_link_end: '</a>'.html_safe }
.form-group
......
= email_default_heading(_('Hello %{name},') % { name: @name })
%p
= _('Your account has been deactivated. You will not be able to: ')
%ul
%li
= _('Access Git repositories or the API.')
%li
= _('Receive any notifications from GitLab.')
%li
= _('Use slash commands.')
%p
- gitlab_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: root_url }
- link_end = '</a>'.html_safe
= _('To reactivate your account, %{gitlab_link_start}sign in to GitLab.%{link_end}').html_safe % { gitlab_link_start: gitlab_link_start, link_end: link_end}
%p
= _('Please contact your GitLab administrator if you think this is an error.')
<%= _('Hello %{name},') % { name: @name } %>
<%= _('Your account has been deactivated. You will not be able to: ') %>
- <%= _('Access Git repositories or the API.') %>
- <%= _('Receive any notifications from GitLab.') %>
- <%= _('Use slash commands.') %>
<%= _('To reactivate your account, sign in to GitLab at %{gitlab_url}.') % { gitlab_url: root_url } %>
<%= _('Please contact your GitLab administrator if you think this is an error.') %>
......@@ -75,7 +75,7 @@
.js-sidebar-labels{ data: sidebar_labels_data(issuable_sidebar, @project) }
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar, can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid]
- if issuable_sidebar[:supports_severity]
#js-severity
......
This diff is collapsed.
......@@ -44,7 +44,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Adjust your instance's timezone](timezone.md): Customize the default time zone of GitLab.
- [System hooks](../system_hooks/system_hooks.md): Notifications when users, projects and keys are changed.
- [Security](../security/index.md): Learn what you can do to further secure your GitLab instance.
- [Usage statistics, version check, and usage ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc.
- [Usage statistics, version check, and Service Ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc.
- [Global user settings](user_settings.md): Configure instance-wide user permissions.
- [Polling](polling.md): Configure how often the GitLab UI polls for updates.
- [GitLab Pages configuration](pages/index.md): Enable and configure GitLab Pages.
......
......@@ -1172,17 +1172,17 @@ registry = Geo::SnippetRepositoryRegistry.find(registry_id)
registry.replicator.send(:sync_repository)
```
### Generate usage ping
### Generate Service Ping
#### Generate or get the cached usage ping
#### Generate or get the cached Service Ping
```ruby
Gitlab::UsageData.to_json
```
#### Generate a fresh new usage ping
#### Generate a fresh new Service Ping
This will also refresh the cached usage ping displayed in the admin area
This will also refresh the cached Service Ping displayed in the admin area
```ruby
Gitlab::UsageData.to_json(force_refresh: true)
......@@ -1190,13 +1190,13 @@ Gitlab::UsageData.to_json(force_refresh: true)
#### Generate and print
Generates usage ping data in JSON format.
Generates Service Ping data in JSON format.
```shell
rake gitlab:usage_data:generate
```
#### Generate and send usage ping
#### Generate and send Service Ping
Prints the metrics saved in `conversational_development_index_metrics`.
......
......@@ -7,7 +7,7 @@ type: reference, api
# Usage Data API **(FREE SELF)**
The Usage Data API is associated with [Usage Ping](../development/usage_ping/index.md).
The Usage Data API is associated with [Service Ping](../development/usage_ping/index.md).
## Export metric definitions as a single YAML file
......@@ -48,14 +48,14 @@ Example response:
...
```
## Export Usage Ping SQL queries
## Export Service Ping SQL queries
This action is available only for the GitLab instance [Administrator](../user/permissions.md) users.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57016) in GitLab 13.11.
> - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default.
Return all of the raw SQL queries used to compute Usage Ping.
Return all of the raw SQL queries used to compute Service Ping.
```plaintext
GET /usage_data/queries
......@@ -116,7 +116,7 @@ Example response:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57050) in GitLab 13.11.
> - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default.
Return all non-SQL metrics data used in the usage ping.
Return all non-SQL metrics data used in the Service ping.
Example request:
......
......@@ -95,7 +95,7 @@ notify users that we plan to remove them.
- Define a data-informed deprecation policy that will serve our users better
- Build a dashboard showing usage frequency of deprecated GraphQL fields
- Build mechanisms required to send deprecated fields usage in usage ping
- Build mechanisms required to send deprecated fields usage in Service Ping
### Ensure consistency with the rest of the codebase
......
......@@ -271,7 +271,7 @@ See [database guidelines](database/index.md).
## Product Intelligence guides
- [Product Intelligence guide](https://about.gitlab.com/handbook/product/product-intelligence-guide/)
- [Usage Ping guide](usage_ping/index.md)
- [Service Ping guide](usage_ping/index.md)
- [Snowplow guide](snowplow/index.md)
## Experiment guide
......
......@@ -394,4 +394,4 @@ end
### `EachBatch` vs `BatchCount`
When adding new counters for usage ping, the preferred way to count records is using the `Gitlab::Database::BatchCount` class. The iteration logic implemented in `BatchCount` has similar performance characteristics like `EachBatch`. Most of the tips and suggestions for improving `BatchCount` mentioned above applies to `BatchCount` as well.
When adding new counters for Service Ping, the preferred way to count records is using the `Gitlab::Database::BatchCount` class. The iteration logic implemented in `BatchCount` has similar performance characteristics like `EachBatch`. Most of the tips and suggestions for improving `BatchCount` mentioned above applies to `BatchCount` as well.
......@@ -21,7 +21,7 @@ When you are optimizing your SQL queries, there are two dimensions to pay attent
| Queries in a migration | `100ms` | This is different than the total [migration time](database_review.md#timing-guidelines-for-migrations). |
| Concurrent operations in a migration | `5min` | Concurrent operations do not block the database, but they block the GitLab update. This includes operations such as `add_concurrent_index` and `add_concurrent_foreign_key`. |
| Background migrations | `1s` | |
| Usage Ping | `1s` | See the [usage ping docs](usage_ping/index.md#developing-and-testing-usage-ping) for more details. |
| Service Ping | `1s` | See the [Service Ping docs](usage_ping/index.md#developing-and-testing-service-ping) for more details. |
- When analyzing your query's performance, pay attention to if the time you are seeing is on a [cold or warm cache](#cold-and-warm-cache). These guidelines apply for both cache types.
- When working with batched queries, change the range and batch size to see how it effects the query timing and caching.
......
......@@ -11,7 +11,7 @@ This guide provides an overview of how Snowplow works, and implementation detail
For more information about Product Intelligence, see:
- [Product Intelligence Guide](https://about.gitlab.com/handbook/product/product-intelligence-guide/)
- [Usage Ping Guide](../usage_ping/index.md)
- [Service Ping Guide](../usage_ping/index.md)
More useful links:
......
This diff is collapsed.
......@@ -12,7 +12,7 @@ This guide describes Metrics Dictionary and how it's implemented
We are using [JSON Schema](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/schema.json) to validate the metrics definition.
This process is meant to ensure consistent and valid metrics defined for Usage Ping. All metrics *must*:
This process is meant to ensure consistent and valid metrics defined for Service Ping. All metrics *must*:
- Comply with the defined [JSON schema](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/schema.json).
- Have a unique `key_path` .
......@@ -26,7 +26,7 @@ Each metric is defined in a separate YAML file consisting of a number of fields:
| Field | Required | Additional information |
|---------------------|----------|----------------------------------------------------------------|
| `key_path` | yes | JSON key path for the metric, location in Usage Ping payload. |
| `key_path` | yes | JSON key path for the metric, location in Service Ping payload. |
| `name` | no | Metric name suggestion. Can replace the last part of `key_path`. |
| `description` | yes | |
| `product_section` | yes | The [section](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/sections.yml). |
......@@ -58,7 +58,7 @@ Metric definitions can have one of the following statuses:
- `broken`: Metric reports broken data (for example, -1 fallback), or does not report data at all. A metric marked as `broken` must also have the `repair_issue_url` attribute.
- `not_used`: Metric is not used in any dashboard.
- `deprecated`: Metric is deprecated and possibly planned to be removed.
- `removed`: Metric was removed, but it may appear in Usage Ping payloads sent from instances running on older versions of GitLab.
- `removed`: Metric was removed, but it may appear in Service Ping payloads sent from instances running on older versions of GitLab.
### Metric value_type
......@@ -99,7 +99,7 @@ should be changed.
We use the following categories to classify a metric:
- `Operational`: Required data for operational purposes.
- `Optional`: Data that is optional to collect. This can be [enabled or disabled](../usage_ping/index.md#disable-usage-ping) in the Admin Area.
- `Optional`: Data that is optional to collect. This can be [enabled or disabled](../usage_ping/index.md#disable-service-ping) in the Admin Area.
- `Subscription`: Data related to licensing.
- `Standard`: Standard set of identifiers that are included when collecting data.
......@@ -214,9 +214,9 @@ bundle exec rails generate gitlab:usage_metric_definition counts.issues --ee --d
create ee/config/metrics/counts_7d/issues.yml
```
## Metrics added dynamic to Usage Ping payload
## Metrics added dynamic to Service Ping payload
The [Redis HLL metrics](index.md#known-events-are-added-automatically-in-usage-data-payload) are added automatically to Usage Ping payload.
The [Redis HLL metrics](index.md#known-events-are-added-automatically-in-usage-data-payload) are added automatically to Service Ping payload.
A YAML metric definition is required for each metric. A dedicated generator is provided to create metric definitions for Redis HLL events.
......
......@@ -6,13 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Metrics instrumentation guide
This guide describes how to develop Usage Ping metrics using metrics instrumentation.
This guide describes how to develop Service Ping metrics using metrics instrumentation.
## Nomenclature
- **Instrumentation class**:
- Inherits one of the metric classes: `DatabaseMetric`, `RedisHLLMetric` or `GenericMetric`.
- Implements the logic that calculates the value for a Usage Ping metric.
- Implements the logic that calculates the value for a Service Ping metric.
- **Metric definition**
The Usage Data metric YAML definition.
......@@ -27,7 +27,7 @@ A metric definition has the [`instrumentation_class`](metrics_dictionary.md) fie
The defined instrumentation class should have one of the existing metric classes: `DatabaseMetric`, `RedisHLLMetric`, or `GenericMetric`.
Using the instrumentation classes ensures that metrics can fail safe individually, without breaking the entire
process of Usage Ping generation.
process of Service Ping generation.
We have built a domain-specific language (DSL) to define the metrics instrumentation.
......@@ -88,7 +88,7 @@ end
## Creating a new metric instrumentation class
To create a stub instrumentation for a Usage Ping metric, you can use a dedicated [generator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/generators/gitlab/usage_metric_generator.rb):
To create a stub instrumentation for a Service Ping metric, you can use a dedicated [generator](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/generators/gitlab/usage_metric_generator.rb):
The generator takes the class name as an argument and the following options:
......
......@@ -4,22 +4,22 @@ group: Product Intelligence
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Usage Ping review guidelines
# Service Ping review guidelines
This page includes introductory material for a
[Product Intelligence](https://about.gitlab.com/handbook/engineering/development/growth/product-intelligence/)
review, and is specific to Usage Ping related reviews. For broader advice and
review, and is specific to Service Ping related reviews. For broader advice and
general best practices for code reviews, refer to our [code review guide](../code_review.md).
## Resources for reviewers
- [Usage Ping Guide](index.md)
- [Service Ping Guide](index.md)
- [Metrics Dictionary](metrics_dictionary.md)
## Review process
We recommend a Product Intelligence review when a merge request (MR) touches
any of the following Usage Ping files:
any of the following Service Ping files:
- `usage_data*` files.
- The Metrics Dictionary, including files in:
......@@ -50,7 +50,7 @@ any of the following Usage Ping files:
- Perform a first-pass review on the merge request and suggest improvements to the author.
- Check the [metrics location](index.md#1-naming-and-placing-the-metrics) in
the Usage Ping JSON payload.
the Service Ping JSON payload.
- Add the `~database` label and ask for a [database review](../database_review.md) for
metrics that are based on Database.
- For tracking using Redis HLL (HyperLogLog):
......
......@@ -46,7 +46,7 @@ The following Rake tasks are available for use with GitLab:
| [Repository storage](../administration/raketasks/storage.md) | List and migrate existing projects and attachments from legacy storage to hashed storage. |
| [Uploads migrate](../administration/raketasks/uploads/migrate.md) | Migrate uploads between local storage and object storage. |
| [Uploads sanitize](../administration/raketasks/uploads/sanitize.md) | Remove EXIF data from images uploaded to earlier versions of GitLab. |
| [Usage data](../administration/troubleshooting/gitlab_rails_cheat_sheet.md#generate-usage-ping) | Generate and troubleshoot [Usage Ping](../development/usage_ping/index.md). |
| [Usage data](../administration/troubleshooting/gitlab_rails_cheat_sheet.md#generate-service-ping) | Generate and troubleshoot [Service Ping](../development/usage_ping/index.md). |
| [User management](user_management.md) | Perform user management tasks. |
| [Webhooks administration](web_hooks.md) | Maintain project webhooks. |
| [X.509 signatures](x509_signatures.md) | Update X.509 commit signatures, which can be useful if the certificate store changed. |
......@@ -20,19 +20,19 @@ To see DevOps Report:
## DevOps Score
NOTE:
Your GitLab instance's [usage ping](../settings/usage_statistics.md#usage-ping) must be activated in order to use this feature.
Your GitLab instance's [Service Ping](../settings/usage_statistics.md#service-ping) must be activated in order to use this feature.
The DevOps Score tab displays the usage of major GitLab features on your instance over
the last 30 days, averaged over the number of billable users in that time period. It also
provides a Lead score per feature, which is calculated based on GitLab analysis
of top-performing instances based on [usage ping data](../settings/usage_statistics.md#usage-ping) that GitLab has
of top-performing instances based on [Service Ping data](../settings/usage_statistics.md#service-ping) that GitLab has
collected. Your score is compared to the lead score of each feature and then expressed as a percentage at the bottom of said feature.
Your overall **DevOps Score** is an average of your feature scores. You can use this score to compare your DevOps status to other organizations.
The page also provides helpful links to articles and GitLab docs, to help you
improve your scores.
Usage ping data is aggregated on GitLab servers for analysis. Your usage
Service Ping data is aggregated on GitLab servers for analysis. Your usage
information is **not sent** to any other GitLab instances. If you have just started using GitLab, it may take a few weeks for data to be
collected before this feature is available.
......
......@@ -14,7 +14,7 @@ All statistics are opt-out. To enable or disable them:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. In the left sidebar, select **Settings > Metrics and profiling**, and expand **Usage statistics**.
1. Enable or disable **Version check** and **Usage ping**.
1. Enable or disable **Version check** and **Service ping**.
1. Select **Save changes**.
## Network configuration
......@@ -67,14 +67,14 @@ sequenceDiagram
Version Application->>GitLab instance: Response (PNG/SVG)
```
## Usage Ping **(FREE SELF)**
## Service Ping **(FREE SELF)**
See [Usage Ping guide](../../../development/usage_ping/index.md).
See [Service Ping guide](../../../development/usage_ping/index.md).
## Instance-level analytics availability
After usage ping is enabled, GitLab gathers data from other instances and
enables certain [instance-level analytics features](../analytics/index.md) that are dependent on usage ping.
After Service Ping is enabled, GitLab gathers data from other instances and
enables certain [instance-level analytics features](../analytics/index.md) that are dependent on Service Ping.
<!-- ## Troubleshooting
......
......@@ -36,7 +36,7 @@ export default {
},
},
methods: {
...mapActions(['setActiveIssueWeight', 'setError']),
...mapActions(['setActiveItemWeight', 'setError']),
handleFormSubmit() {
this.$refs.sidebarItem.collapse({ emitEvent: false });
this.setWeight();
......@@ -51,7 +51,7 @@ export default {
this.loading = true;
try {
await this.setActiveIssueWeight({ weight, projectPath: this.projectPathForActiveIssue });
await this.setActiveItemWeight({ weight, projectPath: this.projectPathForActiveIssue });
this.weight = weight;
} catch (e) {
this.weight = this.activeBoardItem.weight;
......
......@@ -35,7 +35,6 @@ import epicMoveListMutation from '../graphql/epic_move_list.mutation.graphql';
import epicsSwimlanesQuery from '../graphql/epics_swimlanes.query.graphql';
import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import issueSetWeightMutation from '../graphql/issue_set_weight.mutation.graphql';
import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics.mutation.graphql';
import listsEpicsQuery from '../graphql/lists_epics.query.graphql';
import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql';
......@@ -326,26 +325,11 @@ export default {
commit(types.RESET_EPICS);
},
setActiveIssueWeight: async ({ commit, getters }, input) => {
const { data } = await gqlClient.mutate({
mutation: issueSetWeightMutation,
variables: {
input: {
iid: String(getters.activeBoardItem.iid),
weight: input.weight,
projectPath: input.projectPath,
},
},
});
if (!data.issueSetWeight || data.issueSetWeight?.errors?.length > 0) {
throw new Error(data.issueSetWeight?.errors);
}
setActiveItemWeight: async ({ commit, getters }, weight) => {
commit(typesCE.UPDATE_BOARD_ITEM_BY_ID, {
itemId: getters.activeBoardItem.id,
prop: 'weight',
value: data.issueSetWeight.issue.weight,
value: weight,
});
},
......
<script>
import {
GlButton,
GlForm,
GlFormInput,
GlLoadingIcon,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { weightQueries, MAX_DISPLAY_WEIGHT } from '../../constants';
export default {
tracking: {
event: 'click_edit_button',
label: 'right_sidebar',
property: 'weight',
},
components: {
GlButton,
GlForm,
GlFormInput,
GlIcon,
GlLoadingIcon,
SidebarEditableItem,
},
directives: {
autofocusonshow,
GlTooltip: GlTooltipDirective,
},
inject: ['canUpdate'],
props: {
iid: {
type: String,
required: true,
},
fullPath: {
type: String,
required: true,
},
issuableType: {
required: true,
type: String,
},
},
data() {
return {
weight: null,
loading: false,
};
},
apollo: {
weight: {
query() {
return weightQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.fullPath,
iid: String(this.iid),
};
},
update(data) {
return data.workspace?.issuable?.weight || null;
},
result({ data }) {
this.$emit('weightUpdated', data.workspace?.issuable?.weight || null);
},
error() {
createFlash({
message: sprintf(__('Something went wrong while setting %{issuableType} weight.'), {
issuableType: this.issuableType,
}),
});
},
},
},
computed: {
isLoading() {
return this.$apollo.queries?.weight?.loading || this.loading;
},
hasWeight() {
return this.weight !== null;
},
weightLabel() {
return this.hasWeight ? this.weight : this.$options.i18n.noWeightLabel;
},
tooltipTitle() {
let tooltipTitle = this.$options.i18n.weight;
if (this.hasWeight) {
tooltipTitle += ` ${this.weight}`;
}
return tooltipTitle;
},
collapsedWeightLabel() {
return this.hasWeight
? this.weight.toString().substr(0, 5)
: this.$options.i18n.noWeightLabel;
},
},
methods: {
setWeight(remove) {
const weight = remove ? null : this.weight;
this.loading = true;
this.$apollo
.mutate({
mutation: weightQueries[this.issuableType].mutation,
variables: {
input: {
projectPath: this.fullPath,
iid: this.iid,
weight,
},
},
})
.then(
({
data: {
issuableSetWeight: { errors },
},
}) => {
if (errors.length) {
createFlash({
message: errors[0],
});
}
},
)
.catch(() => {
createFlash({
message: sprintf(__('Something went wrong while setting %{issuableType} weight.'), {
issuableType: this.issuableType,
}),
});
})
.finally(() => {
this.loading = false;
});
},
expandSidebar() {
this.$refs.editable.expand();
this.$emit('expandSidebar');
},
handleFormSubmit() {
this.$refs.editable.collapse({ emitEvent: false });
this.setWeight();
},
},
i18n: {
weight: __('Weight'),
noWeightLabel: __('None'),
removeWeight: __('remove weight'),
inputPlaceholder: __('Enter a number'),
},
maxDisplayWeight: MAX_DISPLAY_WEIGHT,
};
</script>
<template>
<sidebar-editable-item
ref="editable"
:title="$options.i18n.weight"
:tracking="$options.tracking"
:loading="isLoading"
class="block weight"
data-testid="sidebar-weight"
@close="setWeight()"
>
<template #collapsed>
<div class="gl-display-flex gl-align-items-center hide-collapsed">
<span
:class="hasWeight ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"
data-testid="sidebar-weight-value"
data-qa-selector="weight_label_value"
>
{{ weightLabel }}
</span>
<div v-if="hasWeight && canUpdate" class="gl-display-flex">
<span class="gl-mx-2">-</span>
<gl-button
variant="link"
class="gl-text-gray-500!"
:disabled="loading"
@click="setWeight(true)"
>
{{ $options.i18n.removeWeight }}
</gl-button>
</div>
</div>
<div
v-gl-tooltip.left.viewport
:title="tooltipTitle"
class="sidebar-collapsed-icon js-weight-collapsed-block"
@click="expandSidebar"
>
<gl-icon :size="16" name="weight" />
<gl-loading-icon v-if="isLoading" class="js-weight-collapsed-loading-icon" />
<span v-else class="js-weight-collapsed-weight-label">
{{ collapsedWeightLabel }}
<template v-if="weight > $options.maxDisplayWeight">&hellip;</template>
</span>
</div>
</template>
<template #default>
<gl-form @submit.prevent="handleFormSubmit()">
<gl-form-input
v-model.number="weight"
v-autofocusonshow
type="number"
min="0"
:placeholder="$options.i18n.inputPlaceholder"
/>
</gl-form>
</template>
</sidebar-editable-item>
</template>
......@@ -8,10 +8,12 @@ import {
import epicAncestorsQuery from './queries/epic_ancestors.query.graphql';
import groupEpicsQuery from './queries/group_epics.query.graphql';
import groupIterationsQuery from './queries/group_iterations.query.graphql';
import issueWeightQuery from './queries/issue_weight.query.graphql';
import projectIssueEpicMutation from './queries/project_issue_epic.mutation.graphql';
import projectIssueEpicQuery from './queries/project_issue_epic.query.graphql';
import projectIssueIterationMutation from './queries/project_issue_iteration.mutation.graphql';
import projectIssueIterationQuery from './queries/project_issue_iteration.query.graphql';
import updateIssueWeightMutation from './queries/update_issue_weight.mutation.graphql';
export const healthStatus = {
ON_TRACK: 'onTrack',
......@@ -128,3 +130,10 @@ export const ancestorsQueries = {
query: epicAncestorsQuery,
},
};
export const weightQueries = {
[IssuableType.Issue]: {
query: issueWeightQuery,
mutation: updateIssueWeightMutation,
},
};
......@@ -9,7 +9,7 @@ import CveIdRequest from './components/cve_id_request/cve_id_request_sidebar.vue
import IterationSidebarDropdownWidget from './components/iteration_sidebar_dropdown_widget.vue';
import SidebarDropdownWidget from './components/sidebar_dropdown_widget.vue';
import SidebarStatus from './components/status/sidebar_status.vue';
import SidebarWeight from './components/weight/sidebar_weight.vue';
import SidebarWeightWidget from './components/weight/sidebar_weight_widget.vue';
import { IssuableAttributeType } from './constants';
Vue.use(VueApollo);
......@@ -19,12 +19,26 @@ const mountWeightComponent = () => {
if (!el) return false;
const { canEdit, projectPath, issueIid } = el.dataset;
return new Vue({
el,
apolloProvider,
components: {
SidebarWeight,
SidebarWeightWidget,
},
provide: {
canUpdate: parseBoolean(canEdit),
isClassicSidebar: true,
},
render: (createElement) => createElement('sidebar-weight'),
render: (createElement) =>
createElement('sidebar-weight-widget', {
props: {
fullPath: projectPath,
iid: issueIid,
issuableType: IssuableType.Issue,
},
}),
});
};
......
query issueWeight($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: issue(iid: $iid) {
__typename
id
weight
}
}
}
mutation issueSetWeight($input: IssueSetWeightInput!) {
issuableSetWeight: issueSetWeight(input: $input) {
issuable: issue {
id
weight
}
errors
}
}
- if issuable_sidebar[:supports_weight]
- if issuable_sidebar[:features_available][:issue_weights]
.js-sidebar-weight-entry-point
.js-sidebar-weight-entry-point{ data: { can_edit: can_edit, project_path: project_path, issue_iid: issue_iid } }
- else
= render 'shared/promotions/promote_issue_weights'
......@@ -222,7 +222,7 @@ RSpec.describe 'Issue Boards', :js do
context 'weight' do
let(:weight_widget) { find('[data-testid="sidebar-weight"]') }
let(:weight_value) { find('[data-testid="sidebar-weight"] .value') }
let(:weight_value) { find('[data-testid="sidebar-weight-value"]') }
it 'displays weight async' do
click_card(card1)
......
......@@ -41,10 +41,10 @@ RSpec.describe 'Issue Sidebar' do
it 'updates weight in sidebar to 1' do
page.within '.weight' do
click_link 'Edit'
click_button 'Edit'
find('input').send_keys 1, :enter
page.within '.value' do
page.within '[data-testid="sidebar-weight-value"]' do
expect(page).to have_content '1'
end
end
......@@ -52,16 +52,16 @@ RSpec.describe 'Issue Sidebar' do
it 'updates weight in sidebar to no weight' do
page.within '.weight' do
click_link 'Edit'
click_button 'Edit'
find('input').send_keys 1, :enter
page.within '.value' do
page.within '[data-testid="sidebar-weight-value"]' do
expect(page).to have_content '1'
end
click_link 'remove weight'
click_button 'remove weight'
page.within '.value' do
page.within '[data-testid="sidebar-weight-value"]' do
expect(page).to have_content 'None'
end
end
......
......@@ -19,11 +19,11 @@ RSpec.describe 'Issue weight', :js do
page.within('.weight') do
expect(page).to have_content "None"
click_link 'Edit'
click_button 'Edit'
find('.block.weight input').send_keys 1, :enter
page.within('.value') do
page.within('[data-testid="sidebar-weight-value"]') do
expect(page).to have_content "1"
end
end
......@@ -37,11 +37,11 @@ RSpec.describe 'Issue weight', :js do
page.within('.weight') do
expect(page).to have_content "2"
click_link 'Edit'
click_button 'Edit'
find('.block.weight input').send_keys 3, :enter
page.within('.value') do
page.within('[data-testid="sidebar-weight-value"]') do
expect(page).to have_content "3"
end
end
......@@ -55,9 +55,9 @@ RSpec.describe 'Issue weight', :js do
page.within('.weight') do
expect(page).to have_content "5"
click_link 'remove weight'
click_button 'remove weight'
page.within('.value') do
page.within('[data-testid="sidebar-weight-value"]') do
expect(page).to have_content "None"
end
end
......
......@@ -64,8 +64,10 @@ exports[`ee/BoardContentSidebar matches the snapshot 1`] = `
class="labels"
/>
<boardsidebarweightinput-stub
class="weight"
<sidebarweightwidget-stub
full-path="gitlab-org/gitlab-test"
iid="27"
issuable-type="issue"
/>
<sidebarconfidentialitywidget-stub
......
......@@ -69,7 +69,7 @@ describe('ee/BoardContentSidebar', () => {
SidebarConfidentialityWidget: true,
SidebarDateWidget: true,
SidebarSubscriptionsWidget: true,
BoardSidebarWeightInput: true,
SidebarWeightWidget: true,
SidebarDropdownWidget: true,
MountingPortal: true,
},
......
......@@ -52,7 +52,7 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
describe('when weight is submitted', () => {
beforeEach(async () => {
createWrapper();
jest.spyOn(wrapper.vm, 'setActiveIssueWeight');
jest.spyOn(wrapper.vm, 'setActiveItemWeight');
findWeightInput().vm.$emit('input', TEST_WEIGHT);
findWeightForm().vm.$emit('submit', { preventDefault: () => {} });
store.state.boardItems[TEST_ISSUE.id].weight = TEST_WEIGHT;
......@@ -66,7 +66,7 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
});
it('commits change to the server', () => {
expect(wrapper.vm.setActiveIssueWeight).toHaveBeenCalledWith({
expect(wrapper.vm.setActiveItemWeight).toHaveBeenCalledWith({
weight: TEST_WEIGHT,
projectPath: 'h/b',
});
......@@ -76,7 +76,7 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
describe('when weight is set to 0', () => {
beforeEach(async () => {
createWrapper({ weight: TEST_WEIGHT });
jest.spyOn(wrapper.vm, 'setActiveIssueWeight');
jest.spyOn(wrapper.vm, 'setActiveItemWeight');
findWeightInput().vm.$emit('input', 0);
findWeightForm().vm.$emit('submit', { preventDefault: () => {} });
store.state.boardItems[TEST_ISSUE.id].weight = 0;
......@@ -84,7 +84,7 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
});
it('collapses sidebar and renders "None"', () => {
expect(wrapper.vm.setActiveIssueWeight).toHaveBeenCalled();
expect(wrapper.vm.setActiveItemWeight).toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toBe('None');
});
......@@ -93,14 +93,14 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
describe('when weight is resetted', () => {
beforeEach(async () => {
createWrapper({ weight: TEST_WEIGHT });
jest.spyOn(wrapper.vm, 'setActiveIssueWeight');
jest.spyOn(wrapper.vm, 'setActiveItemWeight');
findResetButton().vm.$emit('click');
store.state.boardItems[TEST_ISSUE.id].weight = 0;
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders "None"', () => {
expect(wrapper.vm.setActiveIssueWeight).toHaveBeenCalled();
expect(wrapper.vm.setActiveItemWeight).toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toBe('None');
});
......@@ -109,7 +109,7 @@ describe('ee/boards/components/sidebar/board_sidebar_weight_input.vue', () => {
describe('when the mutation fails', () => {
beforeEach(async () => {
createWrapper({ weight: TEST_WEIGHT });
jest.spyOn(wrapper.vm, 'setActiveIssueWeight').mockImplementation(() => {
jest.spyOn(wrapper.vm, 'setActiveItemWeight').mockImplementation(() => {
throw new Error(['failed mutation']);
});
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
......
......@@ -608,27 +608,13 @@ describe('resetEpics', () => {
});
});
describe('setActiveIssueWeight', () => {
describe('setActiveItemWeight', () => {
const state = { boardItems: { [mockIssue.id]: mockIssue } };
const getters = { activeBoardItem: mockIssue };
const testWeight = mockIssue.weight + 1;
const input = {
weight: testWeight,
projectPath: 'h/b',
};
it('should commit weight after setting the issue', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueSetWeight: {
issue: {
weight: testWeight,
},
errors: [],
},
},
});
const input = testWeight;
it('should commit weight', (done) => {
const payload = {
itemId: getters.activeBoardItem.id,
prop: 'weight',
......@@ -636,7 +622,7 @@ describe('setActiveIssueWeight', () => {
};
testAction(
actions.setActiveIssueWeight,
actions.setActiveItemWeight,
input,
{ ...state, ...getters },
[
......@@ -655,7 +641,7 @@ describe('setActiveIssueWeight', () => {
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { issueSetWeight: { errors: ['failed mutation'] } } });
await expect(actions.setActiveIssueWeight({ getters }, input)).rejects.toThrow(Error);
await expect(actions.setActiveItemWeight({ getters }, input)).rejects.toThrow(Error);
});
});
......
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import SidebarWeightWidget from 'ee_component/sidebar/components/weight/sidebar_weight_widget.vue';
import issueWeightQuery from 'ee_component/sidebar/queries/issue_weight.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { issueNoWeightResponse, issueWeightResponse } from '../../mock_data';
jest.mock('~/flash');
Vue.use(VueApollo);
describe('Sidebar Weight Widget', () => {
let wrapper;
let fakeApollo;
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findWeightValue = () => wrapper.findByTestId('sidebar-weight-value');
const createComponent = ({
weightQueryHandler = jest.fn().mockResolvedValue(issueNoWeightResponse()),
} = {}) => {
fakeApollo = createMockApollo([[issueWeightQuery, weightQueryHandler]]);
wrapper = extendedWrapper(
shallowMount(SidebarWeightWidget, {
apolloProvider: fakeApollo,
provide: {
canUpdate: true,
},
propsData: {
fullPath: 'group/project',
iid: '1',
issuableType: 'issue',
},
stubs: {
SidebarEditableItem,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
it('passes a `loading` prop as true to editable item when query is loading', () => {
createComponent();
expect(findEditableItem().props('loading')).toBe(true);
});
describe('when issue has no weight', () => {
beforeEach(() => {
createComponent();
return waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('toggle is unchecked', () => {
expect(findWeightValue().text()).toBe('None');
});
it('emits `weightUpdated` event with a `null` payload', () => {
expect(wrapper.emitted('weightUpdated')).toEqual([[null]]);
});
});
describe('when issue has weight', () => {
beforeEach(() => {
createComponent({
weightQueryHandler: jest.fn().mockResolvedValue(issueWeightResponse(true)),
});
return waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('toggle is checked', () => {
expect(findWeightValue().text()).toBe('1');
});
it('emits `weightUpdated` event with a `true` payload', () => {
expect(wrapper.emitted('weightUpdated')).toEqual([[1]]);
});
});
it('displays a flash message when query is rejected', async () => {
createComponent({
weightQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
});
......@@ -166,3 +166,21 @@ export const epicAncestorsResponse = () => ({
},
},
});
export const issueNoWeightResponse = () => ({
data: {
workspace: {
issuable: { id: mockIssueId, weight: null, __typename: 'Issue' },
__typename: 'Project',
},
},
});
export const issueWeightResponse = () => ({
data: {
workspace: {
issuable: { id: mockIssueId, weight: 1, __typename: 'Issue' },
__typename: 'Project',
},
},
});
......@@ -1704,6 +1704,9 @@ msgstr ""
msgid "Access Git repositories"
msgstr ""
msgid "Access Git repositories or the API."
msgstr ""
msgid "Access Tokens"
msgstr ""
......@@ -26854,6 +26857,9 @@ msgstr ""
msgid "Receive alerts from manually configured Prometheus servers."
msgstr ""
msgid "Receive any notifications from GitLab."
msgstr ""
msgid "Receive notifications about your own activity"
msgstr ""
......@@ -28305,6 +28311,12 @@ msgstr ""
msgid "Runners|group"
msgstr ""
msgid "Runners|instance"
msgstr ""
msgid "Runners|project"
msgstr ""
msgid "Runners|shared"
msgstr ""
......@@ -30485,6 +30497,9 @@ msgstr ""
msgid "Something went wrong while setting %{issuableType} to-do item."
msgstr ""
msgid "Something went wrong while setting %{issuableType} weight."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again."
msgstr ""
......@@ -34152,6 +34167,12 @@ msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
msgstr ""
msgid "To reactivate your account, %{gitlab_link_start}sign in to GitLab.%{link_end}"
msgstr ""
msgid "To reactivate your account, sign in to GitLab at %{gitlab_url}."
msgstr ""
msgid "To receive alerts from manually configured Prometheus services, add the following URL and Authorization key to your Prometheus webhook config file. Learn more about %{linkStart}configuring Prometheus%{linkEnd} to send alerts to GitLab."
msgstr ""
......@@ -35423,6 +35444,9 @@ msgstr ""
msgid "Use shortcuts"
msgstr ""
msgid "Use slash commands."
msgstr ""
msgid "Use template"
msgstr ""
......@@ -37793,9 +37817,15 @@ msgstr ""
msgid "Your access request to the %{source_type} has been withdrawn."
msgstr ""
msgid "Your account has been deactivated"
msgstr ""
msgid "Your account has been deactivated by your administrator. Please log back in to reactivate your account."
msgstr ""
msgid "Your account has been deactivated. You will not be able to: "
msgstr ""
msgid "Your account is locked."
msgstr ""
......
......@@ -1898,6 +1898,14 @@ RSpec.describe User do
expect(user.deactivated?).to be_truthy
end
it 'sends deactivated user an email' do
expect_next_instance_of(NotificationService) do |notification|
allow(notification).to receive(:user_deactivated).with(user.name, user.notification_email)
end
user.deactivate
end
end
context "a user who is blocked" do
......
......@@ -2619,6 +2619,16 @@ RSpec.describe NotificationService, :mailer do
end
end
describe '#user_deactivated', :deliver_mails_inline do
let_it_be(:user) { create(:user) }
it 'sends the user an email' do
notification.user_deactivated(user.name, user.notification_email)
should_only_email(user)
end
end
describe 'GroupMember', :deliver_mails_inline do
let(:added_user) { create(:user) }
......
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