mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-03 16:04:30 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
@ -99,8 +99,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isLastDeployment() {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return this.environment?.isLastDeployment || this.environment?.lastDeployment?.['last?'];
|
||||
return this.environment?.isLastDeployment || this.environment?.lastDeployment?.isLast;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -1,8 +1,9 @@
|
||||
<script>
|
||||
import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { formatTime } from '~/lib/utils/datetime_utility';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
import actionMutation from '../graphql/mutations/action.mutation.graphql';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
@ -12,7 +13,6 @@ export default {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
actions: {
|
||||
@ -20,6 +20,11 @@ export default {
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
graphql: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -49,7 +54,11 @@ export default {
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
eventHub.$emit('postAction', { endpoint: action.playPath });
|
||||
if (this.graphql) {
|
||||
this.$apollo.mutate({ mutation: actionMutation, variables: { action } });
|
||||
} else {
|
||||
eventHub.$emit('postAction', { endpoint: action.playPath });
|
||||
}
|
||||
},
|
||||
|
||||
isActionDisabled(action) {
|
||||
@ -70,18 +79,16 @@ export default {
|
||||
<template>
|
||||
<gl-dropdown
|
||||
v-gl-tooltip
|
||||
:text="title"
|
||||
:title="title"
|
||||
:loading="isLoading"
|
||||
:aria-label="title"
|
||||
:disabled="isLoading"
|
||||
icon="play"
|
||||
text-sr-only
|
||||
right
|
||||
data-container="body"
|
||||
data-testid="environment-actions-button"
|
||||
>
|
||||
<template #button-content>
|
||||
<gl-icon name="play" />
|
||||
<gl-icon name="chevron-down" />
|
||||
<gl-loading-icon v-if="isLoading" size="sm" />
|
||||
</template>
|
||||
<gl-dropdown-item
|
||||
v-for="(action, i) in actions"
|
||||
:key="i"
|
||||
|
@ -2,9 +2,11 @@
|
||||
import { GlButton, GlCollapse, GlIcon, GlBadge, GlLink } from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import folderQuery from '../graphql/queries/folder.query.graphql';
|
||||
import EnvironmentItem from './new_environment_item.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EnvironmentItem,
|
||||
GlButton,
|
||||
GlCollapse,
|
||||
GlIcon,
|
||||
@ -51,16 +53,25 @@ export default {
|
||||
folderPath() {
|
||||
return this.nestedEnvironment.latest.folderPath;
|
||||
},
|
||||
environments() {
|
||||
return this.folder?.environments;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCollapse() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
isFirstEnvironment(index) {
|
||||
return index === 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-border-b-solid gl-border-gray-100 gl-border-1 gl-px-3 gl-pt-3 gl-pb-5">
|
||||
<div
|
||||
:class="{ 'gl-pb-5': !visible }"
|
||||
class="gl-border-b-solid gl-border-gray-100 gl-border-1 gl-px-3 gl-pt-3"
|
||||
>
|
||||
<div class="gl-w-full gl-display-flex gl-align-items-center">
|
||||
<gl-button
|
||||
class="gl-mr-4 gl-fill-current-color gl-text-gray-500"
|
||||
@ -77,6 +88,15 @@ export default {
|
||||
<gl-badge size="sm" class="gl-mr-auto">{{ count }}</gl-badge>
|
||||
<gl-link v-if="visible" :href="folderPath">{{ $options.i18n.link }}</gl-link>
|
||||
</div>
|
||||
<gl-collapse :visible="visible" />
|
||||
<gl-collapse :visible="visible">
|
||||
<environment-item
|
||||
v-for="(environment, index) in environments"
|
||||
:key="environment.name"
|
||||
:environment="environment"
|
||||
:class="{ 'gl-mt-5': isFirstEnvironment(index) }"
|
||||
class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-pl-7 gl-pt-3"
|
||||
in-folder
|
||||
/>
|
||||
</gl-collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -0,0 +1,239 @@
|
||||
<script>
|
||||
import {
|
||||
GlCollapse,
|
||||
GlDropdown,
|
||||
GlButton,
|
||||
GlLink,
|
||||
GlTooltipDirective as GlTooltip,
|
||||
} from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { truncate } from '~/lib/utils/text_utility';
|
||||
import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql';
|
||||
import ExternalUrl from './environment_external_url.vue';
|
||||
import Actions from './environment_actions.vue';
|
||||
import StopComponent from './environment_stop.vue';
|
||||
import Rollback from './environment_rollback.vue';
|
||||
import Pin from './environment_pin.vue';
|
||||
import Monitoring from './environment_monitoring.vue';
|
||||
import Terminal from './environment_terminal_button.vue';
|
||||
import Delete from './environment_delete.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlCollapse,
|
||||
GlDropdown,
|
||||
GlButton,
|
||||
GlLink,
|
||||
Actions,
|
||||
ExternalUrl,
|
||||
StopComponent,
|
||||
Rollback,
|
||||
Monitoring,
|
||||
Pin,
|
||||
Terminal,
|
||||
Delete,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip,
|
||||
},
|
||||
props: {
|
||||
environment: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
inFolder: {
|
||||
required: false,
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
isLastDeployment: {
|
||||
query: isLastDeployment,
|
||||
variables() {
|
||||
return { environment: this.environment };
|
||||
},
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
collapse: __('Collapse'),
|
||||
expand: __('Expand'),
|
||||
},
|
||||
data() {
|
||||
return { visible: false };
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
return this.visible ? 'angle-down' : 'angle-right';
|
||||
},
|
||||
externalUrl() {
|
||||
return this.environment.externalUrl;
|
||||
},
|
||||
name() {
|
||||
return this.inFolder ? this.environment.nameWithoutType : this.environment.name;
|
||||
},
|
||||
label() {
|
||||
return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand;
|
||||
},
|
||||
actions() {
|
||||
if (!this.environment?.lastDeployment) {
|
||||
return [];
|
||||
}
|
||||
const { manualActions = [], scheduledActions = [] } = this.environment.lastDeployment;
|
||||
const combinedActions = [...manualActions, ...scheduledActions];
|
||||
return combinedActions.map((action) => ({
|
||||
...action,
|
||||
}));
|
||||
},
|
||||
canStop() {
|
||||
return this.environment?.canStop;
|
||||
},
|
||||
retryPath() {
|
||||
return this.environment?.lastDeployment?.deployable?.retryPath;
|
||||
},
|
||||
hasExtraActions() {
|
||||
return Boolean(
|
||||
this.retryPath ||
|
||||
this.canShowAutoStopDate ||
|
||||
this.metricsPath ||
|
||||
this.terminalPath ||
|
||||
this.canDeleteEnvironment,
|
||||
);
|
||||
},
|
||||
canShowAutoStopDate() {
|
||||
if (!this.environment?.autoStopAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const autoStopDate = new Date(this.environment?.autoStopAt);
|
||||
const now = new Date();
|
||||
|
||||
return now < autoStopDate;
|
||||
},
|
||||
autoStopPath() {
|
||||
return this.environment?.cancelAutoStopPath ?? '';
|
||||
},
|
||||
metricsPath() {
|
||||
return this.environment?.metricsPath ?? '';
|
||||
},
|
||||
terminalPath() {
|
||||
return this.environment?.terminalPath ?? '';
|
||||
},
|
||||
canDeleteEnvironment() {
|
||||
return Boolean(this.environment?.canDelete && this.environment?.deletePath);
|
||||
},
|
||||
displayName() {
|
||||
return truncate(this.name, 80);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCollapse() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="gl-px-3 gl-pt-3 gl-pb-5 gl-display-flex gl-justify-content-space-between gl-align-items-center"
|
||||
>
|
||||
<div class="gl-min-w-0 gl-mr-4 gl-display-flex gl-align-items-center">
|
||||
<gl-button
|
||||
class="gl-mr-4 gl-min-w-fit-content"
|
||||
:icon="icon"
|
||||
:aria-label="label"
|
||||
size="small"
|
||||
category="tertiary"
|
||||
@click="toggleCollapse"
|
||||
/>
|
||||
<gl-link
|
||||
v-gl-tooltip
|
||||
:href="environment.environmentPath"
|
||||
class="gl-text-blue-500 gl-text-truncate"
|
||||
:class="{ 'gl-font-weight-bold': visible }"
|
||||
:title="name"
|
||||
>
|
||||
{{ displayName }}
|
||||
</gl-link>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group table-action-buttons" role="group">
|
||||
<external-url
|
||||
v-if="externalUrl"
|
||||
:external-url="externalUrl"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_url"
|
||||
/>
|
||||
|
||||
<actions
|
||||
v-if="actions.length > 0"
|
||||
:actions="actions"
|
||||
data-track-action="click_dropdown"
|
||||
data-track-label="environment_actions"
|
||||
graphql
|
||||
/>
|
||||
|
||||
<stop-component
|
||||
v-if="canStop"
|
||||
:environment="environment"
|
||||
class="gl-z-index-2"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_stop"
|
||||
graphql
|
||||
/>
|
||||
|
||||
<gl-dropdown
|
||||
v-if="hasExtraActions"
|
||||
icon="ellipsis_v"
|
||||
text-sr-only
|
||||
:text="__('More actions')"
|
||||
category="secondary"
|
||||
no-caret
|
||||
right
|
||||
>
|
||||
<rollback
|
||||
v-if="retryPath"
|
||||
:environment="environment"
|
||||
:is-last-deployment="isLastDeployment"
|
||||
:retry-url="retryPath"
|
||||
graphql
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_rollback"
|
||||
/>
|
||||
|
||||
<pin
|
||||
v-if="canShowAutoStopDate"
|
||||
:auto-stop-url="autoStopPath"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_pin"
|
||||
/>
|
||||
|
||||
<monitoring
|
||||
v-if="metricsPath"
|
||||
:monitoring-url="metricsPath"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_monitoring"
|
||||
/>
|
||||
|
||||
<terminal
|
||||
v-if="terminalPath"
|
||||
:terminal-path="terminalPath"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_terminal"
|
||||
/>
|
||||
|
||||
<delete
|
||||
v-if="canDeleteEnvironment"
|
||||
:environment="environment"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_delete"
|
||||
graphql
|
||||
/>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<gl-collapse :visible="visible" />
|
||||
</div>
|
||||
</template>
|
@ -5,20 +5,28 @@ import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_util
|
||||
import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
|
||||
import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
|
||||
import pageInfoQuery from '../graphql/queries/page_info.query.graphql';
|
||||
import environmentToDeleteQuery from '../graphql/queries/environment_to_delete.query.graphql';
|
||||
import environmentToRollbackQuery from '../graphql/queries/environment_to_rollback.query.graphql';
|
||||
import environmentToStopQuery from '../graphql/queries/environment_to_stop.query.graphql';
|
||||
import EnvironmentFolder from './new_environment_folder.vue';
|
||||
import EnableReviewAppModal from './enable_review_app_modal.vue';
|
||||
import StopEnvironmentModal from './stop_environment_modal.vue';
|
||||
import EnvironmentItem from './new_environment_item.vue';
|
||||
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
|
||||
import DeleteEnvironmentModal from './delete_environment_modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DeleteEnvironmentModal,
|
||||
ConfirmRollbackModal,
|
||||
EnvironmentFolder,
|
||||
EnableReviewAppModal,
|
||||
EnvironmentItem,
|
||||
StopEnvironmentModal,
|
||||
GlBadge,
|
||||
GlPagination,
|
||||
GlTab,
|
||||
GlTabs,
|
||||
StopEnvironmentModal,
|
||||
},
|
||||
apollo: {
|
||||
environmentApp: {
|
||||
@ -39,6 +47,12 @@ export default {
|
||||
pageInfo: {
|
||||
query: pageInfoQuery,
|
||||
},
|
||||
environmentToDelete: {
|
||||
query: environmentToDeleteQuery,
|
||||
},
|
||||
environmentToRollback: {
|
||||
query: environmentToRollbackQuery,
|
||||
},
|
||||
environmentToStop: {
|
||||
query: environmentToStopQuery,
|
||||
},
|
||||
@ -63,6 +77,8 @@ export default {
|
||||
isReviewAppModalVisible: false,
|
||||
page: parseInt(page, 10),
|
||||
scope,
|
||||
environmentToDelete: {},
|
||||
environmentToRollback: {},
|
||||
environmentToStop: {},
|
||||
};
|
||||
},
|
||||
@ -71,7 +87,10 @@ export default {
|
||||
return this.environmentApp?.reviewApp?.canSetupReviewApp;
|
||||
},
|
||||
folders() {
|
||||
return this.environmentApp?.environments.filter((e) => e.size > 1) ?? [];
|
||||
return this.environmentApp?.environments?.filter((e) => e.size > 1) ?? [];
|
||||
},
|
||||
environments() {
|
||||
return this.environmentApp?.environments?.filter((e) => e.size === 1) ?? [];
|
||||
},
|
||||
availableCount() {
|
||||
return this.environmentApp?.availableCount;
|
||||
@ -164,7 +183,9 @@ export default {
|
||||
:modal-id="$options.modalId"
|
||||
data-testid="enable-review-app-modal"
|
||||
/>
|
||||
<delete-environment-modal :environment="environmentToDelete" graphql />
|
||||
<stop-environment-modal :environment="environmentToStop" graphql />
|
||||
<confirm-rollback-modal :environment="environmentToRollback" graphql />
|
||||
<gl-tabs
|
||||
:action-secondary="addEnvironment"
|
||||
:action-primary="openReviewAppModal"
|
||||
@ -195,6 +216,12 @@ export default {
|
||||
class="gl-mb-3"
|
||||
:nested-environment="folder"
|
||||
/>
|
||||
<environment-item
|
||||
v-for="environment in environments"
|
||||
:key="environment.name"
|
||||
class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid"
|
||||
:environment="environment.latest"
|
||||
/>
|
||||
<gl-pagination
|
||||
align="center"
|
||||
:total-items="totalItems"
|
||||
|
@ -0,0 +1,5 @@
|
||||
mutation action($action: LocalAction) {
|
||||
action(action: $action) @client {
|
||||
errors
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
query isLastDeployment($environment: LocalEnvironment) {
|
||||
isLastDeployment(environment: $environment) @client
|
||||
}
|
@ -66,8 +66,7 @@ export const resolvers = (endpoint) => ({
|
||||
}));
|
||||
},
|
||||
isLastDeployment(_, { environment }) {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return environment?.lastDeployment?.['last?'];
|
||||
return environment?.lastDeployment?.isLast;
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
@ -115,6 +114,14 @@ export const resolvers = (endpoint) => ({
|
||||
data: { environmentToStop: environment },
|
||||
});
|
||||
},
|
||||
action(_, { action: { playPath } }) {
|
||||
return axios
|
||||
.post(playPath)
|
||||
.then(() => buildErrors())
|
||||
.catch(() =>
|
||||
buildErrors([s__('Environments|An error occurred while making the request.')]),
|
||||
);
|
||||
},
|
||||
setEnvironmentToDelete(_, { environment }, { client }) {
|
||||
client.writeQuery({
|
||||
query: environmentToDeleteQuery,
|
||||
|
@ -70,7 +70,7 @@ extend type Query {
|
||||
environmentToRollback: LocalEnvironment
|
||||
environmentToStop: LocalEnvironment
|
||||
isEnvironmentStopping(environment: LocalEnvironmentInput): Boolean
|
||||
isLastDeployment: Boolean
|
||||
isLastDeployment(environment: LocalEnvironmentInput): Boolean
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
@ -81,4 +81,5 @@ extend type Mutation {
|
||||
setEnvironmentToDelete(environment: LocalEnvironmentInput): LocalErrors
|
||||
setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors
|
||||
setEnvironmentToStop(environment: LocalEnvironmentInput): LocalErrors
|
||||
action(environment: LocalEnvironmentInput): LocalErrors
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ module HooksHelper
|
||||
|
||||
def hook_log_path(hook, hook_log)
|
||||
case hook
|
||||
when ProjectHook
|
||||
when ProjectHook, ServiceHook
|
||||
hook_log.present.details_path
|
||||
when SystemHook
|
||||
admin_hook_hook_log_path(hook, hook_log)
|
||||
|
@ -14,68 +14,84 @@ GitLab can be used as a dependency proxy for a variety of common package manager
|
||||
This is the administration documentation. If you want to learn how to use the
|
||||
dependency proxies, see the [user guide](../../user/packages/dependency_proxy/index.md).
|
||||
|
||||
## Enabling the Dependency Proxy feature
|
||||
The GitLab Dependency Proxy:
|
||||
|
||||
NOTE:
|
||||
Dependency proxy requires the Puma web server to be enabled.
|
||||
- Is turned on by default.
|
||||
- Can be turned off by an administrator.
|
||||
- Requires the [Puma web server](../operations/puma.md)
|
||||
to be enabled. Puma is enabled by default in GitLab 13.0 and later.
|
||||
|
||||
To enable the dependency proxy feature:
|
||||
## Turn off the Dependency Proxy
|
||||
|
||||
**Omnibus GitLab installations**
|
||||
The Dependency Proxy is enabled by default. If you are an administrator, you
|
||||
can turn off the Dependency Proxy. To turn off the Dependency Proxy, follow the instructions that
|
||||
correspond to your GitLab installation:
|
||||
|
||||
- [Omnibus GitLab installations](#omnibus-gitlab-installations)
|
||||
- [Helm chart installations](#helm-chart-installations)
|
||||
- [Installations from source](#installations-from-source)
|
||||
|
||||
### Omnibus GitLab installations
|
||||
|
||||
1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
|
||||
|
||||
```ruby
|
||||
gitlab_rails['dependency_proxy_enabled'] = true
|
||||
gitlab_rails['dependency_proxy_enabled'] = false
|
||||
```
|
||||
|
||||
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
|
||||
1. Enable the [Puma web server](../operations/puma.md).
|
||||
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure)
|
||||
for the changes to take effect.
|
||||
|
||||
**Helm chart installations**
|
||||
### Helm chart installations
|
||||
|
||||
1. After the installation is complete, update the global `appConfig` to enable the feature:
|
||||
After the installation is complete, update the global `appConfig` to turn off the Dependency Proxy:
|
||||
|
||||
```yaml
|
||||
global:
|
||||
appConfig:
|
||||
dependencyProxy:
|
||||
enabled: true
|
||||
bucket: gitlab-dependency-proxy
|
||||
connection: {}
|
||||
secret:
|
||||
key:
|
||||
```
|
||||
```yaml
|
||||
global:
|
||||
appConfig:
|
||||
dependencyProxy:
|
||||
enabled: false
|
||||
bucket: gitlab-dependency-proxy
|
||||
connection: {}
|
||||
secret:
|
||||
key:
|
||||
```
|
||||
|
||||
For more information, see [Configure Charts using Globals](https://docs.gitlab.com/charts/charts/globals.html#configure-appconfig-settings).
|
||||
|
||||
**Installations from source**
|
||||
### Installations from source
|
||||
|
||||
1. After the installation is complete, configure the `dependency_proxy`
|
||||
section in `config/gitlab.yml`. Set to `true` to enable it:
|
||||
1. After the installation is complete, configure the `dependency_proxy` section in
|
||||
`config/gitlab.yml`. Set `enabled` to `false` to turn off the Dependency Proxy:
|
||||
|
||||
```yaml
|
||||
dependency_proxy:
|
||||
enabled: true
|
||||
enabled: false
|
||||
```
|
||||
|
||||
1. [Restart GitLab](../restart_gitlab.md#installations-from-source "How to restart GitLab") for the changes to take effect.
|
||||
1. [Restart GitLab](../restart_gitlab.md#installations-from-source "How to restart GitLab")
|
||||
for the changes to take effect.
|
||||
|
||||
Since Puma is already the default web server for installations from source as of GitLab 12.9,
|
||||
no further changes are needed.
|
||||
### Multi-node GitLab installations
|
||||
|
||||
**Multi-node GitLab installations**
|
||||
Follow the steps for [Omnibus GitLab installations](#omnibus-gitlab-installations)
|
||||
for each Web and Sidekiq node.
|
||||
|
||||
Follow the steps for **Omnibus GitLab installation** for each Web and Sidekiq nodes.
|
||||
## Turn on the Dependency Proxy
|
||||
|
||||
The Dependency Proxy is turned on by default, but can be turned off by an
|
||||
administrator. To turn on the Dependency Proxy, follow the instructions in
|
||||
[Turn off the Dependency Proxy](#turn-off-the-dependency-proxy),
|
||||
but set the `enabled` fields to `true`.
|
||||
|
||||
## Changing the storage path
|
||||
|
||||
By default, the dependency proxy files are stored locally, but you can change the default
|
||||
By default, the Dependency Proxy files are stored locally, but you can change the default
|
||||
local location or even use object storage.
|
||||
|
||||
### Changing the local storage path
|
||||
|
||||
The dependency proxy files for Omnibus GitLab installations are stored under
|
||||
The Dependency Proxy files for Omnibus GitLab installations are stored under
|
||||
`/var/opt/gitlab/gitlab-rails/shared/dependency_proxy/` and for source
|
||||
installations under `shared/dependency_proxy/` (relative to the Git home directory).
|
||||
To change the local storage path:
|
||||
@ -105,7 +121,7 @@ To change the local storage path:
|
||||
### Using object storage
|
||||
|
||||
Instead of relying on the local storage, you can use an object storage to
|
||||
store the blobs of the dependency proxy.
|
||||
store the blobs of the Dependency Proxy.
|
||||
|
||||
[Read more about using object storage with GitLab](../object_storage.md).
|
||||
|
||||
@ -199,5 +215,3 @@ Feature.disable(:dependency_proxy_for_private_groups)
|
||||
# Re-enable the authentication
|
||||
Feature.enable(:dependency_proxy_for_private_groups)
|
||||
```
|
||||
|
||||
The ability to disable this feature will be [removed in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/276777).
|
||||
|
@ -4,9 +4,14 @@ group: Monitor
|
||||
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
|
||||
---
|
||||
|
||||
# Tracing **(FREE)**
|
||||
# Tracing (DEPRECATED) **(FREE)**
|
||||
|
||||
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/42645) from GitLab Ultimate to GitLab Free in 13.5.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/42645) from GitLab Ultimate to GitLab Free in 13.5.
|
||||
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346540) in GitLab 14.7.
|
||||
|
||||
WARNING:
|
||||
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346540)
|
||||
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
|
||||
|
||||
Tracing provides insight into the performance and health of a deployed application, tracking each
|
||||
function or microservice that handles a given request. Tracing makes it easy to understand the
|
||||
|
@ -5,10 +5,10 @@ info: "To determine the technical writer assigned to the Stage/Group associated
|
||||
type: howto, reference
|
||||
---
|
||||
|
||||
# GitLab and SSH keys **(FREE)**
|
||||
# Use SSH keys to communicate with GitLab **(FREE)**
|
||||
|
||||
Git is a distributed version control system, which means you can work locally,
|
||||
then share or "push" your changes to a server. In this case, the server is GitLab.
|
||||
then share or *push* your changes to a server. In this case, the server you push to is GitLab.
|
||||
|
||||
GitLab uses the SSH protocol to securely communicate with Git.
|
||||
When you use SSH keys to authenticate to the GitLab remote server,
|
||||
|
@ -3,96 +3,83 @@ stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
description: 'This article describes how to install Git on macOS, Ubuntu Linux and Windows.'
|
||||
type: howto
|
||||
---
|
||||
|
||||
# Installing Git **(FREE)**
|
||||
|
||||
To begin contributing to GitLab projects,
|
||||
you must install the Git client on your computer.
|
||||
|
||||
This article shows you how to install Git on macOS, Ubuntu Linux and Windows.
|
||||
|
||||
Information on [installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
To begin contributing to GitLab projects, you must install the appropriate Git client
|
||||
on your computer. Information about [installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
is also available at the official Git website.
|
||||
|
||||
## Install Git on macOS using the Homebrew package manager
|
||||
## Supported operating systems
|
||||
|
||||
Although you can use the version of Git shipped with macOS or install the latest
|
||||
version of Git on macOS by downloading it from the project website, we recommend
|
||||
installing Git with Homebrew to get access to an extensive selection of
|
||||
dependency-managed libraries and applications.
|
||||
Git is available for the following operating systems:
|
||||
|
||||
If you don't need access to any additional development libraries or don't have
|
||||
approximately 15 GB of available disk space for Xcode and Homebrew, use one of
|
||||
the previously mentioned methods.
|
||||
- [macOS](#macos)
|
||||
- [Ubuntu Linux](#ubuntu-linux)
|
||||
- [Microsoft Windows](#windows)
|
||||
|
||||
### Installing Xcode
|
||||
### macOS
|
||||
|
||||
To build dependencies, Homebrew needs the XCode Command Line Tools. Install
|
||||
it by running in your terminal:
|
||||
A version of Git is supplied by macOS. You can use this version, or install the latest
|
||||
version of Git on macOS by downloading it from the project website. We recommend
|
||||
installing Git with [Homebrew](https://brew.sh/index.html). With Homebrew, you can
|
||||
access an extensive selection of libraries and applications, with their dependencies
|
||||
managed for you.
|
||||
|
||||
```shell
|
||||
xcode-select --install
|
||||
```
|
||||
Prerequisites:
|
||||
|
||||
Click **Install** to download and install it. Alternatively, you can install
|
||||
the entire [XCode](https://developer.apple.com/xcode/) package through the
|
||||
macOS App Store.
|
||||
- 15 GB of available disk space for Homebrew and Xcode.
|
||||
- Extra disk space for any additional development libraries.
|
||||
|
||||
### Installing Homebrew
|
||||
To install Git on macOS:
|
||||
|
||||
With Xcode installed, browse to the [Homebrew website](https://brew.sh/index.html)
|
||||
for the official Homebrew installation instructions.
|
||||
1. Open a terminal and install the XCode Command Line Tools:
|
||||
|
||||
### Installing Git via Homebrew
|
||||
```shell
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
With Homebrew installed, you are now ready to install Git.
|
||||
Open a terminal and enter the following command:
|
||||
Alternatively, you can install the entire [XCode](https://developer.apple.com/xcode/)
|
||||
package through the macOS App Store.
|
||||
|
||||
```shell
|
||||
brew install git
|
||||
```
|
||||
1. Select **Install** to download and install XCode Command Line Tools.
|
||||
1. Install Homebrew according to the [official Homebrew installation instructions](https://brew.sh/index.html).
|
||||
1. Install Git by running `brew install git` from your terminal.
|
||||
1. In a terminal, verify that Git works on your computer:
|
||||
|
||||
Congratulations! You should now have Git installed via Homebrew.
|
||||
```shell
|
||||
git --version
|
||||
```
|
||||
|
||||
To verify that Git works on your system, run:
|
||||
### Ubuntu Linux
|
||||
|
||||
```shell
|
||||
git --version
|
||||
```
|
||||
On Ubuntu and other Linux operating systems, use the built-in package manager
|
||||
to install Git:
|
||||
|
||||
Next, read our article on [adding an SSH key to GitLab](../../../ssh/index.md).
|
||||
1. Open a terminal and run these commands to install the latest Git
|
||||
from the officially
|
||||
maintained package archives:
|
||||
|
||||
## Install Git on Ubuntu Linux
|
||||
```shell
|
||||
sudo apt-add-repository ppa:git-core/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install git
|
||||
```
|
||||
|
||||
On Ubuntu and other Linux operating systems
|
||||
it is recommended to use the built-in package manager to install Git.
|
||||
1. To verify that Git works on your computer, run:
|
||||
|
||||
Open a terminal and enter the following commands
|
||||
to install the latest Git from the official Git maintained package archives:
|
||||
```shell
|
||||
git --version
|
||||
```
|
||||
|
||||
```shell
|
||||
sudo apt-add-repository ppa:git-core/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install git
|
||||
```
|
||||
### Windows
|
||||
|
||||
Congratulations! You should now have Git installed via the Ubuntu package manager.
|
||||
Go to the [Git website](https://git-scm.com/), and then download and install Git for Windows.
|
||||
|
||||
To verify that Git works on your system, run:
|
||||
## After you install Git
|
||||
|
||||
```shell
|
||||
git --version
|
||||
```
|
||||
|
||||
Next, read our article on [adding an SSH key to GitLab](../../../ssh/index.md).
|
||||
|
||||
## Installing Git on Windows from the Git website
|
||||
|
||||
Open the [Git website](https://git-scm.com/) and download and install Git for Windows.
|
||||
|
||||
Next, read our article on [adding an SSH key to GitLab](../../../ssh/index.md).
|
||||
After you successfully install Git on your computer, read about [adding an SSH key to GitLab](../../../ssh/index.md).
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
|
@ -74,36 +74,30 @@ Otherwise, to manually go to the **Subscription** area:
|
||||
1. Select the **Terms of Service** checkbox.
|
||||
1. Select **Upload License**.
|
||||
|
||||
## Add your license at install time
|
||||
## Add your license during installation
|
||||
|
||||
A license can be automatically imported at install time by placing a file named
|
||||
`Gitlab.gitlab-license` in `/etc/gitlab/` for Omnibus GitLab, or `config/` for source installations.
|
||||
You can import a license file when you install GitLab.
|
||||
|
||||
You can also specify a custom location and filename for the license:
|
||||
- **For installations from source**
|
||||
- Place the `Gitlab.gitlab-license` file in the `config/` directory.
|
||||
- To specify a custom location and filename for the license, set the
|
||||
`GITLAB_LICENSE_FILE` environment variable with the path to the file:
|
||||
|
||||
- Source installations should set the `GITLAB_LICENSE_FILE` environment
|
||||
variable with the path to a valid GitLab Enterprise Edition license.
|
||||
```shell
|
||||
export GITLAB_LICENSE_FILE="/path/to/license/file"
|
||||
```
|
||||
|
||||
```shell
|
||||
export GITLAB_LICENSE_FILE="/path/to/license/file"
|
||||
```
|
||||
- **For Omnibus package**
|
||||
- Place the `Gitlab.gitlab-license` file in the `/etc/gitlab/` directory.
|
||||
- To specify a custom location and filename for the license, add this entry to `gitlab.rb`:
|
||||
|
||||
- Omnibus GitLab installations should add this entry to `gitlab.rb`:
|
||||
|
||||
```ruby
|
||||
gitlab_rails['initial_license_file'] = "/path/to/license/file"
|
||||
```
|
||||
```ruby
|
||||
gitlab_rails['initial_license_file'] = "/path/to/license/file"
|
||||
```
|
||||
|
||||
WARNING:
|
||||
These methods only add a license at the time of installation. Use the
|
||||
**{admin}** **Admin Area** in the web user interface to renew or upgrade licenses.
|
||||
|
||||
---
|
||||
|
||||
After the license is uploaded, all GitLab Enterprise Edition functionality
|
||||
is active until the end of the license period. When that period ends, the
|
||||
instance will [fall back](#what-happens-when-your-license-expires) to Free-only
|
||||
functionality.
|
||||
These methods only add a license at the time of installation. To renew or upgrade
|
||||
a license, upload the license in the **Admin Area** in the web user interface.
|
||||
|
||||
## What happens when your license expires
|
||||
|
||||
@ -150,39 +144,44 @@ The banner disappears after the new license becomes active.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### There is no Subscription tab in the Admin Area
|
||||
### No Subscription area in the Admin Area
|
||||
|
||||
If you originally installed Community Edition rather than Enterprise Edition you must
|
||||
[upgrade to Enterprise Edition](../../update/index.md#community-to-enterprise-edition)
|
||||
before uploading your license.
|
||||
You cannot upload your license because there is no **Subscription** area.
|
||||
This issue might occur if:
|
||||
|
||||
GitLab.com users can't upload and use a self-managed license. If you
|
||||
want to use paid features on GitLab.com, you can
|
||||
[purchase a separate subscription](../../subscriptions/gitlab_com/index.md).
|
||||
- You're running GitLab Community Edition. Before you upload your license, you
|
||||
must [upgrade to Enterprise Edition](../../update/index.md#community-to-enterprise-edition).
|
||||
- You're using GitLab.com. You cannot upload a self-managed license to GitLab.com.
|
||||
To use paid features on GitLab.com, [purchase a separate subscription](../../subscriptions/gitlab_com/index.md).
|
||||
|
||||
### Users exceed license limit upon renewal
|
||||
|
||||
If you've added new users to your GitLab instance prior to renewal, you may need to
|
||||
purchase additional seats to cover those users. If this is the case, and a license
|
||||
without enough users is uploaded, GitLab displays a message prompting you to purchase
|
||||
additional users. More information on how to determine the required number of users
|
||||
and how to add additional seats can be found in the
|
||||
[licensing FAQ](https://about.gitlab.com/pricing/licensing-faq/).
|
||||
GitLab displays a message prompting you to purchase
|
||||
additional users. This issue occurs if you upload a license that does not have enough
|
||||
users to cover the number of users in your instance.
|
||||
|
||||
In GitLab 14.2 and later, for instances that use a license file, you can exceed the number of purchased users and still activate your license.
|
||||
To fix this issue, purchase additional seats to cover those users.
|
||||
For more information, read the [licensing FAQ](https://about.gitlab.com/pricing/licensing-faq/).
|
||||
|
||||
- If the users over license are less than or equal to 10% of the users in the subscription,
|
||||
the license is applied and the overage is paid in the next true-up.
|
||||
- If the users over license are more than 10% of the users in the subscription,
|
||||
In GitLab 14.2 and later, for instances that use a license file, the following
|
||||
rules apply:
|
||||
|
||||
- If the users over license are less than or equal to 10% of the users in the license
|
||||
file, the license is applied and you pay the overage in the next renewal.
|
||||
- If the users over license are more than 10% of the users in the license file,
|
||||
you cannot apply the license without purchasing more users.
|
||||
|
||||
For example, if you purchased a license for 100 users, you can have 110 users when you activate
|
||||
your license. However, if you have 111, you must purchase more users before you can activate.
|
||||
For example, if you purchase a license for 100 users, you can have 110 users when you activate
|
||||
your license. However, if you have 111 users, you must purchase more users before you can activate
|
||||
the license.
|
||||
|
||||
### There is a connectivity issue
|
||||
### Cannot activate instance due to connectivity error
|
||||
|
||||
In GitLab 14.1 and later, to activate your subscription, your GitLab instance must be connected to the internet.
|
||||
In GitLab 14.1 and later, to activate your subscription with an activation code,
|
||||
your GitLab instance must be connected to the internet.
|
||||
|
||||
If you have an offline or airgapped environment, you can [upload a license file](license.md#activate-gitlab-ee-with-a-license-file) instead.
|
||||
If you have an offline or airgapped environment,
|
||||
[upload a license file](license.md#activate-gitlab-ee-with-a-license-file) instead.
|
||||
|
||||
If you have questions or need assistance activating your instance, please [contact GitLab Support](https://about.gitlab.com/support/#contact-support).
|
||||
If you have questions or need assistance activating your instance,
|
||||
[contact GitLab Support](https://about.gitlab.com/support/#contact-support).
|
||||
|
@ -357,6 +357,10 @@ Ensure your SAML identity provider sends an attribute statement named `Groups` o
|
||||
</saml:AttributeStatement>
|
||||
```
|
||||
|
||||
WARNING:
|
||||
Setting up Group Sync can disconnect users from SAML IDP if there is any mismatch in the configuration. Ensure the
|
||||
`Groups` attribute is included in the SAML response, and the **SAML Group Name** matches the `AttributeValue` attribute.
|
||||
|
||||
Other attribute names such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/groups`
|
||||
are not accepted as a source of groups.
|
||||
See the [SAML troubleshooting page](../../../administration/troubleshooting/group_saml_scim.md)
|
||||
|
@ -19,7 +19,8 @@ upstream image from a registry, acting as a pull-through cache.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The Dependency Proxy is enabled by default but can be [turned off by an administrator](../../../administration/packages/dependency_proxy.md).
|
||||
To use the Dependency Proxy, it must be enabled for the GitLab instance. It's enabled by default,
|
||||
but [administrators can turn it off](../../../administration/packages/dependency_proxy.md).
|
||||
|
||||
### Supported images and packages
|
||||
|
||||
@ -32,13 +33,17 @@ The following images and packages are supported.
|
||||
For a list of planned additions, view the
|
||||
[direction page](https://about.gitlab.com/direction/package/#dependency-proxy).
|
||||
|
||||
## Enable or disable the Dependency Proxy for a group
|
||||
## Enable or turn off the Dependency Proxy for a group
|
||||
|
||||
To enable or disable the Dependency Proxy for a group:
|
||||
To enable or turn off the Dependency Proxy for a group:
|
||||
|
||||
1. Go to your group's **Settings > Packages & Registries**.
|
||||
1. Expand the **Dependency Proxy** section.
|
||||
1. To enable the proxy, turn on **Enable Proxy**. To disable it, turn the toggle off.
|
||||
1. To enable the proxy, turn on **Enable Proxy**. To turn it off, turn the toggle off.
|
||||
|
||||
This setting only affects the Dependency Proxy for a group. Only an administrator can
|
||||
[turn the Dependency Proxy on or off](../../../administration/packages/dependency_proxy.md)
|
||||
for the entire GitLab instance.
|
||||
|
||||
## View the Dependency Proxy
|
||||
|
||||
|
@ -26,7 +26,7 @@ describe('Confirm Rollback Modal Component', () => {
|
||||
commit: {
|
||||
shortId: 'abc0123',
|
||||
},
|
||||
'last?': true,
|
||||
isLast: true,
|
||||
},
|
||||
modalId: 'test',
|
||||
};
|
||||
@ -145,7 +145,7 @@ describe('Confirm Rollback Modal Component', () => {
|
||||
...environment,
|
||||
lastDeployment: {
|
||||
...environment.lastDeployment,
|
||||
'last?': false,
|
||||
isLast: false,
|
||||
},
|
||||
},
|
||||
hasMultipleCommits,
|
||||
@ -167,7 +167,7 @@ describe('Confirm Rollback Modal Component', () => {
|
||||
...environment,
|
||||
lastDeployment: {
|
||||
...environment.lastDeployment,
|
||||
'last?': false,
|
||||
isLast: false,
|
||||
},
|
||||
},
|
||||
hasMultipleCommits,
|
||||
@ -191,7 +191,7 @@ describe('Confirm Rollback Modal Component', () => {
|
||||
...environment,
|
||||
lastDeployment: {
|
||||
...environment.lastDeployment,
|
||||
'last?': true,
|
||||
isLast: true,
|
||||
},
|
||||
},
|
||||
hasMultipleCommits,
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import EnvironmentActions from '~/environments/components/environment_actions.vue';
|
||||
import eventHub from '~/environments/event_hub';
|
||||
import actionMutation from '~/environments/graphql/mutations/action.mutation.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
|
||||
const scheduledJobAction = {
|
||||
name: 'scheduled action',
|
||||
@ -25,12 +29,13 @@ describe('EnvironmentActions Component', () => {
|
||||
const findEnvironmentActionsButton = () =>
|
||||
wrapper.find('[data-testid="environment-actions-button"]');
|
||||
|
||||
function createComponent(props, { mountFn = shallowMount } = {}) {
|
||||
function createComponent(props, { mountFn = shallowMount, options = {} } = {}) {
|
||||
wrapper = mountFn(EnvironmentActions, {
|
||||
propsData: { actions: [], ...props },
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@ -150,4 +155,32 @@ describe('EnvironmentActions Component', () => {
|
||||
expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('graphql', () => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const action = {
|
||||
name: 'bar',
|
||||
play_path: 'https://gitlab.com/play',
|
||||
};
|
||||
|
||||
let mockApollo;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApollo = createMockApollo();
|
||||
createComponent(
|
||||
{ actions: [action], graphql: true },
|
||||
{ options: { apolloProvider: mockApollo } },
|
||||
);
|
||||
});
|
||||
|
||||
it('should trigger a graphql mutation on click', () => {
|
||||
jest.spyOn(mockApollo.defaultClient, 'mutate');
|
||||
findDropdownItem(action).vm.$emit('click');
|
||||
expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
|
||||
mutation: actionMutation,
|
||||
variables: { action },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -477,7 +477,141 @@ export const resolvedEnvironment = {
|
||||
externalUrl: 'https://example.org',
|
||||
environmentType: 'review',
|
||||
nameWithoutType: 'hello',
|
||||
lastDeployment: null,
|
||||
lastDeployment: {
|
||||
id: 78,
|
||||
iid: 24,
|
||||
sha: 'f3ba6dd84f8f891373e9b869135622b954852db1',
|
||||
ref: { name: 'main', refPath: '/h5bp/html5-boilerplate/-/tree/main' },
|
||||
status: 'success',
|
||||
createdAt: '2022-01-07T15:47:27.415Z',
|
||||
deployedAt: '2022-01-07T15:47:32.450Z',
|
||||
tag: false,
|
||||
isLast: true,
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'root',
|
||||
name: 'Administrator',
|
||||
state: 'active',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
webUrl: 'http://gck.test:3000/root',
|
||||
showStatus: false,
|
||||
path: '/root',
|
||||
},
|
||||
deployable: {
|
||||
id: 1014,
|
||||
name: 'deploy-prod',
|
||||
started: '2022-01-07T15:47:31.037Z',
|
||||
complete: true,
|
||||
archived: false,
|
||||
buildPath: '/h5bp/html5-boilerplate/-/jobs/1014',
|
||||
retryPath: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
|
||||
playable: false,
|
||||
scheduled: false,
|
||||
createdAt: '2022-01-07T15:47:27.404Z',
|
||||
updatedAt: '2022-01-07T15:47:32.341Z',
|
||||
status: {
|
||||
icon: 'status_success',
|
||||
text: 'passed',
|
||||
label: 'passed',
|
||||
group: 'success',
|
||||
tooltip: 'passed',
|
||||
hasDetails: true,
|
||||
detailsPath: '/h5bp/html5-boilerplate/-/jobs/1014',
|
||||
illustration: {
|
||||
image:
|
||||
'/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
|
||||
size: 'svg-430',
|
||||
title: 'This job does not have a trace.',
|
||||
},
|
||||
favicon:
|
||||
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
|
||||
action: {
|
||||
icon: 'retry',
|
||||
title: 'Retry',
|
||||
path: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
|
||||
method: 'post',
|
||||
buttonTitle: 'Retry this job',
|
||||
},
|
||||
},
|
||||
},
|
||||
commit: {
|
||||
id: 'f3ba6dd84f8f891373e9b869135622b954852db1',
|
||||
shortId: 'f3ba6dd8',
|
||||
createdAt: '2022-01-07T15:47:26.000+00:00',
|
||||
parentIds: ['3213b6ac17afab99be37d5d38f38c6c8407387cc'],
|
||||
title: 'Update .gitlab-ci.yml file',
|
||||
message: 'Update .gitlab-ci.yml file',
|
||||
authorName: 'Administrator',
|
||||
authorEmail: 'admin@example.com',
|
||||
authoredDate: '2022-01-07T15:47:26.000+00:00',
|
||||
committerName: 'Administrator',
|
||||
committerEmail: 'admin@example.com',
|
||||
committedDate: '2022-01-07T15:47:26.000+00:00',
|
||||
trailers: {},
|
||||
webUrl:
|
||||
'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
|
||||
author: {
|
||||
id: 1,
|
||||
username: 'root',
|
||||
name: 'Administrator',
|
||||
state: 'active',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
webUrl: 'http://gck.test:3000/root',
|
||||
showStatus: false,
|
||||
path: '/root',
|
||||
},
|
||||
authorGravatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
commitUrl:
|
||||
'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
|
||||
commitPath: '/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
|
||||
},
|
||||
manualActions: [
|
||||
{
|
||||
id: 1015,
|
||||
name: 'deploy-staging',
|
||||
started: null,
|
||||
complete: false,
|
||||
archived: false,
|
||||
buildPath: '/h5bp/html5-boilerplate/-/jobs/1015',
|
||||
playPath: '/h5bp/html5-boilerplate/-/jobs/1015/play',
|
||||
playable: true,
|
||||
scheduled: false,
|
||||
createdAt: '2022-01-07T15:47:27.422Z',
|
||||
updatedAt: '2022-01-07T15:47:28.557Z',
|
||||
status: {
|
||||
icon: 'status_manual',
|
||||
text: 'manual',
|
||||
label: 'manual play action',
|
||||
group: 'manual',
|
||||
tooltip: 'manual action',
|
||||
hasDetails: true,
|
||||
detailsPath: '/h5bp/html5-boilerplate/-/jobs/1015',
|
||||
illustration: {
|
||||
image:
|
||||
'/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg',
|
||||
size: 'svg-394',
|
||||
title: 'This job requires a manual action',
|
||||
content:
|
||||
'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.',
|
||||
},
|
||||
favicon:
|
||||
'/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
|
||||
action: {
|
||||
icon: 'play',
|
||||
title: 'Play',
|
||||
path: '/h5bp/html5-boilerplate/-/jobs/1015/play',
|
||||
method: 'post',
|
||||
buttonTitle: 'Trigger this manual action',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
scheduledActions: [],
|
||||
cluster: null,
|
||||
},
|
||||
hasStopAction: false,
|
||||
rolloutStatus: null,
|
||||
environmentPath: '/h5bp/html5-boilerplate/-/environments/41',
|
||||
|
@ -1,4 +1,5 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { s__ } from '~/locale';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { resolvers } from '~/environments/graphql/resolvers';
|
||||
import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql';
|
||||
@ -226,4 +227,21 @@ describe('~/frontend/environments/graphql/resolvers', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('action', () => {
|
||||
it('should POST to the given path', async () => {
|
||||
mock.onPost(ENDPOINT).reply(200);
|
||||
const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
|
||||
|
||||
expect(errors).toEqual({ __typename: 'LocalEnvironmentErrors', errors: [] });
|
||||
});
|
||||
it('should return a nice error message on fail', async () => {
|
||||
mock.onPost(ENDPOINT).reply(500);
|
||||
const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
|
||||
|
||||
expect(errors).toEqual({
|
||||
__typename: 'LocalEnvironmentErrors',
|
||||
errors: [s__('Environments|An error occurred while making the request.')],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,13 @@
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { GlCollapse, GlIcon } from '@gitlab/ui';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { stubTransition } from 'helpers/stub_transition';
|
||||
import { __, s__ } from '~/locale';
|
||||
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
|
||||
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
|
||||
import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
@ -25,13 +28,20 @@ describe('~/environments/components/new_environments_folder.vue', () => {
|
||||
};
|
||||
|
||||
const createWrapper = (propsData, apolloProvider) =>
|
||||
mountExtended(EnvironmentsFolder, { apolloProvider, propsData });
|
||||
mountExtended(EnvironmentsFolder, {
|
||||
apolloProvider,
|
||||
propsData,
|
||||
stubs: { transition: stubTransition() },
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
environmentFolderMock = jest.fn();
|
||||
[nestedEnvironment] = resolvedEnvironmentsApp.environments;
|
||||
environmentFolderMock.mockReturnValue(resolvedFolder);
|
||||
wrapper = createWrapper({ nestedEnvironment }, createApolloProvider());
|
||||
|
||||
await nextTick();
|
||||
await waitForPromises();
|
||||
folderName = wrapper.findByText(nestedEnvironment.name);
|
||||
button = wrapper.findByRole('button', { name: __('Expand') });
|
||||
});
|
||||
@ -57,7 +67,8 @@ describe('~/environments/components/new_environments_folder.vue', () => {
|
||||
const link = findLink();
|
||||
|
||||
expect(collapse.attributes('visible')).toBeUndefined();
|
||||
expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-right', 'folder-o']);
|
||||
const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
|
||||
expect(iconNames).toEqual(['angle-right', 'folder-o']);
|
||||
expect(folderName.classes('gl-font-weight-bold')).toBe(false);
|
||||
expect(link.exists()).toBe(false);
|
||||
});
|
||||
@ -68,10 +79,21 @@ describe('~/environments/components/new_environments_folder.vue', () => {
|
||||
const link = findLink();
|
||||
|
||||
expect(button.attributes('aria-label')).toBe(__('Collapse'));
|
||||
expect(collapse.attributes('visible')).toBe('true');
|
||||
expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-down', 'folder-open']);
|
||||
expect(collapse.attributes('visible')).toBe('visible');
|
||||
const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
|
||||
expect(iconNames).toEqual(['angle-down', 'folder-open']);
|
||||
expect(folderName.classes('gl-font-weight-bold')).toBe(true);
|
||||
expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
|
||||
});
|
||||
|
||||
it('displays all environments when opened', async () => {
|
||||
await button.trigger('click');
|
||||
|
||||
const names = resolvedFolder.environments.map((e) =>
|
||||
expect.stringMatching(e.nameWithoutType),
|
||||
);
|
||||
const environments = wrapper.findAllComponents(EnvironmentItem).wrappers.map((w) => w.text());
|
||||
expect(environments).toEqual(expect.arrayContaining(names));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
284
spec/frontend/environments/new_environment_item_spec.js
Normal file
284
spec/frontend/environments/new_environment_item_spec.js
Normal file
@ -0,0 +1,284 @@
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import { GlCollapse, GlIcon } from '@gitlab/ui';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { stubTransition } from 'helpers/stub_transition';
|
||||
import { __, s__ } from '~/locale';
|
||||
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
|
||||
import { resolvedEnvironment } from './graphql/mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('~/environments/components/new_environment_item.vue', () => {
|
||||
let wrapper;
|
||||
|
||||
const createApolloProvider = () => {
|
||||
return createMockApollo();
|
||||
};
|
||||
|
||||
const createWrapper = ({ propsData = {}, apolloProvider } = {}) =>
|
||||
mountExtended(EnvironmentItem, {
|
||||
apolloProvider,
|
||||
propsData: { environment: resolvedEnvironment, ...propsData },
|
||||
stubs: { transition: stubTransition() },
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper?.destroy();
|
||||
});
|
||||
|
||||
it('displays the name when not in a folder', () => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
|
||||
const name = wrapper.findByRole('link', { name: resolvedEnvironment.name });
|
||||
expect(name.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays the name minus the folder prefix when in a folder', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: { inFolder: true },
|
||||
apolloProvider: createApolloProvider(),
|
||||
});
|
||||
|
||||
const name = wrapper.findByRole('link', { name: resolvedEnvironment.nameWithoutType });
|
||||
expect(name.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('truncates the name if it is very long', () => {
|
||||
const environment = {
|
||||
...resolvedEnvironment,
|
||||
name:
|
||||
'this is a really long name that should be truncated because otherwise it would look strange in the UI',
|
||||
};
|
||||
wrapper = createWrapper({ propsData: { environment }, apolloProvider: createApolloProvider() });
|
||||
|
||||
const name = wrapper.findByRole('link', {
|
||||
name: (text) => environment.name.startsWith(text.slice(0, -1)),
|
||||
});
|
||||
expect(name.exists()).toBe(true);
|
||||
expect(name.text()).toHaveLength(80);
|
||||
});
|
||||
|
||||
describe('url', () => {
|
||||
it('shows a link for the url if one is present', () => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
|
||||
const url = wrapper.findByRole('link', { name: s__('Environments|Open live environment') });
|
||||
|
||||
expect(url.attributes('href')).toEqual(resolvedEnvironment.externalUrl);
|
||||
});
|
||||
|
||||
it('does not show a link for the url if one is missing', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: { environment: { ...resolvedEnvironment, externalUrl: '' } },
|
||||
apolloProvider: createApolloProvider(),
|
||||
});
|
||||
|
||||
const url = wrapper.findByRole('link', { name: s__('Environments|Open live environment') });
|
||||
|
||||
expect(url.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('shows a dropdown if there are actions to perform', () => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
|
||||
const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
|
||||
|
||||
expect(actions.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show a dropdown if there are no actions to perform', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: {
|
||||
environment: {
|
||||
...resolvedEnvironment,
|
||||
lastDeployment: null,
|
||||
},
|
||||
apolloProvider: createApolloProvider(),
|
||||
},
|
||||
});
|
||||
|
||||
const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
|
||||
|
||||
expect(actions.exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('passes all the actions down to the action component', () => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
|
||||
const action = wrapper.findByRole('menuitem', { name: 'deploy-staging' });
|
||||
|
||||
expect(action.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('shows a buton to stop the environment if the environment is available', () => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
|
||||
const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
|
||||
|
||||
expect(stop.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show a buton to stop the environment if the environment is stopped', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: { environment: { ...resolvedEnvironment, canStop: false } },
|
||||
apolloProvider: createApolloProvider(),
|
||||
});
|
||||
|
||||
const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
|
||||
|
||||
expect(stop.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rollback', () => {
|
||||
it('shows the option to rollback/re-deploy if available', () => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
|
||||
const rollback = wrapper.findByRole('menuitem', {
|
||||
name: s__('Environments|Re-deploy to environment'),
|
||||
});
|
||||
|
||||
expect(rollback.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the option to rollback/re-deploy if not available', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: { environment: { ...resolvedEnvironment, lastDeployment: null } },
|
||||
apolloProvider: createApolloProvider(),
|
||||
});
|
||||
|
||||
const rollback = wrapper.findByRole('menuitem', {
|
||||
name: s__('Environments|Re-deploy to environment'),
|
||||
});
|
||||
|
||||
expect(rollback.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pin', () => {
|
||||
it('shows the option to pin the environment if there is an autostop date', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: {
|
||||
environment: { ...resolvedEnvironment, autoStopAt: new Date(Date.now() + 100000) },
|
||||
},
|
||||
apolloProvider: createApolloProvider(),
|
||||
});
|
||||
|
||||
const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
|
||||
|
||||
expect(rollback.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the option to pin the environment if there is no autostop date', () => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
|
||||
const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
|
||||
|
||||
expect(rollback.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('monitoring', () => {
|
||||
it('shows the link to monitoring if metrics are set up', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: { environment: { ...resolvedEnvironment, metricsPath: '/metrics' } },
|
||||
apolloProvider: createApolloProvider(),
|
||||
});
|
||||
|
||||
const rollback = wrapper.findByRole('menuitem', { name: __('Monitoring') });
|
||||
|
||||
expect(rollback.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the link to monitoring if metrics are not set up', () => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
|
||||
const rollback = wrapper.findByRole('menuitem', { name: __('Monitoring') });
|
||||
|
||||
expect(rollback.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('terminal', () => {
|
||||
it('shows the link to the terminal if set up', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: { environment: { ...resolvedEnvironment, terminalPath: '/terminal' } },
|
||||
apolloProvider: createApolloProvider(),
|
||||
});
|
||||
|
||||
const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') });
|
||||
|
||||
expect(rollback.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the link to the terminal if not set up', () => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
|
||||
const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') });
|
||||
|
||||
expect(rollback.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('shows the button to delete the environment if possible', () => {
|
||||
wrapper = createWrapper({
|
||||
propsData: {
|
||||
environment: { ...resolvedEnvironment, canDelete: true, deletePath: '/terminal' },
|
||||
},
|
||||
apolloProvider: createApolloProvider(),
|
||||
});
|
||||
|
||||
const rollback = wrapper.findByRole('menuitem', {
|
||||
name: s__('Environments|Delete environment'),
|
||||
});
|
||||
|
||||
expect(rollback.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the button to delete the environment if not possible', () => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
|
||||
const rollback = wrapper.findByRole('menuitem', {
|
||||
name: s__('Environments|Delete environment'),
|
||||
});
|
||||
|
||||
expect(rollback.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collapse', () => {
|
||||
let icon;
|
||||
let collapse;
|
||||
let button;
|
||||
let environmentName;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
|
||||
collapse = wrapper.findComponent(GlCollapse);
|
||||
icon = wrapper.findComponent(GlIcon);
|
||||
button = wrapper.findByRole('button', { name: __('Expand') });
|
||||
environmentName = wrapper.findByText(resolvedEnvironment.name);
|
||||
});
|
||||
|
||||
it('is collapsed by default', () => {
|
||||
expect(collapse.attributes('visible')).toBeUndefined();
|
||||
expect(icon.props('name')).toEqual('angle-right');
|
||||
expect(environmentName.classes('gl-font-weight-bold')).toBe(false);
|
||||
});
|
||||
|
||||
it('opens on click', async () => {
|
||||
await button.trigger('click');
|
||||
|
||||
expect(button.attributes('aria-label')).toBe(__('Collapse'));
|
||||
expect(collapse.attributes('visible')).toBe('visible');
|
||||
expect(icon.props('name')).toEqual('angle-down');
|
||||
expect(environmentName.classes('gl-font-weight-bold')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -8,6 +8,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { sprintf, __, s__ } from '~/locale';
|
||||
import EnvironmentsApp from '~/environments/components/new_environments_app.vue';
|
||||
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
|
||||
import EnvironmentsItem from '~/environments/components/new_environment_item.vue';
|
||||
import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
|
||||
import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data';
|
||||
|
||||
@ -93,6 +94,18 @@ describe('~/environments/components/new_environments_app.vue', () => {
|
||||
expect(text).not.toContainEqual(expect.stringMatching('production'));
|
||||
});
|
||||
|
||||
it('should show all the environments that are fetched', async () => {
|
||||
await createWrapperWithMocked({
|
||||
environmentsApp: resolvedEnvironmentsApp,
|
||||
folder: resolvedFolder,
|
||||
});
|
||||
|
||||
const text = wrapper.findAllComponents(EnvironmentsItem).wrappers.map((w) => w.text());
|
||||
|
||||
expect(text).not.toContainEqual(expect.stringMatching('review'));
|
||||
expect(text).toContainEqual(expect.stringMatching('production'));
|
||||
});
|
||||
|
||||
it('should show a button to create a new environment', async () => {
|
||||
await createWrapperWithMocked({
|
||||
environmentsApp: resolvedEnvironmentsApp,
|
||||
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
||||
RSpec.describe HooksHelper do
|
||||
let(:project) { create(:project) }
|
||||
let(:project_hook) { create(:project_hook, project: project) }
|
||||
let(:service_hook) { create(:service_hook, integration: create(:drone_ci_integration)) }
|
||||
let(:system_hook) { create(:system_hook) }
|
||||
|
||||
describe '#link_to_test_hook' do
|
||||
@ -31,6 +32,15 @@ RSpec.describe HooksHelper do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a service hook' do
|
||||
let(:web_hook_log) { create(:web_hook_log, web_hook: service_hook) }
|
||||
|
||||
it 'returns project-namespaced link' do
|
||||
expect(helper.hook_log_path(project_hook, web_hook_log))
|
||||
.to eq(web_hook_log.present.details_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a system hook' do
|
||||
let(:web_hook_log) { create(:web_hook_log, web_hook: system_hook) }
|
||||
|
||||
|
Reference in New Issue
Block a user