Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5f5a1d09aa
commit
ef826d81c6
|
|
@ -101,11 +101,9 @@ export default {
|
|||
'app/assets/javascripts/environments/components/enable_review_app_modal.vue',
|
||||
'app/assets/javascripts/environments/components/environment_flux_resource_selector.vue',
|
||||
'app/assets/javascripts/environments/components/environment_form.vue',
|
||||
'app/assets/javascripts/environments/components/environment_item.vue',
|
||||
'app/assets/javascripts/environments/environment_details/components/deployment_actions.vue',
|
||||
'app/assets/javascripts/environments/environment_details/components/deployment_history.vue',
|
||||
'app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue',
|
||||
'app/assets/javascripts/environments/folder/environments_folder_view.vue',
|
||||
'app/assets/javascripts/error_tracking/components/error_details.vue',
|
||||
'app/assets/javascripts/error_tracking/components/error_tracking_list.vue',
|
||||
'app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue',
|
||||
|
|
@ -367,7 +365,6 @@ export default {
|
|||
'app/assets/javascripts/work_items/components/work_item_detail_modal.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_development/work_item_create_branch_merge_request_modal.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_development/work_item_development_mr_item.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_due_date.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_labels.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_links/work_item_groups_listbox.vue',
|
||||
|
|
@ -596,7 +593,6 @@ export default {
|
|||
'ee/app/assets/javascripts/work_items/components/work_item_iteration.vue',
|
||||
'ee/app/assets/javascripts/work_items/components/work_item_links/work_item_rolled_up_health_status.vue',
|
||||
'ee/app/assets/javascripts/work_items/components/work_item_progress.vue',
|
||||
'ee/app/assets/javascripts/work_items/components/work_item_rolledup_dates.vue',
|
||||
'ee/app/assets/javascripts/workspaces/common/components/workspaces_list/workspaces_table.vue',
|
||||
'ee/app/assets/javascripts/workspaces/dropdown_group/components/workspace_dropdown_item.vue',
|
||||
'ee/app/assets/javascripts/workspaces/user/pages/list.vue',
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@
|
|||
|
||||
.zoekt-services:
|
||||
services:
|
||||
- name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:zoekt-ci-image-1.9
|
||||
- name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:zoekt-ci-image-1.10
|
||||
alias: zoekt-ci-image
|
||||
|
||||
.use-pg14:
|
||||
|
|
|
|||
|
|
@ -201,7 +201,6 @@ Rails/Pluck:
|
|||
- 'spec/requests/api/members_spec.rb'
|
||||
- 'spec/requests/api/merge_requests_spec.rb'
|
||||
- 'spec/requests/api/namespaces_spec.rb'
|
||||
- 'spec/requests/api/package_files_spec.rb'
|
||||
- 'spec/requests/api/pages_domains_spec.rb'
|
||||
- 'spec/requests/api/personal_access_tokens_spec.rb'
|
||||
- 'spec/requests/api/project_clusters_spec.rb'
|
||||
|
|
|
|||
|
|
@ -1108,7 +1108,6 @@ RSpec/BeforeAllRoleAssignment:
|
|||
- 'spec/requests/api/maven_packages_spec.rb'
|
||||
- 'spec/requests/api/merge_request_approvals_spec.rb'
|
||||
- 'spec/requests/api/merge_requests_spec.rb'
|
||||
- 'spec/requests/api/package_files_spec.rb'
|
||||
- 'spec/requests/api/pages/internal_access_spec.rb'
|
||||
- 'spec/requests/api/pages/private_access_spec.rb'
|
||||
- 'spec/requests/api/pages/public_access_spec.rb'
|
||||
|
|
|
|||
|
|
@ -2189,7 +2189,6 @@ RSpec/ContextWording:
|
|||
- 'spec/requests/api/notes_spec.rb'
|
||||
- 'spec/requests/api/npm_project_packages_spec.rb'
|
||||
- 'spec/requests/api/oauth_tokens_spec.rb'
|
||||
- 'spec/requests/api/package_files_spec.rb'
|
||||
- 'spec/requests/api/pages/internal_access_spec.rb'
|
||||
- 'spec/requests/api/pages/private_access_spec.rb'
|
||||
- 'spec/requests/api/pages/public_access_spec.rb'
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ RSpec/RepeatedSubjectCall:
|
|||
- 'spec/models/environment_spec.rb'
|
||||
- 'spec/models/packages/package_file_spec.rb'
|
||||
- 'spec/requests/api/groups_spec.rb'
|
||||
- 'spec/requests/api/package_files_spec.rb'
|
||||
- 'spec/requests/api/projects_spec.rb'
|
||||
- 'spec/requests/api/releases_spec.rb'
|
||||
- 'spec/requests/api/users_spec.rb'
|
||||
|
|
|
|||
|
|
@ -13,11 +13,6 @@ export default {
|
|||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
graphql: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
ingressOptions: Array(100 / 5 + 1)
|
||||
.fill(0)
|
||||
|
|
@ -51,10 +46,7 @@ export default {
|
|||
return uniqueId('canary-weight-');
|
||||
},
|
||||
weight() {
|
||||
if (this.graphql) {
|
||||
return this.canaryIngress.canaryWeight;
|
||||
}
|
||||
return this.canaryIngress.canary_weight;
|
||||
return this.canaryIngress.canaryWeight;
|
||||
},
|
||||
stableWeight() {
|
||||
return 100 - this.weight;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { __, s__, sprintf } from '~/locale';
|
|||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
import rollbackEnvironment from '../graphql/mutations/rollback_environment.mutation.graphql';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
name: 'ConfirmRollbackModal',
|
||||
|
|
@ -43,11 +42,6 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
graphql: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
modalTitle() {
|
||||
|
|
@ -61,31 +55,18 @@ export default {
|
|||
},
|
||||
commitShortSha() {
|
||||
if (this.hasMultipleCommits) {
|
||||
if (this.graphql) {
|
||||
const { lastDeployment } = this.environment;
|
||||
return this.commitData(lastDeployment, 'shortId');
|
||||
}
|
||||
|
||||
const { last_deployment } = this.environment;
|
||||
return this.commitData(last_deployment, 'short_id');
|
||||
const { lastDeployment } = this.environment;
|
||||
return this.commitData(lastDeployment, 'shortId');
|
||||
}
|
||||
|
||||
return this.environment.commitShortSha;
|
||||
},
|
||||
commitUrl() {
|
||||
if (this.hasMultipleCommits) {
|
||||
if (this.graphql) {
|
||||
const { lastDeployment } = this.environment;
|
||||
return (
|
||||
// data shape comming from REST and GraphQL is unfortunately different
|
||||
// once we fully migrate to GraphQL it could be streamlined
|
||||
this.commitData(lastDeployment, 'commitPath') ||
|
||||
this.commitData(lastDeployment, 'webUrl')
|
||||
);
|
||||
}
|
||||
|
||||
const { last_deployment } = this.environment;
|
||||
return this.commitData(last_deployment, 'commit_path');
|
||||
const { lastDeployment } = this.environment;
|
||||
return (
|
||||
this.commitData(lastDeployment, 'commitPath') || this.commitData(lastDeployment, 'webUrl')
|
||||
);
|
||||
}
|
||||
|
||||
return this.environment.commitUrl;
|
||||
|
|
@ -125,21 +106,17 @@ export default {
|
|||
this.$emit('change', event);
|
||||
},
|
||||
onOk() {
|
||||
if (this.graphql) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: rollbackEnvironment,
|
||||
variables: { environment: this.environment },
|
||||
})
|
||||
.then(() => {
|
||||
this.$emit('rollback');
|
||||
})
|
||||
.catch((e) => {
|
||||
Sentry.captureException(e);
|
||||
});
|
||||
} else {
|
||||
eventHub.$emit('rollbackEnvironment', this.environment);
|
||||
}
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: rollbackEnvironment,
|
||||
variables: { environment: this.environment },
|
||||
})
|
||||
.then(() => {
|
||||
this.$emit('rollback');
|
||||
})
|
||||
.catch((e) => {
|
||||
Sentry.captureException(e);
|
||||
});
|
||||
},
|
||||
commitData(lastDeployment, key) {
|
||||
return lastDeployment?.commit?.[key] ?? '';
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
|
||||
import EnvironmentTable from './environments_table.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EnvironmentTable,
|
||||
TablePagination,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
environments: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
pagination: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onChangePage(page) {
|
||||
this.$emit('onChangePage', page);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="environments-container">
|
||||
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" label="Loading environments" />
|
||||
|
||||
<slot name="empty-state"></slot>
|
||||
|
||||
<div v-if="!isLoading && environments.length > 0" class="table-holder">
|
||||
<environment-table :environments="environments" />
|
||||
|
||||
<table-pagination
|
||||
v-if="pagination && pagination.totalPages > 1"
|
||||
:change="onChangePage"
|
||||
:page-info="pagination"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
import { GlTooltipDirective, GlModal } from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
import deleteEnvironmentMutation from '../graphql/mutations/delete_environment.mutation.graphql';
|
||||
|
||||
export default {
|
||||
|
|
@ -19,11 +18,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
graphql: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
primaryProps() {
|
||||
|
|
@ -56,30 +50,26 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
if (this.graphql) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: deleteEnvironmentMutation,
|
||||
variables: { environment: this.environment },
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const [message] = data?.deleteEvironment?.errors ?? [];
|
||||
if (message) {
|
||||
createAlert({ message });
|
||||
}
|
||||
})
|
||||
.catch((error) =>
|
||||
createAlert({
|
||||
message: s__(
|
||||
'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
|
||||
),
|
||||
error,
|
||||
captureError: true,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
eventHub.$emit('deleteEnvironment', this.environment);
|
||||
}
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: deleteEnvironmentMutation,
|
||||
variables: { environment: this.environment },
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const [message] = data?.deleteEnvironment?.errors ?? [];
|
||||
if (message) {
|
||||
createAlert({ message });
|
||||
}
|
||||
})
|
||||
.catch((error) =>
|
||||
createAlert({
|
||||
message: s__(
|
||||
'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
|
||||
),
|
||||
error,
|
||||
captureError: true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,11 +52,6 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
graphql: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
canRenderDeployBoard() {
|
||||
|
|
@ -66,11 +61,7 @@ export default {
|
|||
return this.isEmpty;
|
||||
},
|
||||
canaryIngress() {
|
||||
if (this.graphql) {
|
||||
return this.deployBoardData.canaryIngress;
|
||||
}
|
||||
|
||||
return this.deployBoardData.canary_ingress;
|
||||
return this.deployBoardData.canaryIngress;
|
||||
},
|
||||
canRenderCanaryWeight() {
|
||||
return !isEmpty(this.canaryIngress);
|
||||
|
|
@ -95,16 +86,10 @@ export default {
|
|||
return n__('Instance', 'Instances', this.instanceCount);
|
||||
},
|
||||
rollbackUrl() {
|
||||
if (this.graphql) {
|
||||
return this.deployBoardData.rollbackUrl;
|
||||
}
|
||||
return this.deployBoardData.rollback_url;
|
||||
return this.deployBoardData.rollbackUrl;
|
||||
},
|
||||
abortUrl() {
|
||||
if (this.graphql) {
|
||||
return this.deployBoardData.abortUrl;
|
||||
}
|
||||
return this.deployBoardData.abort_url;
|
||||
return this.deployBoardData.abortUrl;
|
||||
},
|
||||
deployBoardActions() {
|
||||
return this.rollbackUrl || this.abortUrl;
|
||||
|
|
@ -123,11 +108,7 @@ export default {
|
|||
this.$emit('changeCanaryWeight', weight);
|
||||
},
|
||||
podName(instance) {
|
||||
if (this.graphql) {
|
||||
return instance.podName;
|
||||
}
|
||||
|
||||
return instance.pod_name;
|
||||
return instance.podName;
|
||||
},
|
||||
},
|
||||
emptyStateText: s__(
|
||||
|
|
@ -189,7 +170,6 @@ export default {
|
|||
v-if="canRenderCanaryWeight"
|
||||
class="deploy-board-canary-ingress"
|
||||
:canary-ingress="canaryIngress"
|
||||
:graphql="graphql"
|
||||
@change="changeCanaryWeight"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ export default {
|
|||
:is-loading="isLoading"
|
||||
:is-empty="isEmpty"
|
||||
:environment="environment"
|
||||
graphql
|
||||
class="!gl-bg-inherit"
|
||||
@changeCanaryWeight="changeCanaryWeight"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { GlIcon, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/
|
|||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
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 {
|
||||
|
|
@ -18,11 +17,6 @@ export default {
|
|||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
graphql: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -61,13 +55,8 @@ export default {
|
|||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
if (this.graphql) {
|
||||
await this.$apollo.mutate({ mutation: actionMutation, variables: { action } });
|
||||
this.isLoading = false;
|
||||
} else {
|
||||
eventHub.$emit('postAction', { endpoint: action.playPath });
|
||||
}
|
||||
await this.$apollo.mutate({ mutation: actionMutation, variables: { action } });
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
||||
isActionDisabled(action) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { GlDisclosureDropdownItem, GlModalDirective } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
import setEnvironmentToDelete from '../graphql/mutations/set_environment_to_delete.mutation.graphql';
|
||||
|
||||
export default {
|
||||
|
|
@ -21,11 +20,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
graphql: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -36,31 +30,12 @@ export default {
|
|||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!this.graphql) {
|
||||
eventHub.$on('deleteEnvironment', this.onDeleteEnvironment);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (!this.graphql) {
|
||||
eventHub.$off('deleteEnvironment', this.onDeleteEnvironment);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
if (this.graphql) {
|
||||
this.$apollo.mutate({
|
||||
mutation: setEnvironmentToDelete,
|
||||
variables: { environment: this.environment },
|
||||
});
|
||||
} else {
|
||||
eventHub.$emit('requestDeleteEnvironment', this.environment);
|
||||
}
|
||||
},
|
||||
onDeleteEnvironment(environment) {
|
||||
if (this.environment.id === environment.id) {
|
||||
this.isLoading = true;
|
||||
}
|
||||
this.$apollo.mutate({
|
||||
mutation: setEnvironmentToDelete,
|
||||
variables: { environment: this.environment },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,859 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
GlDisclosureDropdown,
|
||||
GlTooltipDirective,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
GlBadge,
|
||||
GlAvatar,
|
||||
GlAvatarLink,
|
||||
} from '@gitlab/ui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import CommitComponent from '~/vue_shared/components/commit.vue';
|
||||
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import eventHub from '../event_hub';
|
||||
import ActionsComponent from './environment_actions.vue';
|
||||
import DeleteComponent from './environment_delete.vue';
|
||||
import ExternalUrlComponent from './environment_external_url.vue';
|
||||
import PinComponent from './environment_pin.vue';
|
||||
import RollbackComponent from './environment_rollback.vue';
|
||||
import StopComponent from './environment_stop.vue';
|
||||
import TerminalButtonComponent from './environment_terminal_button.vue';
|
||||
/**
|
||||
* Environment Item Component
|
||||
*
|
||||
* Renders a table row for each environment.
|
||||
*/
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActionsComponent,
|
||||
CommitComponent,
|
||||
ExternalUrlComponent,
|
||||
GlDisclosureDropdown,
|
||||
GlBadge,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
PinComponent,
|
||||
DeleteComponent,
|
||||
RollbackComponent,
|
||||
StopComponent,
|
||||
TerminalButtonComponent,
|
||||
TooltipOnTruncate,
|
||||
GlAvatar,
|
||||
GlAvatarLink,
|
||||
CiIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
tableData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
deployIconName() {
|
||||
return this.model.isDeployBoardVisible ? 'chevron-down' : 'chevron-right';
|
||||
},
|
||||
/**
|
||||
* Verifies if `last_deployment` key exists in the current Environment.
|
||||
* This key is required to render most of the html - this method works has
|
||||
* an helper.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasLastDeploymentKey() {
|
||||
if (this.model && this.model.last_deployment && !isEmpty(this.model.last_deployment)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object|Undefined} The `upcoming_deployment` object if it exists.
|
||||
* Otherwise, `undefined`.
|
||||
*/
|
||||
upcomingDeployment() {
|
||||
return this.model?.upcoming_deployment;
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {String} Text that will be shown in the tooltip when
|
||||
* the user hovers over the upcoming deployment's status icon.
|
||||
*/
|
||||
upcomingDeploymentTooltipText() {
|
||||
return sprintf(s__('Environments|Deployment %{status}'), {
|
||||
status: this.upcomingDeployment.deployable.status.text,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Checkes whether the row displayed is a folder.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
isFolder() {
|
||||
return this.model.isFolder;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checkes whether the environment is protected.
|
||||
* (`is_protected` currently only set in EE)
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isProtected() {
|
||||
return this.model && this.model.is_protected;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether the environment can be stopped.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
canStopEnvironment() {
|
||||
return this.model && this.model.can_stop;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether the environment can be deleted.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
canDeleteEnvironment() {
|
||||
return Boolean(this.model && this.model.can_delete && this.model.delete_path);
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the `deployable` key is present in `last_deployment` key.
|
||||
* Used to verify whether we should or not render the rollback partial.
|
||||
*
|
||||
* @returns {Boolean|Undefined}
|
||||
*/
|
||||
canRetry() {
|
||||
return (
|
||||
this.model &&
|
||||
this.hasLastDeploymentKey &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.deployable &&
|
||||
this.model.last_deployment.deployable.retry_path
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the autostop date is present.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
canShowAutoStopDate() {
|
||||
if (!this.model.auto_stop_at) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const autoStopDate = new Date(this.model.auto_stop_at);
|
||||
const now = new Date();
|
||||
|
||||
return now < autoStopDate;
|
||||
},
|
||||
|
||||
/**
|
||||
* Human readable deployment date.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
autoStopDate() {
|
||||
if (this.canShowAutoStopDate) {
|
||||
return {
|
||||
formatted: this.timeFormatted(this.model.auto_stop_at),
|
||||
tooltip: this.tooltipTitle(this.model.auto_stop_at),
|
||||
};
|
||||
}
|
||||
return {
|
||||
formatted: '',
|
||||
tooltip: '',
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the deployment date is present.
|
||||
*
|
||||
* @returns {Boolean|Undefined}
|
||||
*/
|
||||
canShowDeploymentDate() {
|
||||
return this.model && this.model.last_deployment && this.model.last_deployment.deployed_at;
|
||||
},
|
||||
|
||||
/**
|
||||
* Human readable deployment date.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
deployedDate() {
|
||||
if (this.canShowDeploymentDate) {
|
||||
return {
|
||||
formatted: this.timeFormatted(this.model.last_deployment.deployed_at),
|
||||
tooltip: this.tooltipTitle(this.model.last_deployment.deployed_at),
|
||||
};
|
||||
}
|
||||
return {
|
||||
formatted: '',
|
||||
tooltip: '',
|
||||
};
|
||||
},
|
||||
|
||||
actions() {
|
||||
if (!this.model || !this.model.last_deployment) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { manualActions, scheduledActions } = convertObjectPropsToCamelCase(
|
||||
this.model.last_deployment,
|
||||
{ deep: true },
|
||||
);
|
||||
const combinedActions = (manualActions || []).concat(scheduledActions || []);
|
||||
return combinedActions.map((action) => ({
|
||||
...action,
|
||||
name: action.name,
|
||||
}));
|
||||
},
|
||||
|
||||
shouldRenderDeployBoard() {
|
||||
return this.model.hasDeployBoard;
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the string used in the user image alt attribute.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
userImageAltDescription() {
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.user &&
|
||||
this.model.last_deployment.user.username
|
||||
) {
|
||||
return sprintf(__("%{username}'s avatar"), {
|
||||
username: this.model.last_deployment.user.username,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Same as `userImageAltDescription`, but for the
|
||||
* upcoming deployment's user
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
upcomingDeploymentUserImageAltDescription() {
|
||||
return sprintf(__("%{username}'s avatar"), {
|
||||
username: this.upcomingDeployment.user.username,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit tag.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitTag() {
|
||||
if (this.model && this.model.last_deployment && this.model.last_deployment.tag) {
|
||||
return this.model.last_deployment.tag;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit ref.
|
||||
*
|
||||
* @returns {Object|Undefined}
|
||||
*/
|
||||
commitRef() {
|
||||
if (this.model && this.model.last_deployment && this.model.last_deployment.ref) {
|
||||
return this.model.last_deployment.ref;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit url.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitUrl() {
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.commit &&
|
||||
this.model.last_deployment.commit.commit_path
|
||||
) {
|
||||
return this.model.last_deployment.commit.commit_path;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit short sha.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitShortSha() {
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.commit &&
|
||||
this.model.last_deployment.commit.short_id
|
||||
) {
|
||||
return this.model.last_deployment.commit.short_id;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit title.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
commitTitle() {
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.commit &&
|
||||
this.model.last_deployment.commit.title
|
||||
) {
|
||||
return this.model.last_deployment.commit.title;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit tag.
|
||||
*
|
||||
* @returns {Object|Undefined}
|
||||
*/
|
||||
commitAuthor() {
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.commit &&
|
||||
this.model.last_deployment.commit.author
|
||||
) {
|
||||
return this.model.last_deployment.commit.author;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the `retry_path` key is present and returns its value.
|
||||
*
|
||||
* @returns {String|Undefined}
|
||||
*/
|
||||
retryUrl() {
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.deployable &&
|
||||
this.model.last_deployment.deployable.retry_path
|
||||
) {
|
||||
return this.model.last_deployment.deployable.retry_path;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the `last?` key is present and returns its value.
|
||||
*
|
||||
* @returns {Boolean|Undefined}
|
||||
*/
|
||||
isLastDeployment() {
|
||||
// name: 'last?' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
|
||||
// Vue i18n ESLint rules issue: https://gitlab.com/gitlab-org/gitlab-foss/issues/63560
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return this.model && this.model.last_deployment && this.model.last_deployment['last?'];
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the name of the builds needed to display both the name and the id.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
buildName() {
|
||||
if (this.model && this.model.last_deployment && this.model.last_deployment.deployable) {
|
||||
const { deployable } = this.model.last_deployment;
|
||||
return `${deployable.name} #${deployable.id}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the needed string to show the internal id.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
deploymentInternalId() {
|
||||
if (this.model && this.model.last_deployment && this.model.last_deployment.iid) {
|
||||
return `#${this.model.last_deployment.iid}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Same as `deploymentInternalId`, but for the upcoming deployment
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
upcomingDeploymentInternalId() {
|
||||
return `#${this.upcomingDeployment.iid}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the user object is present under last_deployment object.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
deploymentHasUser() {
|
||||
return (
|
||||
this.model &&
|
||||
!isEmpty(this.model.last_deployment) &&
|
||||
!isEmpty(this.model.last_deployment.user)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the user object nested with the last_deployment object.
|
||||
* Used to render the template.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
deploymentUser() {
|
||||
if (
|
||||
this.model &&
|
||||
!isEmpty(this.model.last_deployment) &&
|
||||
!isEmpty(this.model.last_deployment.user)
|
||||
) {
|
||||
return this.model.last_deployment.user;
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Checkes whether to display no deployment text.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
showNoDeployments() {
|
||||
return !this.hasLastDeploymentKey && !this.isFolder;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the build name column should be rendered by verifing
|
||||
* if all the information needed is present
|
||||
* and if the environment is not a folder.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
shouldRenderBuildName() {
|
||||
return (
|
||||
!this.isFolder &&
|
||||
!isEmpty(this.model.last_deployment) &&
|
||||
!isEmpty(this.model.last_deployment.deployable)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies the presence of all the keys needed to render the buil_path.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
buildPath() {
|
||||
if (
|
||||
this.model &&
|
||||
this.model.last_deployment &&
|
||||
this.model.last_deployment.deployable &&
|
||||
this.model.last_deployment.deployable.build_path
|
||||
) {
|
||||
return this.model.last_deployment.deployable.build_path;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies the presence of all the keys needed to render the external_url.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
externalURL() {
|
||||
return this.model.external_url || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if deplyment internal ID should be rendered by verifing
|
||||
* if all the information needed is present
|
||||
* and if the environment is not a folder.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
shouldRenderDeploymentID() {
|
||||
return (
|
||||
!this.isFolder &&
|
||||
!isEmpty(this.model.last_deployment) &&
|
||||
this.model.last_deployment.iid !== undefined
|
||||
);
|
||||
},
|
||||
|
||||
environmentPath() {
|
||||
return this.model.environment_path || '';
|
||||
},
|
||||
|
||||
terminalPath() {
|
||||
return this.model?.terminal_path ?? '';
|
||||
},
|
||||
|
||||
autoStopUrl() {
|
||||
return this.model.cancel_auto_stop_path || '';
|
||||
},
|
||||
|
||||
displayEnvironmentActions() {
|
||||
return (
|
||||
this.actions.length > 0 ||
|
||||
this.externalURL ||
|
||||
this.canStopEnvironment ||
|
||||
this.canDeleteEnvironment ||
|
||||
this.canRetry
|
||||
);
|
||||
},
|
||||
|
||||
folderIconName() {
|
||||
return this.model.isOpen ? 'chevron-down' : 'chevron-right';
|
||||
},
|
||||
|
||||
upcomingDeploymentCellClasses() {
|
||||
return [
|
||||
this.tableData.upcoming.spacing,
|
||||
{ '!gl-hidden md:!gl-block': !this.upcomingDeployment },
|
||||
];
|
||||
},
|
||||
tableNameSpacingClass() {
|
||||
return this.isFolder ? 'section-100' : this.tableData.name.spacing;
|
||||
},
|
||||
hasExtraActions() {
|
||||
return Boolean(
|
||||
this.canRetry || this.canShowAutoStopDate || this.terminalPath || this.canDeleteEnvironment,
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDeployBoard() {
|
||||
eventHub.$emit('toggleDeployBoard', this.model);
|
||||
},
|
||||
onClickFolder() {
|
||||
eventHub.$emit('toggleFolder', this.model);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the field title that will be shown in the field's row
|
||||
* in the mobile view.
|
||||
*
|
||||
* @returns `field.mobileTitle` if present;
|
||||
* if not, falls back to `field.title`.
|
||||
*/
|
||||
getMobileViewTitleForField(fieldName) {
|
||||
const field = this.tableData[fieldName];
|
||||
|
||||
return field.mobileTitle || field.title;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'js-child-row environment-child-row': model.isChildren,
|
||||
'folder-row': isFolder,
|
||||
}"
|
||||
class="gl-responsive-table-row"
|
||||
role="row"
|
||||
>
|
||||
<div
|
||||
class="table-section section-wrap text-truncate"
|
||||
:class="tableNameSpacingClass"
|
||||
role="gridcell"
|
||||
data-testid="environment-name-cell"
|
||||
>
|
||||
<div v-if="!isFolder" class="table-mobile-header" role="rowheader">
|
||||
{{ getMobileViewTitleForField('name') }}
|
||||
</div>
|
||||
|
||||
<span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard">
|
||||
<gl-icon :name="deployIconName" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="!isFolder"
|
||||
v-gl-tooltip
|
||||
:title="model.name"
|
||||
class="environment-name table-mobile-content"
|
||||
>
|
||||
<a :href="environmentPath">
|
||||
<span v-if="model.size === 1">{{ model.name }}</span>
|
||||
<span v-else>{{ model.name_without_type }}</span>
|
||||
</a>
|
||||
<gl-badge v-if="isProtected" variant="success">{{
|
||||
s__('Environments|protected')
|
||||
}}</gl-badge>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
v-gl-tooltip
|
||||
:title="model.folderName"
|
||||
class="folder-name"
|
||||
role="button"
|
||||
@click="onClickFolder"
|
||||
>
|
||||
<gl-icon :name="folderIconName" class="folder-icon" />
|
||||
|
||||
<gl-icon name="folder" class="folder-icon" />
|
||||
|
||||
<span> {{ model.folderName }} </span>
|
||||
|
||||
<gl-badge>{{ model.size }}</gl-badge>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isFolder"
|
||||
class="table-section deployment-column gl-hidden md:gl-block"
|
||||
:class="tableData.deploy.spacing"
|
||||
role="gridcell"
|
||||
data-testid="environment-deployment-id-cell"
|
||||
>
|
||||
<span v-if="shouldRenderDeploymentID" class="gl-break-all">
|
||||
{{ deploymentInternalId }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="!isFolder && deploymentHasUser"
|
||||
class="gl-inline-flex gl-items-center gl-break-all"
|
||||
>
|
||||
<gl-sprintf :message="s__('Environments|by %{avatar}')">
|
||||
<template #avatar>
|
||||
<gl-avatar-link :href="deploymentUser.web_url" class="gl-ml-2">
|
||||
<gl-avatar
|
||||
:src="deploymentUser.avatar_url"
|
||||
:entity-name="deploymentUser.username"
|
||||
:title="deploymentUser.username"
|
||||
:alt="userImageAltDescription"
|
||||
:size="24"
|
||||
/>
|
||||
</gl-avatar-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
|
||||
<div v-if="showNoDeployments" class="commit-title table-mobile-content">
|
||||
{{ s__('Environments|No deployments yet') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isFolder"
|
||||
class="table-section gl-hidden md:gl-block"
|
||||
:class="tableData.build.spacing"
|
||||
role="gridcell"
|
||||
data-testid="environment-build-cell"
|
||||
>
|
||||
<a v-if="shouldRenderBuildName" :href="buildPath" class="build-link gl-text-default">
|
||||
<tooltip-on-truncate :title="buildName" truncate-target="child" class="gl-flex">
|
||||
<span class="flex-truncate-child">
|
||||
{{ buildName }}
|
||||
</span>
|
||||
</tooltip-on-truncate>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="!isFolder" class="table-section" :class="tableData.commit.spacing" role="gridcell">
|
||||
<div role="rowheader" class="table-mobile-header">
|
||||
{{ getMobileViewTitleForField('commit') }}
|
||||
</div>
|
||||
<div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content">
|
||||
<commit-component
|
||||
:tag="commitTag"
|
||||
:commit-ref="commitRef"
|
||||
:commit-url="commitUrl"
|
||||
:short-sha="commitShortSha"
|
||||
:title="commitTitle"
|
||||
:author="commitAuthor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isFolder" class="table-section" :class="tableData.date.spacing" role="gridcell">
|
||||
<div role="rowheader" class="table-mobile-header">
|
||||
{{ getMobileViewTitleForField('date') }}
|
||||
</div>
|
||||
<span
|
||||
v-if="canShowDeploymentDate"
|
||||
v-gl-tooltip
|
||||
:title="deployedDate.tooltip"
|
||||
class="environment-created-date-timeago table-mobile-content gl-flex"
|
||||
>
|
||||
<span class="flex-truncate-child">
|
||||
{{ deployedDate.formatted }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isFolder"
|
||||
class="table-section"
|
||||
:class="upcomingDeploymentCellClasses"
|
||||
role="gridcell"
|
||||
data-testid="upcoming-deployment"
|
||||
>
|
||||
<div role="rowheader" class="table-mobile-header">
|
||||
{{ getMobileViewTitleForField('upcoming') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="upcomingDeployment"
|
||||
class="gl-flex gl-w-full gl-flex-row gl-justify-end md:!gl-flex-col"
|
||||
data-testid="upcoming-deployment-content"
|
||||
>
|
||||
<div class="gl-flex gl-items-center">
|
||||
<span class="gl-mr-2">{{ upcomingDeploymentInternalId }}</span>
|
||||
<gl-link
|
||||
v-if="upcomingDeployment.deployable"
|
||||
v-gl-tooltip
|
||||
:href="upcomingDeployment.deployable.build_path"
|
||||
:title="upcomingDeploymentTooltipText"
|
||||
data-testid="upcoming-deployment-status-link"
|
||||
>
|
||||
<ci-icon :status="upcomingDeployment.deployable.status" class="gl-mr-2" />
|
||||
</gl-link>
|
||||
</div>
|
||||
<span
|
||||
v-if="upcomingDeployment.user"
|
||||
class="gl-mt-2 gl-inline-flex gl-items-center gl-break-all"
|
||||
>
|
||||
<gl-sprintf :message="s__('Environments|by %{avatar}')">
|
||||
<template #avatar>
|
||||
<gl-avatar-link :href="upcomingDeployment.user.web_url" class="gl-ml-2">
|
||||
<gl-avatar
|
||||
:src="upcomingDeployment.user.avatar_url"
|
||||
:alt="upcomingDeploymentUserImageAltDescription"
|
||||
:entity-name="upcomingDeployment.user.username"
|
||||
:title="upcomingDeployment.user.username"
|
||||
:size="24"
|
||||
/>
|
||||
</gl-avatar-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isFolder" class="table-section" :class="tableData.autoStop.spacing" role="gridcell">
|
||||
<div role="rowheader" class="table-mobile-header">
|
||||
{{ getMobileViewTitleForField('autoStop') }}
|
||||
</div>
|
||||
<span
|
||||
v-if="canShowAutoStopDate"
|
||||
v-gl-tooltip
|
||||
:title="autoStopDate.tooltip"
|
||||
class="table-mobile-content gl-flex"
|
||||
>
|
||||
<span class="flex-truncate-child js-auto-stop">{{ autoStopDate.formatted }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isFolder && displayEnvironmentActions"
|
||||
class="table-section table-button-footer"
|
||||
:class="tableData.actions.spacing"
|
||||
role="gridcell"
|
||||
>
|
||||
<div class="btn-group table-action-buttons" role="group">
|
||||
<external-url-component
|
||||
v-if="externalURL"
|
||||
:external-url="externalURL"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_url"
|
||||
/>
|
||||
|
||||
<actions-component
|
||||
v-if="actions.length > 0"
|
||||
:actions="actions"
|
||||
data-track-action="click_dropdown"
|
||||
data-track-label="environment_actions"
|
||||
/>
|
||||
|
||||
<stop-component
|
||||
v-if="canStopEnvironment"
|
||||
:environment="model"
|
||||
class="gl-z-2"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_stop"
|
||||
/>
|
||||
|
||||
<gl-disclosure-dropdown
|
||||
text-sr-only
|
||||
no-caret
|
||||
icon="ellipsis_v"
|
||||
category="secondary"
|
||||
placement="bottom-end"
|
||||
:toggle-text="__('More actions')"
|
||||
>
|
||||
<rollback-component
|
||||
v-if="canRetry"
|
||||
:environment="model"
|
||||
:is-last-deployment="isLastDeployment"
|
||||
:retry-url="retryUrl"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_rollback"
|
||||
/>
|
||||
|
||||
<pin-component
|
||||
v-if="canShowAutoStopDate"
|
||||
:auto-stop-url="autoStopUrl"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_pin"
|
||||
/>
|
||||
|
||||
<terminal-button-component
|
||||
v-if="terminalPath"
|
||||
:terminal-path="terminalPath"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_terminal"
|
||||
/>
|
||||
|
||||
<delete-component
|
||||
v-if="canDeleteEnvironment"
|
||||
:environment="model"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_delete"
|
||||
/>
|
||||
</gl-disclosure-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
import { GlDisclosureDropdownItem } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
import cancelAutoStopMutation from '../graphql/mutations/cancel_auto_stop.mutation.graphql';
|
||||
|
||||
export default {
|
||||
|
|
@ -17,11 +16,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
graphql: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -30,14 +24,10 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
onPinClick() {
|
||||
if (this.graphql) {
|
||||
this.$apollo.mutate({
|
||||
mutation: cancelAutoStopMutation,
|
||||
variables: { autoStopUrl: this.autoStopUrl },
|
||||
});
|
||||
} else {
|
||||
eventHub.$emit('cancelAutoStop', this.autoStopUrl);
|
||||
}
|
||||
this.$apollo.mutate({
|
||||
mutation: cancelAutoStopMutation,
|
||||
variables: { autoStopUrl: this.autoStopUrl },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
import { GlDisclosureDropdownItem, GlModalDirective } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
import setEnvironmentToRollback from '../graphql/mutations/set_environment_to_rollback.mutation.graphql';
|
||||
|
||||
export default {
|
||||
|
|
@ -33,12 +32,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
graphql: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
@ -58,16 +51,13 @@ export default {
|
|||
retryUrl: this.retryUrl,
|
||||
isLastDeployment: this.isLastDeployment,
|
||||
};
|
||||
if (this.graphql) {
|
||||
this.$apollo.mutate({
|
||||
mutation: setEnvironmentToRollback,
|
||||
variables: {
|
||||
environment: rollbackEnvironmentData,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
eventHub.$emit('requestRollbackEnvironment', rollbackEnvironmentData);
|
||||
}
|
||||
|
||||
this.$apollo.mutate({
|
||||
mutation: setEnvironmentToRollback,
|
||||
variables: {
|
||||
environment: rollbackEnvironmentData,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
|
||||
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
|
||||
import { s__ } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
import setEnvironmentToStopMutation from '../graphql/mutations/set_environment_to_stop.mutation.graphql';
|
||||
import isEnvironmentStoppingQuery from '../graphql/queries/is_environment_stopping.query.graphql';
|
||||
|
||||
|
|
@ -24,11 +23,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
graphql: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
isEnvironmentStopping: {
|
||||
|
|
@ -56,51 +50,30 @@ export default {
|
|||
return this.isLoadingState ? this.$options.i18n.stoppingTitle : this.$options.i18n.stopTitle;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
eventHub.$on('stopEnvironment', this.onStopEnvironment);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('stopEnvironment', this.onStopEnvironment);
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$root.$emit(BV_HIDE_TOOLTIP, this.$options.stopEnvironmentTooltipId);
|
||||
if (this.graphql) {
|
||||
this.$apollo.mutate({
|
||||
mutation: setEnvironmentToStopMutation,
|
||||
variables: { environment: this.environment },
|
||||
});
|
||||
} else {
|
||||
eventHub.$emit('requestStopEnvironment', this.environment);
|
||||
}
|
||||
},
|
||||
onStopEnvironment(environment) {
|
||||
if (this.environment.id === environment.id) {
|
||||
this.isLoading = true;
|
||||
}
|
||||
this.$apollo.mutate({
|
||||
mutation: setEnvironmentToStopMutation,
|
||||
variables: { environment: this.environment },
|
||||
});
|
||||
},
|
||||
},
|
||||
stopEnvironmentTooltipId: 'stop-environment-button-tooltip',
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
<gl-button
|
||||
v-gl-modal-directive="'stop-environment-modal'"
|
||||
v-gl-tooltip="{ id: $options.stopEnvironmentTooltipId }"
|
||||
:title="title"
|
||||
:tabindex="isLoadingState ? 0 : null"
|
||||
class="gl-relative -gl-ml-[1px]"
|
||||
>
|
||||
<gl-button
|
||||
v-gl-modal-directive="'stop-environment-modal'"
|
||||
:loading="isLoadingState"
|
||||
:aria-label="title"
|
||||
:class="{ 'gl-pointer-events-none': isLoadingState }"
|
||||
class="!gl-rounded-none"
|
||||
size="small"
|
||||
icon="stop"
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
@click="onClick"
|
||||
/>
|
||||
</div>
|
||||
:loading="isLoadingState"
|
||||
:aria-label="title"
|
||||
:class="{ 'gl-pointer-events-none': isLoadingState }"
|
||||
size="small"
|
||||
icon="stop"
|
||||
variant="danger"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -272,9 +272,9 @@ export default {
|
|||
:modal-id="$options.stopStaleEnvsModalId"
|
||||
data-testid="stop-stale-environments-modal"
|
||||
/>
|
||||
<delete-environment-modal :environment="environmentToDelete" graphql />
|
||||
<stop-environment-modal :environment="environmentToStop" graphql />
|
||||
<confirm-rollback-modal :environment="environmentToRollback" graphql />
|
||||
<delete-environment-modal :environment="environmentToDelete" />
|
||||
<stop-environment-modal :environment="environmentToStop" />
|
||||
<confirm-rollback-modal :environment="environmentToRollback" />
|
||||
<canary-update-modal :environment="environmentToChangeCanary" :weight="weight" />
|
||||
<template v-if="showTabs">
|
||||
<gl-tabs
|
||||
|
|
|
|||
|
|
@ -1,217 +0,0 @@
|
|||
<script>
|
||||
/**
|
||||
* Render environments table.
|
||||
*/
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { flow, reverse, sortBy } from 'lodash/fp';
|
||||
import { s__ } from '~/locale';
|
||||
import CanaryUpdateModal from './canary_update_modal.vue';
|
||||
import DeployBoard from './deploy_board.vue';
|
||||
import EnvironmentItem from './environment_item.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EnvironmentItem,
|
||||
GlLoadingIcon,
|
||||
DeployBoard,
|
||||
EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'),
|
||||
CanaryUpdateModal,
|
||||
},
|
||||
props: {
|
||||
environments: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canaryWeight: 0,
|
||||
environmentToChange: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedEnvironments() {
|
||||
return this.sortEnvironments(this.environments).map((env) =>
|
||||
this.shouldRenderFolderContent(env)
|
||||
? { ...env, children: this.sortEnvironments(env.children) }
|
||||
: env,
|
||||
);
|
||||
},
|
||||
tableData() {
|
||||
return {
|
||||
// percent spacing for cols, should add up to 100
|
||||
name: {
|
||||
title: s__('Environments|Environment'),
|
||||
spacing: 'section-10',
|
||||
},
|
||||
deploy: {
|
||||
title: s__('Environments|Deployment'),
|
||||
spacing: 'section-10',
|
||||
},
|
||||
build: {
|
||||
title: s__('Environments|Job'),
|
||||
spacing: 'section-15',
|
||||
},
|
||||
commit: {
|
||||
title: s__('Environments|Commit'),
|
||||
spacing: 'section-15',
|
||||
},
|
||||
date: {
|
||||
title: s__('Environments|Updated'),
|
||||
spacing: 'section-10',
|
||||
},
|
||||
upcoming: {
|
||||
title: s__('Environments|Upcoming'),
|
||||
mobileTitle: s__('Environments|Upcoming deployment'),
|
||||
spacing: 'section-10',
|
||||
},
|
||||
autoStop: {
|
||||
title: s__('Environments|Auto stop'),
|
||||
spacing: 'section-10',
|
||||
},
|
||||
actions: {
|
||||
spacing: 'section-20',
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
folderUrl(model) {
|
||||
return `${window.location.pathname}/folders/${model.folderName}`;
|
||||
},
|
||||
shouldRenderDeployBoard(model) {
|
||||
return model.hasDeployBoard && model.isDeployBoardVisible;
|
||||
},
|
||||
shouldRenderFolderContent(env) {
|
||||
return env.isFolder && env.isOpen && env.children && env.children.length > 0;
|
||||
},
|
||||
shouldRenderAlert(env) {
|
||||
return env?.has_opened_alert;
|
||||
},
|
||||
sortEnvironments(environments) {
|
||||
/*
|
||||
* The sorting algorithm should sort in the following priorities:
|
||||
*
|
||||
* 1. folders first,
|
||||
* 2. last updated descending,
|
||||
* 3. by name ascending,
|
||||
*
|
||||
* the sorting algorithm must:
|
||||
*
|
||||
* 1. Sort by name ascending,
|
||||
* 2. Reverse (sort by name descending),
|
||||
* 3. Sort by last deployment ascending,
|
||||
* 4. Reverse (last deployment descending, name ascending),
|
||||
* 5. Put folders first.
|
||||
*/
|
||||
return flow(
|
||||
sortBy((env) => (env.isFolder ? env.folderName : env.name)),
|
||||
reverse,
|
||||
sortBy((env) => (env.last_deployment ? env.last_deployment.created_at : '0000')),
|
||||
reverse,
|
||||
sortBy((env) => (env.isFolder ? -1 : 1)),
|
||||
)(environments);
|
||||
},
|
||||
changeCanaryWeight(model, weight) {
|
||||
this.environmentToChange = model;
|
||||
this.canaryWeight = weight;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="ci-table" role="grid">
|
||||
<canary-update-modal :environment="environmentToChange" :weight="canaryWeight" />
|
||||
<div class="gl-responsive-table-row table-row-header" role="row">
|
||||
<div class="table-section" :class="tableData.name.spacing" role="columnheader">
|
||||
{{ tableData.name.title }}
|
||||
</div>
|
||||
<div class="table-section" :class="tableData.deploy.spacing" role="columnheader">
|
||||
{{ tableData.deploy.title }}
|
||||
</div>
|
||||
<div class="table-section" :class="tableData.build.spacing" role="columnheader">
|
||||
{{ tableData.build.title }}
|
||||
</div>
|
||||
<div class="table-section" :class="tableData.commit.spacing" role="columnheader">
|
||||
{{ tableData.commit.title }}
|
||||
</div>
|
||||
<div class="table-section" :class="tableData.date.spacing" role="columnheader">
|
||||
{{ tableData.date.title }}
|
||||
</div>
|
||||
<div class="table-section" :class="tableData.upcoming.spacing" role="columnheader">
|
||||
{{ tableData.upcoming.title }}
|
||||
</div>
|
||||
<div class="table-section" :class="tableData.autoStop.spacing" role="columnheader">
|
||||
{{ tableData.autoStop.title }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="(model, i) in sortedEnvironments">
|
||||
<environment-item :key="`environment-item-${i}`" :model="model" :table-data="tableData" />
|
||||
|
||||
<div
|
||||
v-if="shouldRenderDeployBoard(model)"
|
||||
:key="`deploy-board-row-${i}`"
|
||||
class="js-deploy-board-row"
|
||||
>
|
||||
<div class="deploy-board-container">
|
||||
<deploy-board
|
||||
:deploy-board-data="model.deployBoardData"
|
||||
:is-loading="model.isLoadingDeployBoard"
|
||||
:is-empty="model.isEmptyDeployBoard"
|
||||
@changeCanaryWeight="changeCanaryWeight(model, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<environment-alert
|
||||
v-if="shouldRenderAlert(model)"
|
||||
:key="`alert-row-${i}`"
|
||||
:environment="model"
|
||||
/>
|
||||
|
||||
<template v-if="shouldRenderFolderContent(model)">
|
||||
<div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`">
|
||||
<gl-loading-icon size="lg" class="gl-mt-5" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<template v-for="(child, index) in model.children">
|
||||
<environment-item
|
||||
:key="`environment-row-${i}-${index}`"
|
||||
:model="child"
|
||||
:table-data="tableData"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="shouldRenderDeployBoard(child)"
|
||||
:key="`deploy-board-row-${i}-${index}`"
|
||||
class="js-deploy-board-row"
|
||||
>
|
||||
<div class="deploy-board-container">
|
||||
<deploy-board
|
||||
:deploy-board-data="child.deployBoardData"
|
||||
:is-loading="child.isLoadingDeployBoard"
|
||||
:is-empty="child.isEmptyDeployBoard"
|
||||
@changeCanaryWeight="changeCanaryWeight(child, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<environment-alert
|
||||
v-if="shouldRenderAlert(model)"
|
||||
:key="`alert-row-${i}-${index}`"
|
||||
:environment="child"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div :key="`sub-div-${i}`">
|
||||
<div class="text-center gl-mt-3">
|
||||
<a :href="folderUrl(model)" class="btn btn-default">
|
||||
{{ s__('Environments|Show all') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -232,7 +232,6 @@ export default {
|
|||
:actions="actions"
|
||||
data-track-action="click_dropdown"
|
||||
data-track-label="environment_actions"
|
||||
graphql
|
||||
/>
|
||||
|
||||
<stop-component
|
||||
|
|
@ -240,7 +239,6 @@ export default {
|
|||
:environment="environment"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_stop"
|
||||
graphql
|
||||
/>
|
||||
|
||||
<gl-disclosure-dropdown
|
||||
|
|
@ -258,7 +256,6 @@ export default {
|
|||
:environment="environment"
|
||||
:is-last-deployment="isLastDeployment"
|
||||
:retry-url="retryPath"
|
||||
graphql
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_rollback"
|
||||
/>
|
||||
|
|
@ -266,7 +263,6 @@ export default {
|
|||
<pin
|
||||
v-if="canShowAutoStopDate"
|
||||
:auto-stop-url="autoStopPath"
|
||||
graphql
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_pin"
|
||||
/>
|
||||
|
|
@ -283,7 +279,6 @@ export default {
|
|||
:environment="environment"
|
||||
data-track-action="click_button"
|
||||
data-track-label="environment_delete"
|
||||
graphql
|
||||
/>
|
||||
</gl-disclosure-dropdown>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { GlSprintf, GlTooltipDirective, GlModal } from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import eventHub from '../event_hub';
|
||||
import stopEnvironmentMutation from '../graphql/mutations/stop_environment.mutation.graphql';
|
||||
|
||||
export default {
|
||||
|
|
@ -30,11 +29,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
graphql: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
@ -50,7 +44,7 @@ export default {
|
|||
};
|
||||
},
|
||||
hasStopAction() {
|
||||
return this.graphql ? this.environment.hasStopAction : this.environment.has_stop_action;
|
||||
return this.environment.hasStopAction;
|
||||
},
|
||||
stopMessage() {
|
||||
return this.hasStopAction
|
||||
|
|
@ -61,14 +55,10 @@ export default {
|
|||
|
||||
methods: {
|
||||
onSubmit() {
|
||||
if (this.graphql) {
|
||||
this.$apollo.mutate({
|
||||
mutation: stopEnvironmentMutation,
|
||||
variables: { environment: this.environment },
|
||||
});
|
||||
} else {
|
||||
eventHub.$emit('stopEnvironment', this.environment);
|
||||
}
|
||||
this.$apollo.mutate({
|
||||
mutation: stopEnvironmentMutation,
|
||||
variables: { environment: this.environment },
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<actions-component v-if="isActionsShown" :actions="actions" graphql />
|
||||
<actions-component v-if="isActionsShown" :actions="actions" />
|
||||
<gl-button
|
||||
v-if="isRollbackAvailable"
|
||||
v-gl-modal.confirm-rollback-modal
|
||||
|
|
|
|||
|
|
@ -219,6 +219,6 @@ export default {
|
|||
<pagination :page-info="pageInfo" :disabled="isPaginationDisabled" />
|
||||
</div>
|
||||
<empty-state v-if="!isDeploymentTableShown && !isLoading" />
|
||||
<confirm-rollback-modal :environment="environmentToRollback" graphql @rollback="resetPage" />
|
||||
<confirm-rollback-modal :environment="environmentToRollback" @rollback="resetPage" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default createEventHub();
|
||||
|
|
@ -188,9 +188,9 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<delete-environment-modal :environment="environmentToDelete" graphql />
|
||||
<stop-environment-modal :environment="environmentToStop" graphql />
|
||||
<confirm-rollback-modal :environment="environmentToRollback" graphql />
|
||||
<delete-environment-modal :environment="environmentToDelete" />
|
||||
<stop-environment-modal :environment="environmentToStop" />
|
||||
<confirm-rollback-modal :environment="environmentToRollback" />
|
||||
<canary-update-modal :environment="environmentToChangeCanary" :weight="weight" />
|
||||
<h4 class="gl-font-normal" data-testid="folder-name">
|
||||
{{ $options.i18n.pageTitle }} /
|
||||
|
|
@ -251,6 +251,7 @@ export default {
|
|||
v-model="pageNumber"
|
||||
:per-page="$options.perPage"
|
||||
:total-items="totalItems"
|
||||
class="gl-mt-6"
|
||||
align="center"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,90 +1,58 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import VueRouter from 'vue-router';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import { apolloProvider } from '../graphql/client';
|
||||
import EnvironmentsFolderView from './environments_folder_view.vue';
|
||||
import EnvironmentsFolderApp from './environments_folder_app.vue';
|
||||
|
||||
Vue.use(Translate);
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const legacyApolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('environments-folder-list-view');
|
||||
|
||||
if (!el) return null;
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const environmentsData = el.dataset;
|
||||
if (gon.features.environmentsFolderNewLook) {
|
||||
Vue.use(VueRouter);
|
||||
const folderPath = environmentsData.endpoint.replace('.json', '');
|
||||
const { projectPath, folderName, helpPagePath } = environmentsData;
|
||||
|
||||
const folderPath = environmentsData.endpoint.replace('.json', '');
|
||||
const { projectPath, folderName, helpPagePath } = environmentsData;
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: window.location.pathname,
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'environments_folder',
|
||||
component: EnvironmentsFolderApp,
|
||||
props: (route) => ({
|
||||
scope: route.query.scope,
|
||||
page: Number(route.query.page || '1'),
|
||||
folderName,
|
||||
folderPath,
|
||||
}),
|
||||
},
|
||||
],
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
}
|
||||
return { top: 0 };
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: window.location.pathname,
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'environments_folder',
|
||||
component: EnvironmentsFolderApp,
|
||||
props: (route) => ({
|
||||
scope: route.query.scope,
|
||||
page: Number(route.query.page || '1'),
|
||||
folderName,
|
||||
folderPath,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
projectPath,
|
||||
helpPagePath,
|
||||
},
|
||||
apolloProvider,
|
||||
router,
|
||||
render(createElement) {
|
||||
return createElement('router-view');
|
||||
},
|
||||
});
|
||||
}
|
||||
],
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
}
|
||||
return { top: 0 };
|
||||
},
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
components: {
|
||||
EnvironmentsFolderView,
|
||||
},
|
||||
apolloProvider: legacyApolloProvider,
|
||||
provide: {
|
||||
projectPath: el.dataset.projectPath,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
endpoint: environmentsData.endpoint,
|
||||
folderName: environmentsData.folderName,
|
||||
cssContainerClass: environmentsData.cssClass,
|
||||
};
|
||||
projectPath,
|
||||
helpPagePath,
|
||||
},
|
||||
apolloProvider,
|
||||
router,
|
||||
render(createElement) {
|
||||
return createElement('environments-folder-view', {
|
||||
props: {
|
||||
endpoint: this.endpoint,
|
||||
folderName: this.folderName,
|
||||
cssContainerClass: this.cssContainerClass,
|
||||
},
|
||||
});
|
||||
return createElement('router-view');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
<script>
|
||||
import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
|
||||
import DeleteEnvironmentModal from '../components/delete_environment_modal.vue';
|
||||
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
|
||||
import environmentsMixin from '../mixins/environments_mixin';
|
||||
import EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
|
||||
import ConfirmRollbackModal from '../components/confirm_rollback_modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DeleteEnvironmentModal,
|
||||
GlBadge,
|
||||
GlTab,
|
||||
GlTabs,
|
||||
StopEnvironmentModal,
|
||||
ConfirmRollbackModal,
|
||||
},
|
||||
|
||||
mixins: [environmentsMixin, EnvironmentsPaginationApiMixin],
|
||||
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
folderName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
cssContainerClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
successCallback(resp) {
|
||||
this.saveData(resp);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div :class="cssContainerClass">
|
||||
<stop-environment-modal :environment="environmentInStopModal" />
|
||||
<delete-environment-modal :environment="environmentInDeleteModal" />
|
||||
<confirm-rollback-modal :environment="environmentInRollbackModal" />
|
||||
|
||||
<h4 class="gl-font-normal" data-testid="folder-name">
|
||||
{{ s__('Environments|Environments') }} /
|
||||
<b>{{ folderName }}</b>
|
||||
</h4>
|
||||
|
||||
<gl-tabs v-if="!isLoading" scope="environments" content-class="gl-hidden">
|
||||
<gl-tab
|
||||
v-for="(tab, i) in tabs"
|
||||
:key="`${tab.name}-${i}`"
|
||||
:active="tab.isActive"
|
||||
:title-item-class="tab.isActive ? 'gl-outline-none' : ''"
|
||||
:title-link-attributes="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
|
||||
'data-testid': `environments-tab-${tab.scope}`,
|
||||
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
|
||||
@click="onChangeTab(tab.scope)"
|
||||
>
|
||||
<template #title>
|
||||
<span>{{ tab.name }}</span>
|
||||
<gl-badge class="gl-tab-counter-badge">{{ tab.count }}</gl-badge>
|
||||
</template>
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-undef-components -->
|
||||
<container
|
||||
:is-loading="isLoading"
|
||||
:environments="state.environments"
|
||||
:pagination="state.paginationInformation"
|
||||
@onChangePage="onChangePage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,292 +0,0 @@
|
|||
/**
|
||||
* Common code between environmets app and folder view
|
||||
*/
|
||||
import { isEqual, isFunction, omitBy } from 'lodash';
|
||||
import Visibility from 'visibilityjs';
|
||||
import { createAlert } from '~/alert';
|
||||
import Poll from '~/lib/utils/poll';
|
||||
import { getParameterByName } from '~/lib/utils/url_utility';
|
||||
import { s__, __ } from '~/locale';
|
||||
import tabs from '~/vue_shared/components/navigation_tabs.vue';
|
||||
import tablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
|
||||
import container from '../components/container.vue';
|
||||
import environmentTable from '../components/environments_table.vue';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
import EnvironmentsService from '../services/environments_service';
|
||||
import EnvironmentsStore from '../stores/environments_store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
environmentTable,
|
||||
container,
|
||||
tabs,
|
||||
tablePagination,
|
||||
},
|
||||
|
||||
data() {
|
||||
const store = new EnvironmentsStore();
|
||||
|
||||
const isDetailView = document.body.contains(
|
||||
document.getElementById('environments-detail-view'),
|
||||
);
|
||||
|
||||
return {
|
||||
store,
|
||||
state: store.state,
|
||||
isLoading: false,
|
||||
isMakingRequest: false,
|
||||
scope: getParameterByName('scope') || 'available',
|
||||
page: getParameterByName('page') || '1',
|
||||
requestData: {},
|
||||
environmentInStopModal: {},
|
||||
environmentInDeleteModal: {},
|
||||
environmentInRollbackModal: {},
|
||||
isDetailView,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
saveData(resp) {
|
||||
this.isLoading = false;
|
||||
|
||||
// Prevent the absence of the nested flag from causing mismatches
|
||||
const response = this.filterNilValues(resp.config.params);
|
||||
const request = this.filterNilValues(this.requestData);
|
||||
|
||||
if (isEqual(response, request)) {
|
||||
this.store.storeAvailableCount(resp.data.available_count);
|
||||
this.store.storeStoppedCount(resp.data.stopped_count);
|
||||
this.store.storeEnvironments(resp.data.environments);
|
||||
this.store.setReviewAppDetails(resp.data.review_app);
|
||||
this.store.setPagination(resp.headers);
|
||||
}
|
||||
},
|
||||
|
||||
filterNilValues(obj) {
|
||||
return omitBy(obj, (value) => value === undefined || value === null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles URL and query parameter changes.
|
||||
* When the user uses the pagination or the tabs,
|
||||
* - update URL
|
||||
* - Make API request to the server with new parameters
|
||||
* - Update the polling function
|
||||
* - Update the internal state
|
||||
*/
|
||||
updateContent(parameters) {
|
||||
this.updateInternalState(parameters);
|
||||
// fetch new data
|
||||
return this.service
|
||||
.fetchEnvironments(this.requestData)
|
||||
.then((response) => {
|
||||
this.successCallback(response);
|
||||
this.poll.enable({ data: this.requestData, response });
|
||||
})
|
||||
.catch(() => {
|
||||
this.errorCallback();
|
||||
|
||||
// restart polling
|
||||
this.poll.restart();
|
||||
});
|
||||
},
|
||||
|
||||
errorCallback() {
|
||||
this.isLoading = false;
|
||||
createAlert({
|
||||
message: s__('Environments|An error occurred while fetching the environments.'),
|
||||
});
|
||||
},
|
||||
|
||||
postAction({
|
||||
endpoint,
|
||||
errorMessage = s__('Environments|An error occurred while making the request.'),
|
||||
}) {
|
||||
if (!this.isMakingRequest) {
|
||||
this.isLoading = true;
|
||||
|
||||
this.service
|
||||
.postAction(endpoint)
|
||||
.then(() => {
|
||||
// Originally, the detail page buttons were implemented as <form>s that POSTed
|
||||
// to the server, which would naturally result in a page refresh.
|
||||
// When environment details page was converted to Vue, the buttons were updated to trigger
|
||||
// HTTP requests using `axios`, which did not cause a refresh on completion.
|
||||
// To preserve the original behavior, we manually reload the page when
|
||||
// network requests complete successfully.
|
||||
if (!this.isDetailView) {
|
||||
this.fetchEnvironments();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.isLoading = false;
|
||||
createAlert({
|
||||
message: isFunction(errorMessage) ? errorMessage(err.response.data) : errorMessage,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
fetchEnvironments() {
|
||||
this.isLoading = true;
|
||||
|
||||
return this.service
|
||||
.fetchEnvironments(this.requestData)
|
||||
.then(this.successCallback)
|
||||
.catch(this.errorCallback);
|
||||
},
|
||||
|
||||
updateStopModal(environment) {
|
||||
this.environmentInStopModal = environment;
|
||||
},
|
||||
|
||||
updateDeleteModal(environment) {
|
||||
this.environmentInDeleteModal = environment;
|
||||
},
|
||||
|
||||
updateRollbackModal(environment) {
|
||||
this.environmentInRollbackModal = environment;
|
||||
},
|
||||
|
||||
stopEnvironment(environment) {
|
||||
const endpoint = environment.stop_path;
|
||||
const errorMessage = s__(
|
||||
'Environments|An error occurred while stopping the environment, please try again',
|
||||
);
|
||||
this.postAction({ endpoint, errorMessage });
|
||||
},
|
||||
|
||||
deleteEnvironment(environment) {
|
||||
const endpoint = environment.delete_path;
|
||||
const { onSingleEnvironmentPage } = environment;
|
||||
const errorMessage = s__(
|
||||
'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
|
||||
);
|
||||
|
||||
this.service
|
||||
.deleteAction(endpoint)
|
||||
.then(() => {
|
||||
if (!onSingleEnvironmentPage) {
|
||||
// Reload as a first solution to bust the ETag cache
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
const url = window.location.href.split('/');
|
||||
url.pop();
|
||||
window.location.href = url.join('/');
|
||||
})
|
||||
.catch(() => {
|
||||
createAlert({
|
||||
message: errorMessage,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
rollbackEnvironment(environment) {
|
||||
const { retryUrl, isLastDeployment } = environment;
|
||||
const errorMessage = isLastDeployment
|
||||
? s__('Environments|An error occurred while re-deploying the environment, please try again')
|
||||
: s__(
|
||||
'Environments|An error occurred while rolling back the environment, please try again',
|
||||
);
|
||||
this.postAction({ endpoint: retryUrl, errorMessage });
|
||||
},
|
||||
|
||||
cancelAutoStop(autoStopPath) {
|
||||
const errorMessage = ({ message }) =>
|
||||
message ||
|
||||
s__('Environments|An error occurred while canceling the auto stop, please try again');
|
||||
this.postAction({ endpoint: autoStopPath, errorMessage });
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
tabs() {
|
||||
return [
|
||||
{
|
||||
name: __('Available'),
|
||||
scope: 'available',
|
||||
count: this.state.availableCounter,
|
||||
isActive: this.scope === 'available',
|
||||
},
|
||||
{
|
||||
name: __('Stopped'),
|
||||
scope: 'stopped',
|
||||
count: this.state.stoppedCounter,
|
||||
isActive: this.scope === 'stopped',
|
||||
},
|
||||
];
|
||||
},
|
||||
activeTab() {
|
||||
return this.tabs.findIndex(({ isActive }) => isActive) ?? 0;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches all the environments and stores them.
|
||||
* Toggles loading property.
|
||||
*/
|
||||
created() {
|
||||
this.service = new EnvironmentsService(this.endpoint);
|
||||
this.requestData = { page: this.page, scope: this.scope, nested: true };
|
||||
|
||||
if (!this.isDetailView) {
|
||||
this.poll = new Poll({
|
||||
resource: this.service,
|
||||
method: 'fetchEnvironments',
|
||||
data: this.requestData,
|
||||
successCallback: this.successCallback,
|
||||
errorCallback: this.errorCallback,
|
||||
notificationCallback: (isMakingRequest) => {
|
||||
this.isMakingRequest = isMakingRequest;
|
||||
},
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
this.isLoading = true;
|
||||
this.poll.makeRequest();
|
||||
} else {
|
||||
this.fetchEnvironments();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
this.poll.restart();
|
||||
} else {
|
||||
this.poll.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
eventHub.$on('postAction', this.postAction);
|
||||
|
||||
eventHub.$on('requestStopEnvironment', this.updateStopModal);
|
||||
eventHub.$on('stopEnvironment', this.stopEnvironment);
|
||||
|
||||
eventHub.$on('requestDeleteEnvironment', this.updateDeleteModal);
|
||||
eventHub.$on('deleteEnvironment', this.deleteEnvironment);
|
||||
|
||||
eventHub.$on('requestRollbackEnvironment', this.updateRollbackModal);
|
||||
eventHub.$on('rollbackEnvironment', this.rollbackEnvironment);
|
||||
|
||||
eventHub.$on('cancelAutoStop', this.cancelAutoStop);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
eventHub.$off('postAction', this.postAction);
|
||||
|
||||
eventHub.$off('requestStopEnvironment', this.updateStopModal);
|
||||
eventHub.$off('stopEnvironment', this.stopEnvironment);
|
||||
|
||||
eventHub.$off('requestDeleteEnvironment', this.updateDeleteModal);
|
||||
eventHub.$off('deleteEnvironment', this.deleteEnvironment);
|
||||
|
||||
eventHub.$off('requestRollbackEnvironment', this.updateRollbackModal);
|
||||
eventHub.$off('rollbackEnvironment', this.rollbackEnvironment);
|
||||
|
||||
eventHub.$off('cancelAutoStop', this.cancelAutoStop);
|
||||
},
|
||||
};
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/**
|
||||
* API callbacks for pagination and tabs
|
||||
*
|
||||
* Components need to have `scope`, `page` and `requestData`
|
||||
*/
|
||||
import { validateParams } from '~/ci/pipeline_details/utils';
|
||||
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
onChangeTab(scope) {
|
||||
if (this.scope === scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
let params = {
|
||||
scope,
|
||||
page: '1',
|
||||
nested: true,
|
||||
};
|
||||
|
||||
params = this.onChangeWithFilter(params);
|
||||
|
||||
this.updateContent(params);
|
||||
},
|
||||
|
||||
onChangePage(page) {
|
||||
/* URLS parameters are strings, we need to parse to match types */
|
||||
let params = {
|
||||
page: Number(page).toString(),
|
||||
nested: true,
|
||||
};
|
||||
|
||||
if (this.scope) {
|
||||
params.scope = this.scope;
|
||||
}
|
||||
|
||||
params = this.onChangeWithFilter(params);
|
||||
|
||||
this.updateContent(params);
|
||||
},
|
||||
|
||||
onChangeWithFilter(params) {
|
||||
return { ...params, ...validateParams(this.requestData) };
|
||||
},
|
||||
|
||||
updateInternalState(parameters) {
|
||||
// stop polling
|
||||
this.poll.stop();
|
||||
|
||||
const queryString = Object.keys(parameters)
|
||||
.map((parameter) => {
|
||||
const value = parameters[parameter];
|
||||
// update internal state for UI
|
||||
this[parameter] = value;
|
||||
return `${parameter}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
.join('&');
|
||||
|
||||
// update polling parameters
|
||||
this.requestData = parameters;
|
||||
|
||||
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
|
||||
|
||||
this.isLoading = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -8,7 +8,6 @@ import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
|
|||
import EnvironmentBreadcrumbs from './environment_details/environment_breadcrumbs.vue';
|
||||
import EnvironmentsDetailHeader from './components/environments_detail_header.vue';
|
||||
import { apolloProvider as createApolloProvider } from './graphql/client';
|
||||
import environmentsMixin from './mixins/environments_mixin';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(GlToast);
|
||||
|
|
@ -23,7 +22,6 @@ export const initHeader = () => {
|
|||
return new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
mixins: [environmentsMixin],
|
||||
provide: {
|
||||
projectFullPath: dataset.projectFullPath,
|
||||
},
|
||||
|
|
@ -36,10 +34,8 @@ export const initHeader = () => {
|
|||
hasTerminals: dataset.hasTerminals,
|
||||
autoStopAt: dataset.autoStopAt,
|
||||
onSingleEnvironmentPage: true,
|
||||
// TODO: These two props are snake_case because the environments_mixin file uses
|
||||
// them and the mixin is imported in several files. It would be nice to convert them to camelCase.
|
||||
stop_path: dataset.environmentStopPath,
|
||||
delete_path: dataset.environmentDeletePath,
|
||||
stopPath: dataset.environmentStopPath,
|
||||
deletePath: dataset.environmentDeletePath,
|
||||
descriptionHtml: dataset.descriptionHtml,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
export default class EnvironmentsService {
|
||||
constructor(endpoint) {
|
||||
this.environmentsEndpoint = endpoint;
|
||||
this.folderResults = 3;
|
||||
}
|
||||
|
||||
fetchEnvironments(options = {}) {
|
||||
const { scope, page, nested } = options;
|
||||
return axios.get(this.environmentsEndpoint, { params: { scope, page, nested } });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
postAction(endpoint) {
|
||||
return axios.post(endpoint, {});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
deleteAction(endpoint) {
|
||||
return axios.delete(endpoint, {});
|
||||
}
|
||||
|
||||
getFolderContent(folderUrl, scope) {
|
||||
return axios.get(`${folderUrl}.json?per_page=${this.folderResults}&scope=${scope}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
|
||||
import { setDeployBoard } from './helpers';
|
||||
|
||||
/**
|
||||
* Environments Store.
|
||||
*
|
||||
* Stores received environments, count of stopped environments and count of
|
||||
* available environments.
|
||||
*/
|
||||
export default class EnvironmentsStore {
|
||||
constructor() {
|
||||
this.state = {};
|
||||
this.state.environments = [];
|
||||
this.state.stoppedCounter = 0;
|
||||
this.state.availableCounter = 0;
|
||||
this.state.paginationInformation = {};
|
||||
this.state.reviewAppDetails = {};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Stores the received environments.
|
||||
*
|
||||
* In the main environments endpoint (with { nested: true } in params), each folder
|
||||
* has the following schema:
|
||||
* { name: String, size: Number, latest: Object }
|
||||
* In the endpoint to retrieve environments from each folder, the environment does
|
||||
* not have the `latest` key and the data is all in the root level.
|
||||
* To avoid doing this check in the view, we store both cases the same by extracting
|
||||
* what is inside the `latest` key.
|
||||
*
|
||||
* If the `size` is bigger than 1, it means it should be rendered as a folder.
|
||||
* In those cases we add `isFolder` key in order to render it properly.
|
||||
*
|
||||
* Top level environments - when the size is 1 - with `rollout_status`
|
||||
* can render a deploy board. We add `isDeployBoardVisible` and `deployBoardData`
|
||||
* keys to those environments.
|
||||
* The first key will let's us know if we should or not render the deploy board.
|
||||
* It will be toggled when the user clicks to seee the deploy board.
|
||||
*
|
||||
* The second key will allow us to update the environment with the received deploy board data.
|
||||
*
|
||||
* @param {Array} environments
|
||||
* @returns {Array}
|
||||
*/
|
||||
storeEnvironments(environments = []) {
|
||||
const filteredEnvironments = environments.map((env) => {
|
||||
const oldEnvironmentState =
|
||||
this.state.environments.find((element) => {
|
||||
if (env.latest) {
|
||||
return element.id === env.latest.id;
|
||||
}
|
||||
return element.id === env.id;
|
||||
}) || {};
|
||||
|
||||
let filtered = {};
|
||||
|
||||
if (env.size > 1) {
|
||||
filtered = {
|
||||
...env,
|
||||
isFolder: true,
|
||||
isLoadingFolderContent: oldEnvironmentState.isLoading || false,
|
||||
folderName: env.name,
|
||||
isOpen: oldEnvironmentState.isOpen || false,
|
||||
children: oldEnvironmentState.children || [],
|
||||
};
|
||||
}
|
||||
|
||||
if (env.latest) {
|
||||
filtered = Object.assign(filtered, env, env.latest);
|
||||
delete filtered.latest;
|
||||
} else {
|
||||
filtered = Object.assign(filtered, env);
|
||||
}
|
||||
|
||||
filtered = setDeployBoard(oldEnvironmentState, filtered);
|
||||
return filtered;
|
||||
});
|
||||
|
||||
this.state.environments = filteredEnvironments;
|
||||
|
||||
return filteredEnvironments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the pagination information needed to render the pagination for the
|
||||
* table.
|
||||
*
|
||||
* Normalizes the headers to uppercase since they can be provided either
|
||||
* in uppercase or lowercase.
|
||||
*
|
||||
* Parses to an integer the normalized ones needed for the pagination component.
|
||||
*
|
||||
* Stores the normalized and parsed information.
|
||||
*
|
||||
* @param {Object} pagination = {}
|
||||
* @return {Object}
|
||||
*/
|
||||
setPagination(pagination = {}) {
|
||||
const normalizedHeaders = normalizeHeaders(pagination);
|
||||
const paginationInformation = parseIntPagination(normalizedHeaders);
|
||||
|
||||
this.state.paginationInformation = paginationInformation;
|
||||
return paginationInformation;
|
||||
}
|
||||
|
||||
setReviewAppDetails(details = {}) {
|
||||
this.state.reviewAppDetails = details;
|
||||
return details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the number of available environments.
|
||||
*
|
||||
* @param {Number} count = 0
|
||||
* @return {Number}
|
||||
*/
|
||||
storeAvailableCount(count = 0) {
|
||||
this.state.availableCounter = count;
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the number of closed environments.
|
||||
*
|
||||
* @param {Number} count = 0
|
||||
* @return {Number}
|
||||
*/
|
||||
storeStoppedCount(count = 0) {
|
||||
this.state.stoppedCounter = count;
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles deploy board visibility for the provided environment ID.
|
||||
*
|
||||
* @param {Object} environment
|
||||
* @return {Array}
|
||||
*/
|
||||
toggleDeployBoard(environmentID) {
|
||||
const environments = this.state.environments.slice();
|
||||
|
||||
this.state.environments = environments.map((env) => {
|
||||
let updated = { ...env };
|
||||
|
||||
if (env.id === environmentID) {
|
||||
updated = { ...updated, isDeployBoardVisible: !env.isDeployBoardVisible };
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
|
||||
return this.state.environments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles folder open property for the given folder.
|
||||
*
|
||||
* @param {Object} folder
|
||||
* @return {Array}
|
||||
*/
|
||||
toggleFolder(folder) {
|
||||
return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the folder with the received environments.
|
||||
*
|
||||
*
|
||||
* @param {Object} folder Folder to update
|
||||
* @param {Array} environments Received environments
|
||||
* @return {Object}
|
||||
*/
|
||||
setfolderContent(folder, environments) {
|
||||
const updatedEnvironments = environments.map((env) => {
|
||||
let updated = env;
|
||||
|
||||
if (env.latest) {
|
||||
updated = { ...env, ...env.latest };
|
||||
delete updated.latest;
|
||||
} else {
|
||||
updated = env;
|
||||
}
|
||||
|
||||
updated.isChildren = true;
|
||||
|
||||
updated = setDeployBoard(env, updated);
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
return this.updateEnvironmentProp(folder, 'children', updatedEnvironments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a environment, a prop and a new value updates the correct environment.
|
||||
*
|
||||
* @param {Object} environment
|
||||
* @param {String} prop
|
||||
* @param {String|Boolean|Object|Array} newValue
|
||||
* @return {Array}
|
||||
*/
|
||||
updateEnvironmentProp(environment, prop, newValue) {
|
||||
const { environments } = this.state;
|
||||
|
||||
const updatedEnvironments = environments.map((env) => {
|
||||
const updateEnv = { ...env };
|
||||
if (env.id === environment.id) {
|
||||
updateEnv[prop] = newValue;
|
||||
}
|
||||
|
||||
return updateEnv;
|
||||
});
|
||||
|
||||
this.state.environments = updatedEnvironments;
|
||||
}
|
||||
|
||||
getOpenFolders() {
|
||||
const { environments } = this.state;
|
||||
|
||||
return environments.filter((env) => env.isFolder && env.isOpen);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
/**
|
||||
* @param {Object} environment
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const setDeployBoard = (oldEnvironmentState, environment) => {
|
||||
let parsedEnvironment = environment;
|
||||
if (!environment.isFolder && environment.rollout_status) {
|
||||
parsedEnvironment = {
|
||||
...environment,
|
||||
hasDeployBoard: true,
|
||||
isDeployBoardVisible:
|
||||
oldEnvironmentState.isDeployBoardVisible === false
|
||||
? oldEnvironmentState.isDeployBoardVisible
|
||||
: true,
|
||||
deployBoardData:
|
||||
environment.rollout_status.status === 'found' ? environment.rollout_status : {},
|
||||
isLoadingDeployBoard: environment.rollout_status.status === 'loading',
|
||||
isEmptyDeployBoard: environment.rollout_status.status === 'not_found',
|
||||
};
|
||||
}
|
||||
return parsedEnvironment;
|
||||
};
|
||||
|
|
@ -4,14 +4,17 @@ import { __ } from '~/locale';
|
|||
|
||||
const badgeVariants = {
|
||||
issues: { opened: 'success', closed: 'info' },
|
||||
workItems: { OPEN: 'success', CLOSED: 'info' },
|
||||
mergeRequests: { opened: 'success', closed: 'danger', merged: 'info' },
|
||||
};
|
||||
const badgeLabels = {
|
||||
issues: { opened: __('Open'), closed: __('Closed') },
|
||||
workItems: { OPEN: __('Open'), CLOSED: __('Closed') },
|
||||
mergeRequests: { opened: __('Open'), closed: __('Closed'), merged: __('Merged') },
|
||||
};
|
||||
const badgeIcons = {
|
||||
issues: { opened: 'issue-open-m', closed: 'issue-close' },
|
||||
workItems: { OPEN: 'issue-open-m', CLOSED: 'issue-close' },
|
||||
mergeRequests: {
|
||||
opened: 'merge-request-open',
|
||||
closed: 'merge-request-close',
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export default {
|
|||
<gl-avatar :src="user.avatarUrl" :size="32" class="!gl-bg-white" />
|
||||
<span
|
||||
v-if="reviewStateIcon(user)"
|
||||
class="gl-absolute -gl-bottom-2 -gl-right-2 gl-flex gl-h-5 gl-w-5 gl-items-center gl-justify-center gl-rounded-full gl-p-1"
|
||||
class="gl-absolute -gl-bottom-2 -gl-left-2 gl-flex gl-h-5 gl-w-5 gl-items-center gl-justify-center gl-rounded-full gl-p-1"
|
||||
:class="reviewStateIcon(user).backgroundClass"
|
||||
data-testid="review-state-icon"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,13 @@ export const todoLabel = (hasTodo) => {
|
|||
return hasTodo ? __('Mark as done') : __('Add a to-do item');
|
||||
};
|
||||
|
||||
/**
|
||||
* Optimistic update of to-do count, use this function if you have a delta
|
||||
* of todos after a user interaction, e.g. answering to a thread or closing an MR.
|
||||
*
|
||||
* This likely should be followed by re-fetching all user counts
|
||||
* @param {number} delta
|
||||
*/
|
||||
export const updateGlobalTodoCount = (delta) => {
|
||||
// Optimistic update of user counts
|
||||
document.dispatchEvent(new CustomEvent('todo:toggle', { detail: { delta } }));
|
||||
|
|
|
|||
|
|
@ -77,3 +77,17 @@ export function createUserCountsManager() {
|
|||
broadcastUserCounts(userCounts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EXACT update of the user to-do count. Only use this one, if you got the new
|
||||
* to-count returned by an API. Will also broadcast the current count to
|
||||
* all other open tabs
|
||||
*
|
||||
* @param {number} count
|
||||
*/
|
||||
export const setGlobalTodoCount = (count) => {
|
||||
if (Number.isSafeInteger(count) && count >= 0) {
|
||||
userCounts.todos = count;
|
||||
broadcastUserCounts({ todos: userCounts.todos, last_update: Date.now() });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
|||
import { reportToSentry } from '~/ci/utils';
|
||||
import { s__ } from '~/locale';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import { updateGlobalTodoCount } from '~/sidebar/utils';
|
||||
import { INSTRUMENT_TODO_ITEM_CLICK, TODO_STATE_DONE, TODO_STATE_PENDING } from '../constants';
|
||||
import markAsDoneMutation from './mutations/mark_as_done.mutation.graphql';
|
||||
import markAsPendingMutation from './mutations/mark_as_pending.mutation.graphql';
|
||||
|
|
@ -76,15 +77,21 @@ export default {
|
|||
variables: {
|
||||
todoId: this.todo.id,
|
||||
},
|
||||
optimisticResponse: {
|
||||
toggleStatus: {
|
||||
todo: {
|
||||
id: this.todo.id,
|
||||
state: this.isDone ? TODO_STATE_PENDING : TODO_STATE_DONE,
|
||||
__typename: 'Todo',
|
||||
optimisticResponse: () => {
|
||||
if (!this.isSnoozed) {
|
||||
updateGlobalTodoCount(this.isDone ? +1 : -1);
|
||||
}
|
||||
|
||||
return {
|
||||
toggleStatus: {
|
||||
todo: {
|
||||
id: this.todo.id,
|
||||
state: this.isDone ? TODO_STATE_PENDING : TODO_STATE_DONE,
|
||||
__typename: 'Todo',
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
TODO_WAIT_BEFORE_RELOAD,
|
||||
TABS_INDICES,
|
||||
} from '~/todos/constants';
|
||||
import { setGlobalTodoCount, userCounts } from '~/super_sidebar/user_counts_manager';
|
||||
import getTodosQuery from './queries/get_todos.query.graphql';
|
||||
import getPendingTodosCount from './queries/get_pending_todos_count.query.graphql';
|
||||
import TodoItem from './todo_item.vue';
|
||||
|
|
@ -72,7 +73,7 @@ export default {
|
|||
pageInfo: {},
|
||||
todos: [],
|
||||
currentTab: TABS_INDICES.pending,
|
||||
pendingTodosCount: '-',
|
||||
refreshPendingCount: null,
|
||||
queryFilterValues: {
|
||||
groupId: [],
|
||||
projectId: [],
|
||||
|
|
@ -116,17 +117,23 @@ export default {
|
|||
this.needsRefresh = false;
|
||||
},
|
||||
},
|
||||
pendingTodosCount: {
|
||||
refreshPendingCount: {
|
||||
query: getPendingTodosCount,
|
||||
variables() {
|
||||
return this.queryFilterValues;
|
||||
},
|
||||
update({ currentUser: { todos: { count } } = {} }) {
|
||||
return count;
|
||||
manual: true,
|
||||
result({ loading, data }) {
|
||||
if (!loading) {
|
||||
setGlobalTodoCount(data?.currentUser?.todos?.count);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pendingTodosCount() {
|
||||
return userCounts.todos;
|
||||
},
|
||||
statusByTab() {
|
||||
return STATUS_BY_TAB[this.currentTab];
|
||||
},
|
||||
|
|
@ -294,7 +301,7 @@ export default {
|
|||
this.updateAllQueries(false);
|
||||
},
|
||||
updateCounts() {
|
||||
return this.$apollo.queries.pendingTodosCount.refetch();
|
||||
return this.$apollo.queries.refreshPendingCount.refetch();
|
||||
},
|
||||
async updateAllQueries(showLoading = true) {
|
||||
this.$root.$emit('bv::hide::tooltip', 'todo-refresh-btn');
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { s__ } from '~/locale';
|
|||
import { reportToSentry } from '~/ci/utils';
|
||||
import Tracking from '~/tracking';
|
||||
import { INSTRUMENT_TODO_ITEM_CLICK } from '~/todos/constants';
|
||||
import { updateGlobalTodoCount } from '~/sidebar/utils';
|
||||
import { snoozeTodo } from '../utils';
|
||||
import unSnoozeTodoMutation from './mutations/un_snooze_todo.mutation.graphql';
|
||||
import SnoozeTimePicker from './todo_snooze_until_picker.vue';
|
||||
|
|
@ -61,15 +62,19 @@ export default {
|
|||
variables: {
|
||||
todoId: this.todo.id,
|
||||
},
|
||||
optimisticResponse: {
|
||||
todoUnSnooze: {
|
||||
todo: {
|
||||
id: this.todo.id,
|
||||
snoozedUntil: null,
|
||||
__typename: 'Todo',
|
||||
optimisticResponse: () => {
|
||||
updateGlobalTodoCount(+1);
|
||||
|
||||
return {
|
||||
todoUnSnooze: {
|
||||
todo: {
|
||||
id: this.todo.id,
|
||||
snoozedUntil: null,
|
||||
__typename: 'Todo',
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { updateGlobalTodoCount } from '~/sidebar/utils';
|
||||
import snoozeTodoMutation from './components/mutations/snooze_todo.mutation.graphql';
|
||||
|
||||
export function snoozeTodo(apolloClient, todo, until) {
|
||||
|
|
@ -7,15 +8,19 @@ export function snoozeTodo(apolloClient, todo, until) {
|
|||
todoId: todo.id,
|
||||
snoozeUntil: until,
|
||||
},
|
||||
optimisticResponse: {
|
||||
todoSnooze: {
|
||||
todo: {
|
||||
id: todo.id,
|
||||
snoozedUntil: until,
|
||||
__typename: 'Todo',
|
||||
optimisticResponse: () => {
|
||||
updateGlobalTodoCount(-1);
|
||||
|
||||
return {
|
||||
todoSnooze: {
|
||||
todo: {
|
||||
id: todo.id,
|
||||
snoozedUntil: until,
|
||||
__typename: 'Todo',
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import TabContentLoadingIndicator from './tab_content_loading_indicator.vue';
|
||||
import TabContentLoadingError from './tab_content_loading_error.vue';
|
||||
|
||||
/**
|
||||
* Creates a wrapper for async components to show loading and error states
|
||||
* https://v2.vuejs.org/v2/guide/components-dynamic-async#Async-Components
|
||||
*/
|
||||
|
||||
export const createAsyncTabContentWrapper = (component) => {
|
||||
return {
|
||||
loading: TabContentLoadingIndicator,
|
||||
error: TabContentLoadingError,
|
||||
component,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { createAsyncTabContentWrapper } from './index';
|
||||
|
||||
export default {
|
||||
title: 'usage_quotas/async_tab_content_wrapper',
|
||||
};
|
||||
|
||||
const Template = (component) => {
|
||||
const AsyncTabComponent = () => createAsyncTabContentWrapper(component);
|
||||
|
||||
return {
|
||||
render(h) {
|
||||
return h(AsyncTabComponent);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const Default = () =>
|
||||
Template(
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ template: '<div>Loaded!</div>' });
|
||||
}, 1000);
|
||||
}),
|
||||
);
|
||||
|
||||
export const Error = () =>
|
||||
Template(
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(Error('null'));
|
||||
}, 1000);
|
||||
}),
|
||||
);
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
name: 'TabContentLoadingError',
|
||||
components: { GlAlert },
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-alert
|
||||
class="gl-my-3"
|
||||
variant="danger"
|
||||
:dismissible="false"
|
||||
:title="s__('UsageQuotas|There was an error while loading the tab contents')"
|
||||
>
|
||||
{{ s__('UsageQuotas|Reload the page to try again') }}
|
||||
</gl-alert>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
name: 'TabContentLoadingIndicator',
|
||||
components: { GlLoadingIcon },
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-my-5 gl-flex gl-w-full gl-items-center gl-justify-center gl-gap-3 gl-text-lg">
|
||||
{{ s__('UsageQuotas|Loading Usage Quotas tab content') }}
|
||||
<gl-loading-icon size="md" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
import { parseNamespaceProvideData } from 'ee_else_ce/usage_quotas/storage/namespace/utils';
|
||||
import { createAsyncTabContentWrapper } from '~/usage_quotas/components/async_tab_content_wrapper';
|
||||
import { getStorageTabMetadata } from '../utils';
|
||||
import NamespaceStorageApp from './components/namespace_storage_app.vue';
|
||||
|
||||
export const getNamespaceStorageTabMetadata = ({ customApolloProvider } = {}) => {
|
||||
const NamespaceStorageApp = () => {
|
||||
const component = import(
|
||||
/* webpackChunkName: 'uq_storage_namespace' */ './components/namespace_storage_app.vue'
|
||||
);
|
||||
return createAsyncTabContentWrapper(component);
|
||||
};
|
||||
|
||||
return getStorageTabMetadata({
|
||||
vueComponent: NamespaceStorageApp,
|
||||
parseProvideData: parseNamespaceProvideData,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
import { createAsyncTabContentWrapper } from '~/usage_quotas/components/async_tab_content_wrapper';
|
||||
import { getStorageTabMetadata } from '../utils';
|
||||
import ProjectStorageApp from './components/project_storage_app.vue';
|
||||
import { parseProjectProvideData } from './utils';
|
||||
|
||||
export const getProjectStorageTabMetadata = () => {
|
||||
const ProjectStorageApp = () => {
|
||||
const component = import(
|
||||
/* webpackChunkName: 'uq_storage_project' */ './components/project_storage_app.vue'
|
||||
);
|
||||
return createAsyncTabContentWrapper(component);
|
||||
};
|
||||
|
||||
return getStorageTabMetadata({
|
||||
vueComponent: ProjectStorageApp,
|
||||
parseProvideData: parseProjectProvideData,
|
||||
|
|
|
|||
|
|
@ -1,26 +1,19 @@
|
|||
<script>
|
||||
import {
|
||||
GlButton,
|
||||
GlDatepicker,
|
||||
GlFormGroup,
|
||||
GlOutsideDirective as Outside,
|
||||
GlFormRadio,
|
||||
} from '@gitlab/ui';
|
||||
import { GlDatepicker, GlFormGroup, GlFormRadio } from '@gitlab/ui';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { newWorkItemId, findStartAndDueDateWidget } from '~/work_items/utils';
|
||||
import { findStartAndDueDateWidget, newWorkItemId } from '~/work_items/utils';
|
||||
import { s__ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import { Mousetrap } from '~/lib/mousetrap';
|
||||
import { keysFor, SIDEBAR_CLOSE_WIDGET } from '~/behaviors/shortcuts/keybindings';
|
||||
import { formatDate, newDate, toISODateFormat } from '~/lib/utils/datetime_utility';
|
||||
import {
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
sprintfWorkItem,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
WIDGET_TYPE_START_AND_DUE_DATE,
|
||||
} from '~/work_items/constants';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '~/work_items/graphql/update_new_work_item.mutation.graphql';
|
||||
} from '../constants';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
|
||||
import WorkItemSidebarWidget from './shared/work_item_sidebar_widget.vue';
|
||||
|
||||
const nullObjectDate = new Date(0);
|
||||
|
||||
|
|
@ -28,24 +21,13 @@ const ROLLUP_TYPE_FIXED = 'fixed';
|
|||
const ROLLUP_TYPE_INHERITED = 'inherited';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
dates: s__('WorkItem|Dates'),
|
||||
dueDate: s__('WorkItem|Due'),
|
||||
none: s__('WorkItem|None'),
|
||||
startDate: s__('WorkItem|Start'),
|
||||
fixed: s__('WorkItem|Fixed'),
|
||||
inherited: s__('WorkItem|Inherited'),
|
||||
},
|
||||
dueDateInputId: 'due-date-input',
|
||||
startDateInputId: 'start-date-input',
|
||||
components: {
|
||||
GlButton,
|
||||
GlDatepicker,
|
||||
GlFormGroup,
|
||||
GlFormRadio,
|
||||
},
|
||||
directives: {
|
||||
Outside,
|
||||
WorkItemSidebarWidget,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
|
|
@ -89,10 +71,9 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
dirtyDueDate: null,
|
||||
dirtyStartDate: null,
|
||||
localDueDate: null,
|
||||
localStartDate: null,
|
||||
isUpdating: false,
|
||||
isEditing: false,
|
||||
rollupType: null,
|
||||
};
|
||||
},
|
||||
|
|
@ -101,13 +82,13 @@ export default {
|
|||
return this.workItem.id;
|
||||
},
|
||||
datesUnchanged() {
|
||||
const dirtyDueDate = this.dirtyDueDate || nullObjectDate;
|
||||
const dirtyStartDate = this.dirtyStartDate || nullObjectDate;
|
||||
const localDueDate = this.localDueDate || nullObjectDate;
|
||||
const localStartDate = this.localStartDate || nullObjectDate;
|
||||
const dueDate = this.dueDate ? newDate(this.dueDate) : nullObjectDate;
|
||||
const startDate = this.startDate ? newDate(this.startDate) : nullObjectDate;
|
||||
return (
|
||||
dirtyDueDate.getTime() === dueDate.getTime() &&
|
||||
dirtyStartDate.getTime() === startDate.getTime()
|
||||
localDueDate.getTime() === dueDate.getTime() &&
|
||||
localStartDate.getTime() === startDate.getTime()
|
||||
);
|
||||
},
|
||||
isDatepickerDisabled() {
|
||||
|
|
@ -124,10 +105,10 @@ export default {
|
|||
startDateValue() {
|
||||
return this.startDate
|
||||
? formatDate(this.startDate, 'mmm d, yyyy', true)
|
||||
: this.$options.i18n.none;
|
||||
: s__('WorkItem|None');
|
||||
},
|
||||
dueDateValue() {
|
||||
return this.dueDate ? formatDate(this.dueDate, 'mmm d, yyyy', true) : this.$options.i18n.none;
|
||||
return this.dueDate ? formatDate(this.dueDate, 'mmm d, yyyy', true) : s__('WorkItem|None');
|
||||
},
|
||||
optimisticResponse() {
|
||||
const workItemDatesWidget = findStartAndDueDateWidget(this.workItem);
|
||||
|
|
@ -143,8 +124,8 @@ export default {
|
|||
),
|
||||
{
|
||||
...workItemDatesWidget,
|
||||
dueDate: this.dirtyDueDate ? toISODateFormat(this.dirtyDueDate) : null,
|
||||
startDate: this.dirtyStartDate ? toISODateFormat(this.dirtyStartDate) : null,
|
||||
dueDate: this.localDueDate ? toISODateFormat(this.localDueDate) : null,
|
||||
startDate: this.localStartDate ? toISODateFormat(this.localStartDate) : null,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -155,13 +136,13 @@ export default {
|
|||
watch: {
|
||||
dueDate: {
|
||||
handler(newDueDate) {
|
||||
this.dirtyDueDate = newDate(newDueDate);
|
||||
this.localDueDate = newDate(newDueDate);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
startDate: {
|
||||
handler(newStartDate) {
|
||||
this.dirtyStartDate = newDate(newStartDate);
|
||||
this.localStartDate = newDate(newStartDate);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
|
|
@ -172,22 +153,16 @@ export default {
|
|||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
Mousetrap.bind(keysFor(SIDEBAR_CLOSE_WIDGET), this.collapseWidget);
|
||||
},
|
||||
beforeDestroy() {
|
||||
Mousetrap.unbind(keysFor(SIDEBAR_CLOSE_WIDGET));
|
||||
},
|
||||
methods: {
|
||||
clearDueDatePicker() {
|
||||
this.dirtyDueDate = null;
|
||||
this.localDueDate = null;
|
||||
},
|
||||
clearStartDatePicker() {
|
||||
this.dirtyStartDate = null;
|
||||
this.localStartDate = null;
|
||||
},
|
||||
handleStartDateInput() {
|
||||
if (this.dirtyDueDate && this.dirtyStartDate > this.dirtyDueDate) {
|
||||
this.dirtyDueDate = this.dirtyStartDate;
|
||||
if (this.localDueDate && this.localStartDate > this.localDueDate) {
|
||||
this.localDueDate = this.localStartDate;
|
||||
}
|
||||
},
|
||||
updateRollupType() {
|
||||
|
|
@ -260,8 +235,8 @@ export default {
|
|||
fullPath: this.fullPath,
|
||||
rolledUpDates: {
|
||||
isFixed: true,
|
||||
dueDate: this.dirtyDueDate ? toISODateFormat(this.dirtyDueDate) : null,
|
||||
startDate: this.dirtyStartDate ? toISODateFormat(this.dirtyStartDate) : null,
|
||||
dueDate: this.localDueDate ? toISODateFormat(this.localDueDate) : null,
|
||||
startDate: this.localStartDate ? toISODateFormat(this.localStartDate) : null,
|
||||
rollUp: this.shouldRollUp,
|
||||
},
|
||||
},
|
||||
|
|
@ -280,8 +255,8 @@ export default {
|
|||
id: this.workItemId,
|
||||
startAndDueDateWidget: {
|
||||
isFixed: true,
|
||||
dueDate: this.dirtyDueDate ? toISODateFormat(this.dirtyDueDate) : null,
|
||||
startDate: this.dirtyStartDate ? toISODateFormat(this.dirtyStartDate) : null,
|
||||
dueDate: this.localDueDate ? toISODateFormat(this.localDueDate) : null,
|
||||
startDate: this.localStartDate ? toISODateFormat(this.localStartDate) : null,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -301,132 +276,94 @@ export default {
|
|||
this.isUpdating = false;
|
||||
});
|
||||
},
|
||||
expandWidget() {
|
||||
this.isEditing = true;
|
||||
},
|
||||
collapseWidget(event = {}) {
|
||||
// This prevents outside directive from treating
|
||||
// a click on a select element within datepicker as an outside click,
|
||||
// therefore allowing user to select a month and a year without
|
||||
// triggering the mutation and immediately closing the dropdown
|
||||
if (event.target?.classList.contains('pika-select', 'pika-select-month', 'pika-select-year'))
|
||||
return;
|
||||
this.isEditing = false;
|
||||
this.updateDates();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section data-testid="work-item-due-dates">
|
||||
<div class="gl-flex gl-items-center gl-gap-3">
|
||||
<h3 :class="{ 'gl-sr-only': isEditing }" class="gl-heading-5 !gl-mb-0">
|
||||
{{ $options.i18n.dates }}
|
||||
</h3>
|
||||
<gl-button
|
||||
v-if="canUpdate && !isEditing"
|
||||
data-testid="edit-button"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
class="gl-ml-auto"
|
||||
:disabled="isUpdating"
|
||||
@click="expandWidget"
|
||||
>{{ __('Edit') }}</gl-button
|
||||
>
|
||||
</div>
|
||||
<fieldset v-if="!isEditing && shouldRollUp" class="gl-mt-2 gl-flex gl-gap-5">
|
||||
<gl-form-radio
|
||||
v-model="rollupType"
|
||||
value="fixed"
|
||||
:disabled="!canUpdate || isUpdating"
|
||||
@change="updateRollupType"
|
||||
>
|
||||
{{ $options.i18n.fixed }}
|
||||
</gl-form-radio>
|
||||
<gl-form-radio
|
||||
v-model="rollupType"
|
||||
value="inherited"
|
||||
:disabled="!canUpdate || isUpdating"
|
||||
@change="updateRollupType"
|
||||
>
|
||||
{{ $options.i18n.inherited }}
|
||||
</gl-form-radio>
|
||||
</fieldset>
|
||||
<fieldset v-if="isEditing" data-testid="datepicker-wrapper">
|
||||
<div class="gl-flex gl-items-center gl-justify-between">
|
||||
<legend class="gl-mb-0 gl-border-b-0 gl-text-base gl-font-bold">
|
||||
{{ $options.i18n.dates }}
|
||||
</legend>
|
||||
<gl-button
|
||||
data-testid="apply-button"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
class="gl-mr-2"
|
||||
:disabled="isUpdating"
|
||||
@click="collapseWidget"
|
||||
>{{ __('Apply') }}</gl-button
|
||||
<work-item-sidebar-widget
|
||||
:can-update="canUpdate"
|
||||
:is-updating="isUpdating"
|
||||
data-testid="work-item-due-dates"
|
||||
@stopEditing="updateDates"
|
||||
>
|
||||
<template #title>
|
||||
{{ s__('WorkItem|Dates') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<fieldset v-if="shouldRollUp" class="gl-mt-2 gl-flex gl-gap-5">
|
||||
<legend class="gl-sr-only">{{ s__('WorkItem|Dates') }}</legend>
|
||||
<gl-form-radio
|
||||
v-model="rollupType"
|
||||
value="fixed"
|
||||
:disabled="!canUpdate || isUpdating"
|
||||
@change="updateRollupType"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-outside="collapseWidget"
|
||||
class="gl-flex gl-flex-col gl-flex-wrap gl-gap-x-5 gl-gap-y-3 gl-pt-2 sm:gl-flex-row md:gl-flex-col"
|
||||
>
|
||||
<gl-form-group
|
||||
class="gl-m-0 gl-flex gl-items-center gl-gap-3"
|
||||
:label="$options.i18n.startDate"
|
||||
:label-for="$options.startDateInputId"
|
||||
label-class="!gl-font-normal !gl-pb-0 gl-min-w-7 sm:gl-min-w-fit md:gl-min-w-7 gl-break-words"
|
||||
{{ s__('WorkItem|Fixed') }}
|
||||
</gl-form-radio>
|
||||
<gl-form-radio
|
||||
v-model="rollupType"
|
||||
value="inherited"
|
||||
:disabled="!canUpdate || isUpdating"
|
||||
@change="updateRollupType"
|
||||
>
|
||||
<gl-datepicker
|
||||
ref="startDatePicker"
|
||||
v-model="dirtyStartDate"
|
||||
container="body"
|
||||
:disabled="isDatepickerDisabled"
|
||||
:input-id="$options.startDateInputId"
|
||||
show-clear-button
|
||||
:target="null"
|
||||
class="work-item-date-picker gl-max-w-20"
|
||||
@clear="clearStartDatePicker"
|
||||
@close="handleStartDateInput"
|
||||
@keydown.esc.native="collapseWidget"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
class="gl-m-0 gl-flex gl-items-center gl-gap-3"
|
||||
:label="$options.i18n.dueDate"
|
||||
:label-for="$options.dueDateInputId"
|
||||
label-class="!gl-font-normal !gl-pb-0 gl-min-w-7 sm:gl-min-w-fit md:gl-min-w-7 gl-break-words"
|
||||
>
|
||||
<gl-datepicker
|
||||
v-model="dirtyDueDate"
|
||||
container="body"
|
||||
:disabled="isDatepickerDisabled"
|
||||
:input-id="$options.dueDateInputId"
|
||||
:min-date="dirtyStartDate"
|
||||
show-clear-button
|
||||
:target="null"
|
||||
class="work-item-date-picker gl-max-w-20"
|
||||
data-testid="due-date-picker"
|
||||
@clear="clearDueDatePicker"
|
||||
@keydown.esc.native="collapseWidget"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
</fieldset>
|
||||
<template v-else>
|
||||
{{ s__('WorkItem|Inherited') }}
|
||||
</gl-form-radio>
|
||||
</fieldset>
|
||||
<p class="gl-m-0 gl-py-1">
|
||||
<span class="gl-inline-block gl-min-w-8">{{ $options.i18n.startDate }}:</span>
|
||||
<span class="gl-inline-block gl-min-w-8">{{ s__('WorkItem|Start') }}:</span>
|
||||
<span data-testid="start-date-value" :class="{ 'gl-text-subtle': !startDate }">
|
||||
{{ startDateValue }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="gl-m-0 gl-pt-1">
|
||||
<span class="gl-inline-block gl-min-w-8">{{ $options.i18n.dueDate }}:</span>
|
||||
<span class="gl-inline-block gl-min-w-8">{{ s__('WorkItem|Due') }}:</span>
|
||||
<span data-testid="due-date-value" :class="{ 'gl-text-subtle': !dueDate }">
|
||||
{{ dueDateValue }}
|
||||
</span>
|
||||
</p>
|
||||
</template>
|
||||
</section>
|
||||
<template #editing-content="{ stopEditing }">
|
||||
<gl-form-group
|
||||
class="gl-m-0 gl-flex gl-items-center gl-gap-3"
|
||||
:label="s__('WorkItem|Start')"
|
||||
:label-for="$options.startDateInputId"
|
||||
label-class="!gl-font-normal !gl-pb-0 gl-min-w-7 sm:gl-min-w-fit md:gl-min-w-7 gl-break-words"
|
||||
>
|
||||
<gl-datepicker
|
||||
v-model="localStartDate"
|
||||
class="gl-max-w-20"
|
||||
container="body"
|
||||
:disabled="isDatepickerDisabled"
|
||||
:input-id="$options.startDateInputId"
|
||||
show-clear-button
|
||||
:target="null"
|
||||
data-testid="start-date-picker"
|
||||
@clear="clearStartDatePicker"
|
||||
@close="handleStartDateInput"
|
||||
@keydown.esc.native="stopEditing"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
class="gl-m-0 gl-flex gl-items-center gl-gap-3"
|
||||
:label="s__('WorkItem|Due')"
|
||||
:label-for="$options.dueDateInputId"
|
||||
label-class="!gl-font-normal !gl-pb-0 gl-min-w-7 sm:gl-min-w-fit md:gl-min-w-7 gl-break-words"
|
||||
>
|
||||
<gl-datepicker
|
||||
v-model="localDueDate"
|
||||
class="gl-max-w-20"
|
||||
container="body"
|
||||
:disabled="isDatepickerDisabled"
|
||||
:input-id="$options.dueDateInputId"
|
||||
:min-date="localStartDate"
|
||||
show-clear-button
|
||||
:target="null"
|
||||
data-testid="due-date-picker"
|
||||
@clear="clearDueDatePicker"
|
||||
@keydown.esc.native="stopEditing"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</template>
|
||||
</work-item-sidebar-widget>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,61 +1,5 @@
|
|||
@import 'page_bundles/mixins_and_variables_and_functions';
|
||||
|
||||
.environments-container {
|
||||
.ci-table {
|
||||
.commit-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.external-url,
|
||||
.dropdown-new {
|
||||
@apply gl-text-subtle;
|
||||
}
|
||||
|
||||
.build-link,
|
||||
.ref-name {
|
||||
color: var(--gray-900, $gray-900);
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
margin-right: 3px;
|
||||
@apply gl-fill-icon-subtle;
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.folder-name {
|
||||
cursor: pointer;
|
||||
@apply gl-text-subtle;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.environment-child-row {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-responsive-table-row {
|
||||
.branch-commit {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.folder-row {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, md)-1) {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.x-axis path,
|
||||
.y-axis path,
|
||||
.label-x-axis-line,
|
||||
|
|
@ -146,21 +90,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.deploy-board-icon {
|
||||
display: none;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
float: left;
|
||||
display: block;
|
||||
}
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
@apply gl-text-disabled;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kubernetes Tree view
|
||||
**/
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
|
||||
layout 'project'
|
||||
|
||||
before_action only: [:folder] do
|
||||
push_frontend_feature_flag(:environments_folder_new_look, project)
|
||||
end
|
||||
|
||||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:k8s_tree_view, project)
|
||||
push_frontend_feature_flag(:use_websocket_for_k8s_watch, project)
|
||||
|
|
|
|||
|
|
@ -36,14 +36,23 @@ module Mutations
|
|||
required: false,
|
||||
description: 'Variables for the pipeline schedule.'
|
||||
|
||||
argument :inputs, [Types::Ci::Inputs::InputType],
|
||||
required: false,
|
||||
description: 'Inputs for the pipeline schedule.',
|
||||
experiment: { milestone: '17.11' }
|
||||
|
||||
field :pipeline_schedule,
|
||||
Types::Ci::PipelineScheduleType,
|
||||
description: 'Updated pipeline schedule.'
|
||||
|
||||
def resolve(id:, variables: [], **pipeline_schedule_attrs)
|
||||
def resolve(id:, variables: [], inputs: [], **pipeline_schedule_attrs)
|
||||
schedule = authorized_find!(id: id)
|
||||
|
||||
params = pipeline_schedule_attrs.merge(variables_attributes: variable_attributes_for(variables))
|
||||
params = pipeline_schedule_attrs.merge(variables_attributes: transform_attributes_for(variables))
|
||||
|
||||
if Feature.enabled?(:ci_inputs_for_pipelines, schedule.project)
|
||||
params = params.merge(inputs_attributes: transform_attributes_for(inputs))
|
||||
end
|
||||
|
||||
service_response = ::Ci::PipelineSchedules::UpdateService
|
||||
.new(schedule, current_user, params)
|
||||
|
|
@ -57,9 +66,10 @@ module Mutations
|
|||
|
||||
private
|
||||
|
||||
def variable_attributes_for(variables)
|
||||
variables.map do |variable|
|
||||
variable.to_h.tap do |hash|
|
||||
# This method transforms the GraphQL argument values into values that can be understood by ActiveRecord.
|
||||
def transform_attributes_for(nodes)
|
||||
nodes.map do |node|
|
||||
node.to_h.tap do |hash|
|
||||
hash[:id] = GlobalID::Locator.locate(hash[:id]).id if hash[:id]
|
||||
|
||||
hash[:_destroy] = hash.delete(:destroy)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ module Types
|
|||
graphql_name 'CiInputsInputType'
|
||||
description 'Attributes for defining an input.'
|
||||
|
||||
argument :id,
|
||||
::Types::GlobalIDType[::Ci::PipelineScheduleInput],
|
||||
required: false,
|
||||
description: 'Global ID of the input. Only needed when updating an input.',
|
||||
experiment: { milestone: '17.11' }
|
||||
|
||||
argument :name,
|
||||
GraphQL::Types::String,
|
||||
required: true,
|
||||
|
|
@ -16,6 +22,11 @@ module Types
|
|||
Inputs::ValueInputType,
|
||||
required: true,
|
||||
description: 'Value of the input.'
|
||||
|
||||
argument :destroy,
|
||||
GraphQL::Types::Boolean,
|
||||
required: false,
|
||||
description: 'Set to `true` to delete the input.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class Packages::PackageFile < ApplicationRecord
|
|||
|
||||
INSTALLABLE_STATUSES = [:default].freeze
|
||||
ENCODED_SLASH = "%2F"
|
||||
SORTABLE_COLUMNS = %w[id file_name created_at].freeze
|
||||
|
||||
delegate :project, :project_id, to: :package
|
||||
delegate :conan_file_type, to: :conan_file_metadatum
|
||||
|
|
@ -53,7 +54,10 @@ class Packages::PackageFile < ApplicationRecord
|
|||
scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) }
|
||||
scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) }
|
||||
scope :preload_helm_file_metadata, -> { preload(:helm_file_metadatum) }
|
||||
scope :order_id_asc, -> { order(id: :asc) }
|
||||
|
||||
scope :order_by, ->(column, order = 'asc') do
|
||||
reorder(arel_table[column].method(order).call) if SORTABLE_COLUMNS.include?(column) && %w[asc desc].include?(order)
|
||||
end
|
||||
|
||||
scope :for_rubygem_with_file_name, ->(project, file_name) do
|
||||
joins(:package).merge(project.packages.rubygems).with_file_name(file_name)
|
||||
|
|
|
|||
|
|
@ -67,4 +67,4 @@
|
|||
-# Project created
|
||||
.project-page-sidebar-block.gl-py-4
|
||||
%p.gl-font-bold.gl-text-strong.gl-m-0.gl-mb-1= s_('ProjectPage|Created on')
|
||||
%span= @project.created_at.to_date.to_fs(:long)
|
||||
%span= l(@project.created_at.to_date, format: :long)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: environments_folder_new_look
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137046
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/431928
|
||||
milestone: '16.7'
|
||||
type: development
|
||||
group: group::environments
|
||||
default_enabled: false
|
||||
|
|
@ -991,6 +991,9 @@ Gitlab.ee do
|
|||
Settings.cron_jobs['ai_active_context_bulk_process_worker'] ||= {}
|
||||
Settings.cron_jobs['ai_active_context_bulk_process_worker']['cron'] ||= '*/1 * * * *'
|
||||
Settings.cron_jobs['ai_active_context_bulk_process_worker']['job_class'] ||= 'Ai::ActiveContext::BulkProcessWorker'
|
||||
Settings.cron_jobs['ai_active_context_migration_worker'] ||= {}
|
||||
Settings.cron_jobs['ai_active_context_migration_worker']['cron'] ||= '*/5 * * * *'
|
||||
Settings.cron_jobs['ai_active_context_migration_worker']['job_class'] ||= 'Ai::ActiveContext::MigrationWorker'
|
||||
Settings.cron_jobs['namespaces_enable_descendants_cache_cron_worker'] ||= {}
|
||||
Settings.cron_jobs['namespaces_enable_descendants_cache_cron_worker']['cron'] ||= '*/11 * * * *'
|
||||
Settings.cron_jobs['namespaces_enable_descendants_cache_cron_worker']['job_class'] = 'Namespaces::EnableDescendantsCacheCronWorker'
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ You can use the following GitLab Duo Chat features with GitLab Duo Self-Hosted:
|
|||
- [Ask about or generate code](../../user/gitlab_duo_chat/examples.md#ask-about-or-generate-code)
|
||||
- [Ask follow up questions](../../user/gitlab_duo_chat/examples.md#ask-follow-up-questions)
|
||||
- [Ask about errors](../../user/gitlab_duo_chat/examples.md#ask-about-errors)
|
||||
- [Ask about specific files](../../user/gitlab_duo_chat/examples.md#ask-about-specific-files)
|
||||
- [Ask about specific files](../../user/gitlab_duo_chat/examples.md#ask-about-specific-files-in-the-ide)
|
||||
- [Ask about CI/CD](../../user/gitlab_duo_chat/examples.md#ask-about-cicd)
|
||||
|
||||
### Prerequisites
|
||||
|
|
|
|||
|
|
@ -13,12 +13,15 @@ title: GitLab Git Large File Storage (LFS) Administration
|
|||
|
||||
{{< /details >}}
|
||||
|
||||
This page contains information about configuring Git LFS on GitLab Self-Managed.
|
||||
With Git Large File Storage (LFS), you can store large files in a Git repository without
|
||||
increasing its size or impacting performance. You can enable or disable LFS, configure local
|
||||
or remote storage for LFS objects, and migrate objects between storage types.
|
||||
|
||||
For user documentation about Git LFS, see [Git Large File Storage](../../topics/git/lfs/_index.md).
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Users need to install [Git LFS client](https://git-lfs.com/) version 1.0.1 or later.
|
||||
- Users must install [Git LFS client](https://git-lfs.com/) version 1.0.1 or later.
|
||||
|
||||
## Enable or disable LFS
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ Read more about update policies and warnings in the PostgreSQL
|
|||
|
||||
| First GitLab version | PostgreSQL versions | Default version for fresh installs | Default version for upgrades | Notes |
|
||||
| -------------- | ------------------- | ---------------------------------- | ---------------------------- | ----- |
|
||||
| 17.10.0 | 14.15, 16.6 | 16.6 | 16.6 | |
|
||||
| 17.11.0 | 14.17, 16.8 | 16.8 | 16.8 | Package upgrades automatically perform an upgrade to PostgreSQL 16 for nodes that are not part of a Geo or HA cluster, unless [opted out](https://docs.gitlab.com/omnibus/settings/database/#opt-out-of-automatic-postgresql-upgrades). |
|
||||
| 17.10.0 | 14.17, 16.8 | 16.8 | 16.8 | Fresh installs now default to PostgreSQL 16. |
|
||||
| 17.9.2, 17.8.5, 17.7.7 | 14.17, 16.8 | 14.17 | 16.8 | |
|
||||
| 17.8.0 | 14.15, 16.6 | 14.15 | 16.6 | |
|
||||
| 17.5.0 | 14.11, 16.4 | 14.11 | 16.4 | Single node upgrades from PostgreSQL 14 to PostgreSQL 16 are now supported. Starting with GitLab 17.5.0, PostgreSQL 16 is fully supported for both new installations and upgrades in Geo deployments (the restriction from 17.4.0 no longer applies). |
|
||||
| 17.4.0 | 14.11, 16.4 | 14.11 | 14.11 | PostgreSQL 16 is available for new installations if not using [Geo](../geo/_index.md#requirements-for-running-geo) or [Patroni](../postgresql/_index.md#postgresql-replication-and-failover-for-linux-package-installations). |
|
||||
|
|
|
|||
|
|
@ -8908,6 +8908,7 @@ Input type: `PipelineScheduleUpdateInput`
|
|||
| <a id="mutationpipelinescheduleupdatecrontimezone"></a>`cronTimezone` | [`String`](#string) | Cron time zone supported by ActiveSupport::TimeZone. For example: "Pacific Time (US & Canada)" (default: "UTC"). |
|
||||
| <a id="mutationpipelinescheduleupdatedescription"></a>`description` | [`String`](#string) | Description of the pipeline schedule. |
|
||||
| <a id="mutationpipelinescheduleupdateid"></a>`id` | [`CiPipelineScheduleID!`](#cipipelinescheduleid) | ID of the pipeline schedule to mutate. |
|
||||
| <a id="mutationpipelinescheduleupdateinputs"></a>`inputs` {{< icon name="warning-solid" >}} | [`[CiInputsInputType!]`](#ciinputsinputtype) | **Deprecated:** **Status**: Experiment. Introduced in GitLab 17.11. |
|
||||
| <a id="mutationpipelinescheduleupdateref"></a>`ref` | [`String`](#string) | Ref of the pipeline schedule. |
|
||||
| <a id="mutationpipelinescheduleupdatevariables"></a>`variables` | [`[PipelineScheduleVariableInput!]`](#pipelineschedulevariableinput) | Variables for the pipeline schedule. |
|
||||
|
||||
|
|
@ -45115,6 +45116,12 @@ A `CiPipelineScheduleID` is a global ID. It is encoded as a string.
|
|||
|
||||
An example `CiPipelineScheduleID` is: `"gid://gitlab/Ci::PipelineSchedule/1"`.
|
||||
|
||||
### `CiPipelineScheduleInputID`
|
||||
|
||||
A `CiPipelineScheduleInputID` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `CiPipelineScheduleInputID` is: `"gid://gitlab/Ci::PipelineScheduleInput/1"`.
|
||||
|
||||
### `CiPipelineScheduleVariableID`
|
||||
|
||||
A `CiPipelineScheduleVariableID` is a global ID. It is encoded as a string.
|
||||
|
|
@ -47471,6 +47478,8 @@ Attributes for defining an input.
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="ciinputsinputtypedestroy"></a>`destroy` | [`Boolean`](#boolean) | Set to `true` to delete the input. |
|
||||
| <a id="ciinputsinputtypeid"></a>`id` {{< icon name="warning-solid" >}} | [`CiPipelineScheduleInputID`](#cipipelinescheduleinputid) | **Deprecated:** **Status**: Experiment. Introduced in GitLab 17.11. |
|
||||
| <a id="ciinputsinputtypename"></a>`name` | [`String!`](#string) | Name of the input. |
|
||||
| <a id="ciinputsinputtypevalue"></a>`value` | [`CiInputsValueInputType!`](#ciinputsvalueinputtype) | Value of the input. |
|
||||
|
||||
|
|
|
|||
|
|
@ -238,9 +238,10 @@ Supported attributes:
|
|||
|--------------------------|----------------|----------|-------------|
|
||||
| `id` | integer/string | Yes | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths). |
|
||||
| `merge_request_iid` | integer | Yes | The internal ID of the merge request. |
|
||||
| `auto_merge` | boolean | No | If true, the merge request is added to the merge train when the checks pass. When false or unspecified, the merge request is added directly to the merge train. |
|
||||
| `sha` | string | No | If present, the SHA must match the `HEAD` of the source branch, otherwise the merge fails. |
|
||||
| `squash` | boolean | No | If true, the commits are squashed into a single commit on merge. |
|
||||
| `when_pipeline_succeeds` | boolean | No | If true, the merge request is added to the merge train when the pipeline succeeds. When false or unspecified, the merge request is added directly to the merge train. |
|
||||
| `when_pipeline_succeeds` | boolean | No | [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/521290) in GitLab 17.11. Use `auto_merge` instead. |
|
||||
|
||||
Example request:
|
||||
|
||||
|
|
|
|||
|
|
@ -283,6 +283,8 @@ GET /projects/:id/packages/:package_id/package_files
|
|||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) |
|
||||
| `package_id` | integer | yes | ID of a package. |
|
||||
| `order_by` | string | no | The field to use as order. One of `id` (default), `file_name`, `created_at`. |
|
||||
| `sort` | string | no | The direction of the order, either `asc` (default) for ascending order or `desc` for descending order. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/packages/:package_id/package_files"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ title: Suggest Changes API
|
|||
|
||||
{{< /details >}}
|
||||
|
||||
This page describes the API for [suggesting changes](../user/project/merge_requests/reviews/suggestions.md).
|
||||
You can programmatically create and apply code suggestions in merge request discussions with
|
||||
this API. When you review code, suggestions provide a way to propose specific changes
|
||||
that can be directly applied.
|
||||
|
||||
For more information, see [Suggest changes](../user/project/merge_requests/reviews/suggestions.md).
|
||||
|
||||
Every API call to suggestions must be authenticated.
|
||||
|
||||
|
|
|
|||
|
|
@ -33,14 +33,10 @@ Usage of ActivityPub in GitLab is governed by the
|
|||
The goal of those documents is to provide an implementation path for adding
|
||||
Fediverse capabilities to GitLab.
|
||||
|
||||
This page describes the conceptual and high level point of view, while
|
||||
sub-pages discuss implementation in more technical depth (as in, how to
|
||||
implement this in the actual rails codebase of GitLab).
|
||||
|
||||
This feature requires two feature flags:
|
||||
ActivityPub requires two feature flags:
|
||||
|
||||
- `activity_pub`: Enables or disables all ActivityPub-related features.
|
||||
- `activity_pub_project`: Enables and disable ActivityPub features specific to
|
||||
- `activity_pub_project`: Enables and disables ActivityPub features specific to
|
||||
projects. Requires the `activity_pub` flag to also be enabled.
|
||||
|
||||
Most of the implementation is being discussed in
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ info: Any user with at least the Maintainer role can merge updates to this conte
|
|||
title: Git LFS development guidelines
|
||||
---
|
||||
|
||||
This page contains developer-centric information for GitLab team members. For the
|
||||
user documentation, see [Git Large File Storage](../topics/git/lfs/_index.md).
|
||||
To handle large binary files, Git Large File Storage (LFS) involves several components working together.
|
||||
These guidelines explain the architecture and code flow for working on the GitLab LFS codebase.
|
||||
|
||||
This diagram is a high-level explanation of a Git `push` when Git LFS is in use:
|
||||
For user documentation, see [Git Large File Storage](../topics/git/lfs/_index.md).
|
||||
|
||||
The following is a high-level diagram that explains Git `push` when Git LFS is in use:
|
||||
|
||||
```mermaid
|
||||
%%{init: { "fontFamily": "GitLab Sans" }}%%
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ in a namespace of your choice. You can use forks to propose changes to another p
|
|||
that you don't have access to. For more information,
|
||||
see [Forking workflows](../../user/project/repository/forking_workflow.md).
|
||||
|
||||
This page describes how to update a fork using Git commands from your command line and
|
||||
how to [collaborate across forks](#collaborate-across-forks).
|
||||
|
||||
You can also update a fork with the [GitLab UI](../../user/project/repository/forking_workflow.md#from-the-ui).
|
||||
|
||||
Prerequisites:
|
||||
|
|
|
|||
|
|
@ -6,15 +6,16 @@ description: How to install Git on your local machine.
|
|||
title: Install Git
|
||||
---
|
||||
|
||||
To contribute to GitLab projects, you must download and install the Git client on your local machine.
|
||||
This page explains how to install and configure Git on macOS and Ubuntu Linux.
|
||||
To contribute to GitLab projects, you must download, install, and configure the Git client on
|
||||
your local machine. GitLab uses the SSH protocol to securely communicate with Git.
|
||||
With SSH, you can authenticate to the GitLab remote server without entering your username
|
||||
and password each time.
|
||||
|
||||
For information on downloading and installing Git on other operating systems, see the
|
||||
[official Git website](https://git-scm.com/downloads).
|
||||
|
||||
After you install and configure Git, [generate and add an SSH key pair](../../../user/ssh.md#generate-an-ssh-key-pair)
|
||||
to your GitLab account. GitLab uses the SSH protocol to securely communicate with Git.
|
||||
With SSH, you can authenticate to the GitLab remote server without entering your username and password each time.
|
||||
to your GitLab account.
|
||||
|
||||
## Install and update Git
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ title: Reduce repository size
|
|||
The size of a Git repository can significantly impact performance and storage costs.
|
||||
It can differ slightly from one instance to another due to compression, housekeeping, and other factors.
|
||||
|
||||
This page explains how to remove large files from your Git repository.
|
||||
|
||||
For more information about repository size, see:
|
||||
|
||||
- [Repository size](../../user/project/repository/repository_size.md)
|
||||
|
|
|
|||
|
|
@ -787,6 +787,7 @@ If you notice any inconsistencies in your merge request approval rules, you can
|
|||
|
||||
- Unassign and then reassign the security policy project to the affected group or project.
|
||||
- Alternatively, you can update a policy to trigger that policy to resynchronize for the affected group or project.
|
||||
- Confirm that the syntax of the YAML file in the security policy project is valid.
|
||||
|
||||
These actions help ensure that your merge request approval policies are correctly applied and consistent across all merge requests.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,16 +20,33 @@ This feature is considered [experimental](../../policy/development_stages_suppor
|
|||
{{< /alert >}}
|
||||
|
||||
Workflow is an experimental product and users should consider their
|
||||
circumstances before using this tool. Workflow is an AI Agent that is given
|
||||
some ability to perform actions on the users behalf. AI tools based on LLMs are
|
||||
circumstances before using this tool. It is subject to our [testing agreement](https://handbook.gitlab.com/handbook/legal/testing-agreement/).
|
||||
Workflow is an AI Agent that is given some ability to perform actions on the user's behalf. AI tools based on LLMs are
|
||||
inherently unpredictable and you should take appropriate precautions.
|
||||
|
||||
Workflow in VS Code runs workflows in a Docker container on your local
|
||||
workstation. Running Duo Workflow inside of Docker is not a security measure but a
|
||||
Workflow in VS Code runs workflows on your local workstation or in a Docker container.
|
||||
Running Duo Workflow inside of a Docker container is not a security measure but a
|
||||
convenience to reduce the amount of disruption to your usual development
|
||||
environment. All the documented risks should be considered before using this
|
||||
product. The following risks are important to understand:
|
||||
|
||||
1. Workflow has access to the local file system of the
|
||||
project where you started running Workflow. Workflow respects your local `.gitignore` file,
|
||||
but it can still access files that are not committed to the project and not called out in `.gitignore`.
|
||||
Such files can contain credentials (for example, `.env` files).
|
||||
1. Workflow also gets access to a time-limited `ai_workflows` scoped GitLab
|
||||
OAuth token with your user's identity. This token can be used to access
|
||||
GitLab APIs on your behalf. This token is limited to the duration of
|
||||
the workflow and only has access to certain APIs in GitLab.
|
||||
Without user approval, Workflow will only perform read operations but the token can still,
|
||||
by design, perform write operations on the users behalf. You should consider
|
||||
the access your user has in GitLab before running Workflow.
|
||||
1. You should not give Workflow any additional credentials or secrets, in
|
||||
goals or messages, as there is a chance it might end up using those in code
|
||||
or other API calls.
|
||||
|
||||
Risks specifically when using Docker to isolate Workflow:
|
||||
|
||||
1. Our supported Docker servers are running in a VM. We do not support Docker
|
||||
Engine running on the host as this offers less isolation. Because Docker
|
||||
Engine is the most common way to run Docker on Linux we will likely not
|
||||
|
|
@ -54,23 +71,7 @@ product. The following risks are important to understand:
|
|||
open in VS Code but depending on how your Docker installation works and
|
||||
whether or not you are running other containers there may still be some
|
||||
risks it could access other parts of your file system.
|
||||
1. Workflow has access to the local file system of the
|
||||
project where you started running Workflow. This may include access to
|
||||
any credentials that you have stored in files in this directory, even if they
|
||||
are not committed to the project (for example, `.env` files)
|
||||
1. All your Docker containers usually run in a single VM. So this
|
||||
may mean that Workflow containers are running in the same VM as other
|
||||
non Workflow containers. While the containers are isolated to some
|
||||
degree this isolation is not as strict as VM level isolation
|
||||
|
||||
Other risks to be aware of when using Workflow:
|
||||
|
||||
1. Workflow also gets access to a time-limited `ai_workflows` scoped GitLab
|
||||
OAuth token with your user's identity. This token can be used to access
|
||||
certain GitLab APIs on your behalf. This token is limited to the duration of
|
||||
the workflow and only has access to certain APIs in GitLab but it can still,
|
||||
by design, perform write operations on the users behalf. You should consider
|
||||
what access your user has in GitLab before running workflows.
|
||||
1. You should not give Workflow any additional credentials or secrets, in
|
||||
goals or messages, as there is a chance it might end up using those in code
|
||||
or other API calls.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@ If you encounter issues:
|
|||
1. Ensure that the project you want to use it with meets the [prerequisites](_index.md#prerequisites).
|
||||
1. Ensure that the folder you opened in VS Code has a Git repository for your GitLab project.
|
||||
1. Ensure that you've checked out the branch for the code you'd like to change.
|
||||
1. Ensure that you can connect to the Workflow service:
|
||||
1. In Google Chrome or Firefox, open Developer Tools and the **Network** tab.
|
||||
1. Right-click the column headers to trigger protocol column visibility.
|
||||
1. In the address bar, enter `https://duo-workflow.runway.gitlab.net/DuoWorkflow/ExecuteWorkflow`.
|
||||
1. Ensure the request was successful and the **Protocol** column includes `h2` in Chrome or `HTTP/2` in Firefox.
|
||||
1. If the request fails, your network might be blocking the connection, for example with a firewall. The network must let HTTP/2 traffic through to the service.
|
||||
1. Check local debugging logs:
|
||||
1. For more output in the logs, open the settings:
|
||||
1. On macOS: <kbd>Cmd</kbd> + <kbd>,</kbd>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ In the IDEs, GitLab Duo Chat knows about these areas:
|
|||
| Selected lines in the editor | With the lines selected, ask about `this code` or `this file`. Chat is not aware of the file; you must select the lines you want to ask about. |
|
||||
| Epics | Ask about the URL. |
|
||||
| Issues | Ask about the URL. |
|
||||
| Files | Use the `/include` command to search for project files to add to Duo Chat's context. After you've added the files, you can ask Duo Chat questions about the file contents. Available for VS Code and JetBrains IDEs. For more information, see [Ask about specific files](examples.md#ask-about-specific-files). |
|
||||
| Files | Use the `/include` command to search for project files to add to Duo Chat's context. After you've added the files, you can ask Duo Chat questions about the file contents. Available for VS Code and JetBrains IDEs. For more information, see [Ask about specific files](examples.md#ask-about-specific-files-in-the-ide). |
|
||||
|
||||
In addition, in the IDEs, when you use any of the slash commands,
|
||||
like `/explain`, `/refactor`, `/fix`, or `/tests,` Duo Chat has access to the
|
||||
|
|
@ -117,7 +117,7 @@ This applies to files added via `/include`, and all generation commands.
|
|||
> - Your [**user profile**](../profile/_index.md).
|
||||
> - **Help**.
|
||||
|
||||
1. Enter your question in the chat input box and press **Enter** or select **Send**. It may take a few seconds for the interactive AI chat to produce an answer.
|
||||
1. Enter your question in the chat text box and press **Enter** or select **Send**. It may take a few seconds for the interactive AI chat to produce an answer.
|
||||
1. Optional. Ask a follow-up question.
|
||||
|
||||
To ask a new question unrelated to the previous conversation, you might receive better answers
|
||||
|
|
@ -129,6 +129,63 @@ Only the last 50 messages are retained in the chat history. The chat history exp
|
|||
|
||||
{{< /alert >}}
|
||||
|
||||
### Have multiple conversations with Chat
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Offering: GitLab.com
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/16108) in GitLab 17.10 [with a flag](../../administration/feature_flags.md) named `duo_chat_multi_thread`. Disabled by default.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
{{< alert type="flag" >}}
|
||||
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
In GitLab 17.10 and later, you can have multiple simultaneous conversations with Chat.
|
||||
|
||||
1. In the upper-right corner, select **GitLab Duo Chat**. A drawer opens on the right side of your screen.
|
||||
1. Enter your question in the chat text box and press **Enter** or select **Send**.
|
||||
1. To create a new conversation with Chat, you can either:
|
||||
- In the top-left corner of the Chat drawer, select **New Chat**.
|
||||
- In the text box, type `/new` and press <kbd>Enter</kbd> or select **Send**.
|
||||
A new Chat drawer appears, replacing the previous Chat drawer.
|
||||
|
||||
There is no limit to the number of simultaneous conversations you can have with Chat.
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
When you use multiple conversations, the `/new` slash command replaces the `/reset` or `/clear` slash commands.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
1. To view all of your conversations, in the top-left corner of the Chat drawer, select **Chat History**.
|
||||
|
||||
Conversations created before the multiple conversations feature was enabled are not visible in the Chat history.
|
||||
|
||||
1. To switch between conversations, in your Chat history, select the appropriate conversation.
|
||||
|
||||
Every conversation persists an unlimited number of messages. However, only the last 50 messages are sent to the LLM to fit the content in the LLM's context window.
|
||||
|
||||
#### Delete a conversation
|
||||
|
||||
To delete a conversation:
|
||||
|
||||
1. In the top-left corner of the Chat drawer, select **Chat History**.
|
||||
1. In the Chat history, select **Delete conversation**.
|
||||
|
||||
> Individual conversations are automatically deleted after 30 days of inactivity.
|
||||
>
|
||||
> When a user's permission or role changes in any project or group, all of that user's chat conversations are deleted.
|
||||
|
||||
## Use GitLab Duo Chat in the Web IDE
|
||||
|
||||
{{< history >}}
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ Programming languages that require compiling the source code may throw cryptic e
|
|||
- `Why is "this" undefined in VueJS? Provide common error cases, and explain how to avoid them.`
|
||||
- `How to debug a Ruby on Rails stacktrace? Share common strategies and an example exception.`
|
||||
|
||||
## Ask about specific files
|
||||
## Ask about specific files in the IDE
|
||||
|
||||
{{< details >}}
|
||||
|
||||
|
|
@ -345,6 +345,7 @@ Programming languages that require compiling the source code may throw cryptic e
|
|||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/477258) in GitLab 17.7 [with flags](../../administration/feature_flags.md) named `duo_additional_context` and `duo_include_context_file`. Disabled by default.
|
||||
- [Enabled](https://gitlab.com/groups/gitlab-org/-/epics/15227) for [self-hosted model configuration](../../administration/gitlab_duo_self_hosted/_index.md#self-hosted-ai-gateway-and-llms) as well as the [default GitLab external AI vendor configuration](../../administration/gitlab_duo_self_hosted/_index.md#gitlabcom-ai-gateway-with-default-gitlab-external-vendor-llms) in GitLab 17.9.
|
||||
- [Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated](https://gitlab.com/groups/gitlab-org/-/epics/15183) in GitLab 17.9.
|
||||
- `duo_additional_context` flag [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/508741) in GitLab 17.10.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
|
|
@ -355,8 +356,8 @@ For more information, see the history.
|
|||
|
||||
{{< /alert >}}
|
||||
|
||||
Add repository files to your Duo Chat conversations in VS Code or JetBrains IDEs by
|
||||
typing `/include` and choosing the files.
|
||||
Add repository files to your Duo Chat conversations in VS Code or JetBrains IDEs
|
||||
by typing `/include` and choosing the files.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
|
|
@ -605,19 +606,46 @@ You can ask GitLab Duo Chat to explain a vulnerability when you are viewing a SA
|
|||
|
||||
For more information, see [Explaining a vulnerability](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability).
|
||||
|
||||
## Create a new conversation
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Tier: Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
|
||||
- Offering: GitLab.com
|
||||
- Editors: GitLab UI
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/16108) in GitLab 17.10 [with a flag](../../administration/feature_flags.md) named `duo_chat_multi_thread`. Disabled by default.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
In GitLab 17.10 and later, you can have multiple simultaneous conversations with Chat.
|
||||
|
||||
- In the top-left corner of the Chat drawer, select **New Chat**.
|
||||
- In the text box, type `/new` and press <kbd>Enter</kbd> or select **Send**.
|
||||
|
||||
## Delete or reset the conversation
|
||||
|
||||
To delete all conversations permanently and clear the chat window:
|
||||
For a single conversation with Chat:
|
||||
|
||||
- In the text box, type `/clear` and select **Send**.
|
||||
- To delete all conversations permanently and clear the chat window:
|
||||
|
||||
To start a new conversation, but keep the previous conversations visible in the chat window:
|
||||
- In the text box, type `/clear` and select **Send**.
|
||||
|
||||
- In the text box, type `/reset` and select **Send**.
|
||||
- To start a new conversation, but keep the previous conversations visible in the chat window:
|
||||
|
||||
- In the text box, type `/reset` and select **Send**.
|
||||
|
||||
In both cases, the conversation history will not be considered when you ask new questions.
|
||||
Deleting or resetting might help improve the answers when you switch contexts, because Duo Chat will not get confused by the unrelated conversations.
|
||||
|
||||
When having multiple conversations with Chat:
|
||||
|
||||
- The `/new` slash command replaces the `/clear` and `/reset` slash commands, and opens a new Chat drawer.
|
||||
|
||||
## GitLab Duo Chat slash commands
|
||||
|
||||
Duo Chat has a list of universal, GitLab UI, and IDE commands, each of which is preceded by a slash (`/`).
|
||||
|
|
@ -640,14 +668,18 @@ Use the commands to quickly accomplish specific tasks.
|
|||
|
||||
{{< /history >}}
|
||||
|
||||
These commands work in Duo Chat in all IDEs and in the GitLab UI:
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| /clear | [Delete all conversations permanently and clear the chat window](#delete-or-reset-the-conversation) |
|
||||
| /reset | [Start a new conversation, but keep the previous conversations visible in the chat window](#delete-or-reset-the-conversation) |
|
||||
| /help | Learn more about how Duo Chat works |
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
On GitLab.com, in GitLab 17.10 and later, when having [multiple conversations](_index.md#have-multiple-conversations-with-chat), the `/clear` and `/reset` slash commands are replaced by the [`/new` slash command](#gitlab-ui).
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
### GitLab UI
|
||||
|
||||
{{< details >}}
|
||||
|
|
@ -664,7 +696,8 @@ These commands are dynamic and are available only in the GitLab UI when using Du
|
|||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | ---- |
|
||||
| /summarize_comments | Generate a summary of all comments on the current issue | Issues |
|
||||
| /troubleshoot | [Troubleshoot failed CI/CD jobs with Root Cause Analysis](#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) | Jobs |
|
||||
| /vulnerability_explain | [Explain current vulnerability](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability) | Vulnerabilities |
|
||||
| /vulnerability_explain | [Explain current vulnerability](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability) | Vulnerabilities |
|
||||
| /new | [Create a new Chat conversation](_index.md#have-multiple-conversations-with-chat). GitLab 17.10 and later. | All |
|
||||
|
||||
### IDE
|
||||
|
||||
|
|
@ -690,4 +723,4 @@ These commands work only when using Duo Chat in supported IDEs:
|
|||
| /explain | [Explain code](#explain-selected-code) |
|
||||
| /refactor | [Refactor the code](#refactor-code-in-the-ide) |
|
||||
| /fix | [Fix the code](#fix-code-in-the-ide) |
|
||||
| /include | [Include file context](#ask-about-specific-files) |
|
||||
| /include | [Include file context](#ask-about-specific-files-in-the-ide) |
|
||||
|
|
|
|||
|
|
@ -193,11 +193,12 @@ You can also use [custom CI/CD variables](../../../ci/variables/_index.md#for-a-
|
|||
{{< history >}}
|
||||
|
||||
- Support for Docker Hub credentials [added](https://gitlab.com/gitlab-org/gitlab/-/issues/331741) in GitLab 17.10.
|
||||
|
||||
- UI support [added](https://gitlab.com/gitlab-org/gitlab/-/issues/521954) in GitLab 17.11.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
By default, the Dependency Proxy does not use credentials when pulling images from Docker Hub.
|
||||
You can configure Docker Hub authentication through the GraphQL API using your Docker Hub credentials or tokens.
|
||||
You can configure Docker Hub authentication using your Docker Hub credentials or tokens.
|
||||
|
||||
To authenticate with Docker Hub, you can use:
|
||||
|
||||
|
|
@ -206,7 +207,19 @@ To authenticate with Docker Hub, you can use:
|
|||
- A Docker Hub [Personal Access Token](https://docs.docker.com/security/for-developers/access-tokens/).
|
||||
- A Docker Hub [Organization Access Token](https://docs.docker.com/security/for-admins/access-tokens/).
|
||||
|
||||
UI support for configuring Docker Hub credentials for groups in self-managed instances is proposed in issue [521954](https://gitlab.com/gitlab-org/gitlab/-/issues/521954).
|
||||
#### Configure credentials
|
||||
|
||||
To set Docker Hub credentials for the dependency proxy for a group:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. Select **Settings > Packages and registries**.
|
||||
1. Expand the **Dependency Proxy** section.
|
||||
1. Turn on **Enable Proxy**.
|
||||
1. Under **Docker Hub authentication**, enter your credentials:
|
||||
- **Identity** is your username (for password or Personal Access Token) or organization name (for Organization Access Token).
|
||||
- **Secret** is your password, Personal Access Token, or Organization Access Token.
|
||||
|
||||
You must either complete both fields or leave both empty. If you leave both fields empty, requests to Docker Hub remain unauthenticated.
|
||||
|
||||
#### Configure credentials using the GraphQL API
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ description: Use Code Owners to define experts for your code base, and set revie
|
|||
title: Advanced `CODEOWNERS` configuration
|
||||
---
|
||||
|
||||
This page describes advanced configuration options for Code Owners in GitLab.
|
||||
The `CODEOWNERS` file helps you define who is responsible for specific files and directories.
|
||||
You can use pattern matching, sections, and inheritance rules to assign reviewers to merge requests
|
||||
and require their approval before merging.
|
||||
|
||||
## Pattern matching
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,6 @@ title: Repository size
|
|||
The size of a Git repository can significantly impact performance and storage costs.
|
||||
It can differ slightly from one instance to another due to compression, housekeeping, and other factors.
|
||||
|
||||
This page explains:
|
||||
|
||||
- [How repository size is calculated](#size-calculation).
|
||||
- [Size and storage limits](#size-and-storage-limits).
|
||||
- [Methods to reduce repository size](#methods-to-reduce-repository-size).
|
||||
|
||||
## Size calculation
|
||||
|
||||
The **Project overview** page shows the size of all files in the repository, including repository files,
|
||||
|
|
|
|||
|
|
@ -16,11 +16,7 @@ See [Getting started](doc/getting_started.md).
|
|||
|
||||
## Usage
|
||||
|
||||
Follow the guides to set up a new collection:
|
||||
|
||||
1. Use an existing queue or [set up a new queue](doc/usage.md#registering-a-queue)
|
||||
1. Use an existing reference class or [set up a new reference class](doc/usage.md#adding-a-new-reference-type)
|
||||
1. [Add a collection class](doc/usage.md#adding-a-new-collection)
|
||||
See [Usage](doc/usage.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
Add an initializer with the following options:
|
||||
|
||||
1. `enabled`: `true|false`. Defaults to `false`
|
||||
1. `databases`: Hash containing database configuration options
|
||||
1. `indexing_enabled`: `true|false`. Defaults to `false`
|
||||
1. `re_enqueue_indexing_workers`: `true|false`. Defaults to `false`
|
||||
1. `logger`: Logger. Defaults to `Logger.new($stdout)`
|
||||
|
|
@ -15,25 +14,49 @@ For example:
|
|||
```ruby
|
||||
ActiveContext.configure do |config|
|
||||
config.enabled = true
|
||||
config.indexing_enabled = true
|
||||
config.logger = ::Gitlab::Elasticsearch::Logger.build
|
||||
|
||||
config.databases = {
|
||||
es1: {
|
||||
adapter: 'ActiveContext::Databases::Elasticsearch::Adapter',
|
||||
prefix: 'gitlab_active_context',
|
||||
options: ::Gitlab::CurrentSettings.elasticsearch_config
|
||||
}
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
### Elasticsearch/OpenSearch Configuration Options
|
||||
## Create a connection
|
||||
|
||||
| Option | Description | Required | Default | Example |
|
||||
|--------|-------------|----------|---------|---------|
|
||||
| `url` | The URL of the Elasticsearch server | Yes | N/A | `'http://localhost:9200'` |
|
||||
| `prefix` | The prefix for Elasticsearch indices | No | `'gitlab_active_context'` | `'my_custom_prefix'` |
|
||||
| `client_request_timeout` | The timeout for client requests in seconds | No | N/A | `60` |
|
||||
| `retry_on_failure` | The number of times to retry a failed request | No | `0` (no retries) | `3` |
|
||||
| `debug` | Enable or disable debug logging | No | `false` | `true` |
|
||||
| `max_bulk_size_bytes` | Maximum size before forcing a bulk operation in megabytes | No | `10.megabytes` | `5242880` |
|
||||
Create a `Ai::ActiveContext::Connection` record in the database with the following fields:
|
||||
|
||||
- `name`: Useful name
|
||||
- `adapter_class`: One of
|
||||
- `ActiveContext::Databases::Elasticsearch::Adapter`
|
||||
- `ActiveContext::Databases::Opensearch::Adapter`
|
||||
- `ActiveContext::Databases::Postgres::Adapter`
|
||||
- `options`: Connection options
|
||||
- For Elasticsearch: `url`, `client_request_timeout`, `retry_on_failure`, `log`, `debug`
|
||||
- For OpenSearch: `url`, `aws`, `aws_region`, `aws_access_key`, `aws_secret_access_key`, `client_request_timeout`, `retry_on_failure`, `log`, `debug`
|
||||
- For Postgres: `port`, `host`, `username`, `password`
|
||||
|
||||
### Use Elasticsearch settings from Advanced Search
|
||||
|
||||
```ruby
|
||||
Ai::ActiveContext::Connection.create!(
|
||||
name: "elastic",
|
||||
adapter_class: "ActiveContext::Databases::Elasticsearch::Adapter",
|
||||
options: ::Gitlab::CurrentSettings.elasticsearch_config
|
||||
)
|
||||
```
|
||||
|
||||
### Use OpenSearch settings from Advanced Search
|
||||
|
||||
```ruby
|
||||
Ai::ActiveContext::Connection.create!(
|
||||
name: "opensearch",
|
||||
adapter_class: "ActiveContext::Databases::Opensearch::Adapter",
|
||||
options: ::Gitlab::CurrentSettings.elasticsearch_config
|
||||
)
|
||||
```
|
||||
|
||||
## Activate a connection
|
||||
|
||||
To make a connection active and deactivate the existing active connection if it is set:
|
||||
|
||||
```ruby
|
||||
connection.activate!
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
# How it works
|
||||
|
||||
## Migrations
|
||||
|
||||
A cron worker runs every 5 minutes to apply outstanding migrations for the currently active connection.
|
||||
|
||||
If another connection is made active, the worker will apply that connection's outstanding migrations.
|
||||
|
||||
## Async processing
|
||||
|
||||
A cron worker triggers a Sidekiq job for every queue in `ActiveContext.raw_queues` every minute. For each of the jobs, it fetches a set amount of references from the queue, processes them and removes them from the queue. The job will re-enqueue itself every second until there are no more references to process in the queue.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,43 @@
|
|||
# Usage
|
||||
|
||||
## Creating a migration
|
||||
|
||||
Migrations are similiar to database migrations: they create collections, update schemas, run backfills, etc.
|
||||
|
||||
### Migration to create a collection
|
||||
|
||||
Create a file in `ActiveContext::Config.migrations_path`, e.g. `ee/db/active_context/migrate/20250311135734_create_merge_requests.rb`:
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateMergeRequests < ActiveContext::Migration[1.0]
|
||||
milestone '17.9'
|
||||
|
||||
def migrate!
|
||||
create_collection :merge_requests, number_of_partitions: 3 do |c|
|
||||
c.bigint :issue_id, index: true
|
||||
c.bigint :namespace_id, index: true
|
||||
c.prefix :traversal_ids
|
||||
c.vector :embeddings, dimensions: 768
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
A migration worker will apply migrations for the active connection. See [Migrations](how_it_works.md#migrations).
|
||||
|
||||
If you want to run the worker manually, execute:
|
||||
|
||||
```ruby
|
||||
Ai::ActiveContext::MigrationWorker.new.perform
|
||||
```
|
||||
|
||||
## Registering a queue
|
||||
|
||||
Queues keep track of items needing to be processed in bulk asynchronously. A queue definition has a unique key which registers queues based on the number of shards defined. Each shard creates a queue.
|
||||
|
||||
To create a new queue: add a file, extend `ActiveContext::Concerns::Queue`, define `number_of_shards` and call `register!`:
|
||||
To create a new queue: add a file, extend `ActiveContext::Concerns::Queue` and define `number_of_shards`:
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
|
@ -47,19 +80,16 @@ Create a class under `lib/active_context/references/` and inherit from the `Refe
|
|||
|
||||
Class methods required:
|
||||
|
||||
- `serialize(object, routing)`: defines a string representation of the reference object
|
||||
- `preload_refs` (optional): preload database records to prevent N+1 issues
|
||||
- `serialize_data`: defines a string representation of the reference object
|
||||
|
||||
Instance methods required:
|
||||
|
||||
- `serialize`: defines a string representation of the reference object
|
||||
- `init`: reads from `serialized_args`
|
||||
- `as_indexed_json`: a hash containing the data representation of the object
|
||||
- `operation`: determines the operation which can be one of `index`, `upsert` or `delete`
|
||||
- `partition_name`: name of the table or index
|
||||
- `identifier`: unique identifier
|
||||
- `routing` (optional)
|
||||
|
||||
Example:
|
||||
Example for a reference reading from a database relation, with preloading and bulk embedding generation:
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
|
@ -68,32 +98,107 @@ module Ai
|
|||
module Context
|
||||
module References
|
||||
class MergeRequest < ::ActiveContext::Reference
|
||||
def self.serialize(record)
|
||||
new(record.id).serialize
|
||||
include ::ActiveContext::Preprocessors::Embeddings
|
||||
include ::ActiveContext::Preprocessors::Preload
|
||||
|
||||
add_preprocessor :preload do |refs|
|
||||
preload(refs)
|
||||
end
|
||||
|
||||
attr_reader :identifier
|
||||
|
||||
def initialize(identifier)
|
||||
@identifier = identifier.to_i
|
||||
add_preprocessor :embeddings do |refs|
|
||||
bulk_embeddings(refs)
|
||||
end
|
||||
|
||||
def serialize
|
||||
self.class.join_delimited([identifier].compact)
|
||||
def self.embedding_content(ref)
|
||||
"title #{ref.database_record.title}\ndescription #{ref.database_record.description}"
|
||||
end
|
||||
|
||||
def self.model_klass
|
||||
::MergeRequest
|
||||
end
|
||||
|
||||
def self.serialize_data(merge_request)
|
||||
{ identifier: merge_request.id }
|
||||
end
|
||||
|
||||
attr_accessor :identifier, :embedding
|
||||
attr_writer :database_record
|
||||
|
||||
def init
|
||||
@identifier, _ = serialized_args
|
||||
end
|
||||
|
||||
def serialized_attributes
|
||||
[identifier]
|
||||
end
|
||||
|
||||
def as_indexed_json
|
||||
{
|
||||
id: identifier
|
||||
id: identifier,
|
||||
issue_id: identifier,
|
||||
namespace_id: database_record.project.id,
|
||||
traversal_ids: database_record.project.elastic_namespace_ancestry,
|
||||
embeddings: embedding
|
||||
}
|
||||
end
|
||||
|
||||
def operation
|
||||
:index
|
||||
def model_klass
|
||||
self.class.model_klass
|
||||
end
|
||||
|
||||
def partition_name
|
||||
'ai_context_merge_requests'
|
||||
def database_record
|
||||
@database_record ||= model_klass.find_by_id(identifier)
|
||||
end
|
||||
|
||||
def operation
|
||||
database_record ? :upsert : :delete
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Example for code embeddings:
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Ai
|
||||
module Context
|
||||
module References
|
||||
class CodeEmbeddings < ::ActiveContext::Reference
|
||||
include ::ActiveContext::Preprocessors::Embeddings
|
||||
|
||||
add_preprocessor :bulk_embeddings do |refs|
|
||||
bulk_embeddings(refs)
|
||||
end
|
||||
|
||||
def self.embedding_content(ref)
|
||||
ref.blob.data
|
||||
end
|
||||
|
||||
attr_accessor :project_id, :identifier, :repository, :blob, :embedding
|
||||
|
||||
def init
|
||||
@project_id, @identifier = serialized_args
|
||||
@repository = Project.find(project_id).repository
|
||||
@blob = Gitlab::Git::Blob.raw(repository, identifier)
|
||||
end
|
||||
|
||||
def serialized_attributes
|
||||
[project_id, identifier]
|
||||
end
|
||||
|
||||
def operation
|
||||
blob.data ? :upsert : :delete
|
||||
end
|
||||
|
||||
def as_indexed_json
|
||||
{
|
||||
project_id: project_id,
|
||||
embeddings: embedding
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -116,21 +221,24 @@ To add a new collection:
|
|||
Example:
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Ai
|
||||
module Context
|
||||
module Collections
|
||||
class MergeRequest
|
||||
include ActiveContext::Concerns::Collection
|
||||
|
||||
def self.collection_name
|
||||
'gitlab_active_context_merge_requests'
|
||||
end
|
||||
|
||||
def self.queue
|
||||
Queues::MergeRequest
|
||||
end
|
||||
|
||||
def self.reference_klasses
|
||||
[
|
||||
References::Embedding,
|
||||
References::MergeRequest
|
||||
]
|
||||
def self.reference_klass
|
||||
References::MergeRequest
|
||||
end
|
||||
|
||||
def self.routing(object)
|
||||
|
|
@ -144,6 +252,8 @@ end
|
|||
|
||||
Adding references to the queue can be done a few ways:
|
||||
|
||||
The prefered method:
|
||||
|
||||
```ruby
|
||||
Ai::Context::Collections::MergeRequest.track!(MergeRequest.first)
|
||||
```
|
||||
|
|
@ -152,16 +262,35 @@ Ai::Context::Collections::MergeRequest.track!(MergeRequest.first)
|
|||
Ai::Context::Collections::MergeRequest.track!(MergeRequest.take(10))
|
||||
```
|
||||
|
||||
Passing a collection:
|
||||
|
||||
```ruby
|
||||
ActiveContext.track!(MergeRequest.first, collection: Ai::Context::Collections::MergeRequest)
|
||||
```
|
||||
|
||||
Passing a collection and queue:
|
||||
|
||||
```ruby
|
||||
ActiveContext.track!(MergeRequest.first, collection: Ai::Context::Collections::MergeRequest, queue: Ai::Context::Queues::Default)
|
||||
```
|
||||
|
||||
Building a reference:
|
||||
|
||||
```ruby
|
||||
ActiveContext.track!(Ai::Context::References::MergeRequest.new(1), queue: Ai::Context::Queues::MergeRequest)
|
||||
ref = Ai::Context::References::CodeEmbeddings.new(collection_id: collection.id, routing: project.root_ancestor.id, project_id: project.id, identifier: blob.id)
|
||||
Ai::Context::Collections::CodeEmbeddings.track!(ref)
|
||||
```
|
||||
|
||||
```ruby
|
||||
ref = Ai::Context::References::CodeEmbeddings.new(collection_id: 24, routing: 24, project_id: 1, identifier: "9ab45314044d664a3b8ac1e05777411482bd0564")
|
||||
Ai::Context::Collections::CodeEmbeddings.track!(ref)
|
||||
```
|
||||
|
||||
Building a reference and passing a queue:
|
||||
|
||||
```ruby
|
||||
ref = Ai::Context::References::MergeRequest.new(collection_id: collection.id, routing: project.root_ancestor.id, identifier: 1)
|
||||
ActiveContext.track!(ref, queue: Ai::Context::Queues::MergeRequest)
|
||||
```
|
||||
|
||||
To view all tracked references:
|
||||
|
|
@ -169,3 +298,41 @@ To view all tracked references:
|
|||
```ruby
|
||||
ActiveContext::Queues.all_queued_items
|
||||
```
|
||||
|
||||
Once references are tracked, they will be executed asyncronously. See [Async Processing](how_it_works.md#async-processing).
|
||||
|
||||
To execute all refs from all refs sync, run
|
||||
|
||||
```ruby
|
||||
ActiveContext.execute_all_queues!
|
||||
```
|
||||
|
||||
To clear a queue:
|
||||
|
||||
```ruby
|
||||
Ai::Context::Queues::MergeRequest.clear_tracking!
|
||||
```
|
||||
|
||||
## Performing a search
|
||||
|
||||
### Example: Find all documents in a project
|
||||
|
||||
```ruby
|
||||
query = ActiveContext::Query.filter(project_id: 1).limit(1)
|
||||
|
||||
results = ActiveContext.adapter.search(collection: "gitlab_active_context_code_embeddings", query: query)
|
||||
|
||||
results.to_a
|
||||
```
|
||||
|
||||
### Example: Find document closest to a given embedding
|
||||
|
||||
```ruby
|
||||
target_embedding = ::ActiveContext::Embeddings.generate_embeddings("some text")
|
||||
|
||||
query = ActiveContext::Query.filter(project_id: 1).knn(target: "embeddings", vector: target_embedding, limit: 1)
|
||||
|
||||
result = ActiveContext.adapter.search(collection: "gitlab_active_context_code_embeddings", query: query)
|
||||
|
||||
result.to_a
|
||||
```
|
||||
|
|
|
|||
|
|
@ -29,4 +29,8 @@ module ActiveContext
|
|||
def self.track!(*objects, collection: nil, queue: nil)
|
||||
ActiveContext::Tracker.track!(*objects, collection: collection, queue: queue)
|
||||
end
|
||||
|
||||
def self.execute_all_queues!
|
||||
raw_queues.each { |q| BulkProcessQueue.process!(q.class, q.shard) }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ module ActiveContext
|
|||
collection_id = self.class.collection_record.id
|
||||
|
||||
reference_klasses.map do |reference_klass|
|
||||
reference_klass.serialize(collection_id, routing, object)
|
||||
reference_klass.serialize(collection_id: collection_id, routing: routing, data: object)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActiveContext
|
||||
module Concerns
|
||||
module MigrationWorker
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
RE_ENQUEUE_DELAY = 30.seconds
|
||||
LOCK_TIMEOUT = 30.minutes
|
||||
LOCK_SLEEP_SEC = 2
|
||||
LOCK_RETRIES = 10
|
||||
|
||||
def perform
|
||||
return false unless preflight_checks
|
||||
|
||||
if failed_migrations?
|
||||
log 'Found failed migrations. All future migrations will be halted. Exiting'
|
||||
return
|
||||
end
|
||||
|
||||
preprocess_migration_records!
|
||||
|
||||
in_lock(self.class.name.underscore, ttl: LOCK_TIMEOUT, retries: LOCK_RETRIES, sleep_sec: LOCK_SLEEP_SEC) do
|
||||
execute_current_migration
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preflight_checks
|
||||
unless ActiveContext::Config.indexing_enabled?
|
||||
log 'indexing disabled. Execution is skipped.'
|
||||
return false
|
||||
end
|
||||
|
||||
unless adapter
|
||||
log 'adapter not configured. Execution is skipped.'
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def failed_migrations?
|
||||
migrations.failed.any?
|
||||
end
|
||||
|
||||
def preprocess_migration_records!
|
||||
migration_files = migration_dictionary_instance.migrations(versions_only: true)
|
||||
migration_records = migrations.pluck(:version)
|
||||
|
||||
create_missing_migration_records!(migration_files - migration_records)
|
||||
delete_orphaned_migration_records!(migration_records - migration_files)
|
||||
end
|
||||
|
||||
def execute_current_migration
|
||||
migration_record = migrations.current
|
||||
|
||||
unless migration_record
|
||||
log 'No pending migrations to process'
|
||||
return true
|
||||
end
|
||||
|
||||
process_migration!(migration_record)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def process_migration!(migration_record)
|
||||
migration_class = find_migration_class(migration_record.version)
|
||||
|
||||
migration_instance = migration_class.new
|
||||
|
||||
log "Starting migration #{migration_record.version}"
|
||||
|
||||
migration_record.mark_as_started!
|
||||
migration_instance.migrate!
|
||||
|
||||
if migration_instance.all_operations_completed?
|
||||
log "Marking migration #{migration_record.version} as completed"
|
||||
|
||||
migration_record.mark_as_completed!
|
||||
else
|
||||
log "Migration #{migration_record.version} partially completed, re-enqueueing worker"
|
||||
|
||||
re_enqueue_worker
|
||||
end
|
||||
rescue StandardError => e
|
||||
migration_record.decrease_retries!(e)
|
||||
|
||||
log "Migration #{migration_record.version} failed: #{e.message}. Retries left: #{migration_record.retries_left}"
|
||||
end
|
||||
|
||||
def create_missing_migration_records!(versions)
|
||||
return unless versions.any?
|
||||
|
||||
connection = adapter.connection
|
||||
|
||||
versions.each do |version|
|
||||
Ai::ActiveContext::Migration.create!(connection: connection, version: version)
|
||||
end
|
||||
|
||||
log "Created missing migration records for #{versions.join(', ')}"
|
||||
end
|
||||
|
||||
def delete_orphaned_migration_records!(versions)
|
||||
return unless versions.any?
|
||||
|
||||
migrations.where(version: versions).delete_all
|
||||
|
||||
log "Deleted orphaned migration records for #{versions.join(', ')}"
|
||||
end
|
||||
|
||||
def re_enqueue_worker
|
||||
self.class.perform_in(RE_ENQUEUE_DELAY)
|
||||
end
|
||||
|
||||
def migrations
|
||||
adapter.connection.migrations
|
||||
end
|
||||
|
||||
def find_migration_class(version)
|
||||
migration_dictionary_instance.find_by_version(version)
|
||||
end
|
||||
|
||||
def migration_dictionary_instance
|
||||
@migration_dictionary_instance ||= ::ActiveContext::Migration::Dictionary.instance
|
||||
end
|
||||
|
||||
def log(message)
|
||||
ActiveContext::Config.logger.info(structured_payload(message: "#{self.class}: #{message}"))
|
||||
end
|
||||
|
||||
def adapter
|
||||
@adapter ||= ActiveContext.adapter
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7,6 +7,10 @@ module ActiveContext
|
|||
string.split(self::DELIMITER)
|
||||
end
|
||||
|
||||
def args_to_hash(args)
|
||||
args.each_with_index.to_h { |arg, index| [:"arg#{index + 1}", arg] }
|
||||
end
|
||||
|
||||
def join_delimited(array)
|
||||
[self, array].join(self::DELIMITER)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -74,11 +74,11 @@ module ActiveContext
|
|||
case ref.operation.to_sym
|
||||
when :index, :upsert
|
||||
[
|
||||
{ update: { _index: ref.partition_name, _id: ref.identifier, routing: ref.routing }.compact },
|
||||
{ update: { _index: ref.partition, _id: ref.identifier, routing: ref.routing }.compact },
|
||||
{ doc: ref.as_indexed_json, doc_as_upsert: true }
|
||||
]
|
||||
when :delete
|
||||
[{ delete: { _index: ref.partition_name, _id: ref.identifier, routing: ref.routing }.compact }]
|
||||
[{ delete: { _index: ref.partition, _id: ref.identifier, routing: ref.routing }.compact }]
|
||||
else
|
||||
raise StandardError, "Operation #{ref.operation} is not supported"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ module ActiveContext
|
|||
|
||||
operation.completed?
|
||||
end
|
||||
|
||||
def all_operations_completed?
|
||||
@operations.values.all?(&:completed?)
|
||||
end
|
||||
end
|
||||
|
||||
def self.[](version)
|
||||
|
|
|
|||
|
|
@ -25,8 +25,10 @@ module ActiveContext
|
|||
end
|
||||
|
||||
# Returns all migrations sorted by version
|
||||
def migrations
|
||||
@migrations.sort_by { |version, _| version }.map(&:last)
|
||||
def migrations(versions_only: false)
|
||||
migrations = @migrations.sort_by { |version, _| version }
|
||||
|
||||
migrations.map { |m| versions_only ? m.first : m.last }
|
||||
end
|
||||
|
||||
# Find a specific migration by version
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@ module ActiveContext
|
|||
end
|
||||
|
||||
def instantiate(string)
|
||||
new(*deserialize_string(string))
|
||||
collection_id, routing, *args = deserialize_string(string)
|
||||
new(collection_id: collection_id, routing: routing, args: args)
|
||||
end
|
||||
|
||||
def serialize(collection_id, routing, data)
|
||||
new(collection_id, routing, *serialize_data(data)).serialize
|
||||
def serialize(collection_id:, routing:, data:)
|
||||
args = serialize_data(data)
|
||||
new(collection_id: collection_id, routing: routing, args: args.values).serialize
|
||||
end
|
||||
|
||||
def serialize_data
|
||||
|
|
@ -35,11 +37,11 @@ module ActiveContext
|
|||
|
||||
attr_reader :collection_id, :collection, :routing, :serialized_args
|
||||
|
||||
def initialize(collection_id, routing, *serialized_args)
|
||||
def initialize(collection_id:, routing:, args: [])
|
||||
@collection_id = collection_id.to_i
|
||||
@collection = ActiveContext::CollectionCache.fetch(@collection_id)
|
||||
@routing = routing
|
||||
@serialized_args = serialized_args
|
||||
@serialized_args = Array(args)
|
||||
init
|
||||
end
|
||||
|
||||
|
|
@ -48,14 +50,14 @@ module ActiveContext
|
|||
end
|
||||
|
||||
def serialize
|
||||
self.class.join_delimited([collection_id, routing, serialize_arguments].flatten.compact)
|
||||
self.class.join_delimited([collection_id, routing, *serialized_attributes].compact)
|
||||
end
|
||||
|
||||
def init
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def serialize_arguments
|
||||
def serialized_attributes
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
|
|
@ -78,5 +80,9 @@ module ActiveContext
|
|||
def partition_number
|
||||
collection.partition_for(routing)
|
||||
end
|
||||
|
||||
def partition
|
||||
"#{partition_name}#{ActiveContext.adapter.separator}#{partition_number}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ RSpec.describe ActiveContext::BulkProcessor do
|
|||
id: 1,
|
||||
as_indexed_json: { title: 'Test Issue' },
|
||||
partition_name: 'issues',
|
||||
partition: 'issues_0',
|
||||
identifier: '1',
|
||||
routing: 'group_1'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ RSpec.describe ActiveContext::Databases::Elasticsearch::Indexer do
|
|||
as_indexed_json: { title: 'Test Issue' },
|
||||
partition_name: 'issues',
|
||||
identifier: '1',
|
||||
partition: 'issues_0',
|
||||
routing: 'group_1',
|
||||
serialize: 'issue 1 group_1'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ RSpec.describe ActiveContext::Databases::Opensearch::Indexer do
|
|||
as_indexed_json: { title: 'Test Issue' },
|
||||
partition_name: 'issues',
|
||||
identifier: '1',
|
||||
partition: 'issues_0',
|
||||
routing: 'group_1',
|
||||
serialize: 'issue 1 group_1'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ RSpec.describe ActiveContext::Preprocessors::Embeddings do
|
|||
end
|
||||
end
|
||||
|
||||
let(:reference_1) { reference_class.new(collection_id, partition, object_id) }
|
||||
let(:reference_2) { reference_class.new(collection_id, partition, object_id) }
|
||||
let(:reference_1) { reference_class.new(collection_id: collection_id, routing: partition, args: object_id) }
|
||||
let(:reference_2) { reference_class.new(collection_id: collection_id, routing: partition, args: object_id) }
|
||||
|
||||
let(:mock_adapter) { double }
|
||||
let(:mock_collection) { double(name: collection_name, partition_for: partition) }
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ RSpec.describe ActiveContext::Preprocessors::Preload do
|
|||
end
|
||||
end
|
||||
|
||||
let(:reference_1) { reference_class.new(collection_id, partition, object_id) }
|
||||
let(:reference_2) { reference_class.new(collection_id, partition, object_id) }
|
||||
let(:reference_1) { reference_class.new(collection_id: collection_id, routing: partition, args: object_id) }
|
||||
let(:reference_2) { reference_class.new(collection_id: collection_id, routing: partition, args: object_id) }
|
||||
|
||||
let(:mock_adapter) { double }
|
||||
let(:mock_collection) { double(name: collection_name, partition_for: partition) }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ module Test
|
|||
module References
|
||||
class Mock < ::ActiveContext::Reference
|
||||
def self.serialize_data(data)
|
||||
[data.id]
|
||||
{ identifier: data.id }
|
||||
end
|
||||
|
||||
attr_reader :identifier
|
||||
|
|
@ -13,7 +13,7 @@ module Test
|
|||
@identifier, _ = serialized_args
|
||||
end
|
||||
|
||||
def serialize_arguments
|
||||
def serialized_attributes
|
||||
[identifier]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,16 @@ module API
|
|||
end
|
||||
params do
|
||||
use :pagination
|
||||
optional :order_by,
|
||||
type: String,
|
||||
values: %w[id created_at file_name],
|
||||
default: 'id',
|
||||
desc: 'Return package files ordered by `id`, `created_at` or `file_name`'
|
||||
optional :sort,
|
||||
type: String,
|
||||
values: %w[asc desc],
|
||||
default: 'asc',
|
||||
desc: 'Return package files sorted in `asc` or `desc` order.'
|
||||
end
|
||||
route_setting :authentication, job_token_allowed: true
|
||||
route_setting :authorization, job_token_policies: :read_packages,
|
||||
|
|
@ -38,7 +48,8 @@ module API
|
|||
.new(user_project, params[:package_id]).execute
|
||||
|
||||
package_files = package.installable_package_files
|
||||
.preload_pipelines.order_id_asc
|
||||
.preload_pipelines
|
||||
.order_by(params[:order_by], params[:sort])
|
||||
|
||||
present paginate(package_files), with: ::API::Entities::PackageFile
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8971,9 +8971,6 @@ msgstr ""
|
|||
msgid "Availability"
|
||||
msgstr ""
|
||||
|
||||
msgid "Available"
|
||||
msgstr ""
|
||||
|
||||
msgid "Available ID"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23232,9 +23229,6 @@ msgstr ""
|
|||
msgid "Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|An error occurred while fetching the environments."
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|An error occurred while making the request."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23250,9 +23244,6 @@ msgstr ""
|
|||
msgid "Environments|Are you sure you want to delete %{podName}? This action cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Auto stop"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Auto stops %{autoStopAt}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23262,9 +23253,6 @@ msgstr ""
|
|||
msgid "Environments|Clean up environments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Commit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Create an environment"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23289,12 +23277,6 @@ msgstr ""
|
|||
msgid "Environments|Deploy to..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Deployment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Deployment %{status}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Deployment history"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23310,9 +23292,6 @@ msgstr ""
|
|||
msgid "Environments|Enable review apps"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Environments"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23334,9 +23313,6 @@ msgstr ""
|
|||
msgid "Environments|If a Flux resource is specified, its reconciliation status is reflected in GitLab."
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Job"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Kubernetes namespace (optional)"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23355,9 +23331,6 @@ msgstr ""
|
|||
msgid "Environments|New environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|No deployments yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|No selection shows all authorized resources in the cluster. %{linkStart}Learn more.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23424,9 +23397,6 @@ msgstr ""
|
|||
msgid "Environments|Select which environments to clean up. Protected environments are excluded. Learn more about cleaning up environments."
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Stop"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23463,15 +23433,6 @@ msgstr ""
|
|||
msgid "Environments|Unable to access the following resources from this environment. Check your authorization on the following and try again:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Upcoming"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Upcoming deployment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Updating"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23487,12 +23448,6 @@ msgstr ""
|
|||
msgid "Environments|You are about to stop the environment %{environmentName}. The environment will be moved to the Stopped tab. There is no %{actionStopLinkStart}action:stop%{actionStopLinkEnd} defined for this environment, so your existing deployments will not be affected."
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|by %{avatar}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|protected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environment|Age"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63009,9 +62964,18 @@ msgstr ""
|
|||
msgid "UsageQuotas|Container registry storage statistics are not used to calculate the total project storage. Total project storage is calculated after namespace container deduplication, where the total of all unique containers is added to the namespace storage total."
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|Loading Usage Quotas tab content"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|Namespace transfer data used"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|Reload the page to try again"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuotas|There was an error while loading the tab contents"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuota|%{linkTitle} help link"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -160,11 +160,11 @@ module QA
|
|||
remove_source_branch: true,
|
||||
squash: true,
|
||||
reviewer_ids: approver_user_valid? ? [approver_user_id] : nil,
|
||||
labels: "group::development analytics,type::maintenance,maintenance::pipelines",
|
||||
labels: "group::development analytics,type::maintenance,maintenance::pipelines,automation:bot-authored",
|
||||
description: "Update fallback knapsack report and example runtime data report.".then do |description|
|
||||
next description if approver_user_valid?
|
||||
|
||||
"#{description}\n\ncc: @gl-dx/qe-maintainers"
|
||||
"#{description}\n\ncc: @gl-dx/maintainers"
|
||||
end
|
||||
}.compact)
|
||||
@mr_iid = resp[:iid]
|
||||
|
|
|
|||
|
|
@ -77,11 +77,11 @@ RSpec.describe QA::Tools::KnapsackReportUpdater, :aggregate_failures do
|
|||
remove_source_branch: true,
|
||||
squash: true,
|
||||
reviewer_ids: reviewer_ids,
|
||||
labels: "group::development analytics,type::maintenance,maintenance::pipelines",
|
||||
labels: "group::development analytics,type::maintenance,maintenance::pipelines,automation:bot-authored",
|
||||
description: "Update fallback knapsack report and example runtime data report.".then do |description|
|
||||
next description unless reviewer_ids.nil?
|
||||
|
||||
"#{description}\n\ncc: @gl-dx/qe-maintainers"
|
||||
"#{description}\n\ncc: @gl-dx/maintainers"
|
||||
end
|
||||
}.compact))
|
||||
end
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ ee/spec/frontend/security_dashboard/components/shared/vulnerability_report/vulne
|
|||
ee/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
|
||||
ee/spec/frontend/status_checks/components/modal_create_spec.js
|
||||
ee/spec/frontend/status_checks/mount_spec.js
|
||||
ee/spec/frontend/tracing/details/tracing_header_spec.js
|
||||
ee/spec/frontend/usage_quotas/transfer/components/usage_by_month_spec.js
|
||||
ee/spec/frontend/users/identity_verification/components/international_phone_input_spec.js
|
||||
ee/spec/frontend/users/identity_verification/components/verify_phone_verification_code_spec.js
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue