Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b1bf08b86b
commit
f105c883a7
|
|
@ -61,6 +61,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
workItem: {},
|
||||
disableTruncation: false,
|
||||
isEditing: this.editMode,
|
||||
isSubmitting: false,
|
||||
isSubmittingWithKeydown: false,
|
||||
|
|
@ -181,6 +182,7 @@ export default {
|
|||
},
|
||||
async startEditing() {
|
||||
this.isEditing = true;
|
||||
this.disableTruncation = true;
|
||||
|
||||
this.descriptionText = getDraft(this.autosaveKey) || this.workItemDescription?.description;
|
||||
|
||||
|
|
@ -359,6 +361,7 @@ export default {
|
|||
:disable-inline-editing="disableInlineEditing"
|
||||
:work-item-description="workItemDescription"
|
||||
:can-edit="canEdit"
|
||||
:disable-truncation="disableTruncation"
|
||||
@startEditing="startEditing"
|
||||
@descriptionUpdated="handleDescriptionTextUpdated"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
const isCheckbox = (target) => target?.classList.contains('task-list-item-checkbox');
|
||||
|
||||
|
|
@ -13,7 +14,13 @@ export default {
|
|||
components: {
|
||||
GlButton,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
disableTruncation: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
workItemDescription: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
|
@ -30,6 +37,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
truncated: false,
|
||||
checkboxes: [],
|
||||
};
|
||||
},
|
||||
|
|
@ -49,6 +57,9 @@ export default {
|
|||
showEditButton() {
|
||||
return this.canEdit && !this.disableInlineEditing;
|
||||
},
|
||||
isTruncated() {
|
||||
return this.truncated && !this.disableTruncation && this.glFeatures.workItemsMvc2;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
descriptionHtml: {
|
||||
|
|
@ -74,6 +85,7 @@ export default {
|
|||
checkbox.disabled = false;
|
||||
});
|
||||
}
|
||||
this.truncateLongDescription();
|
||||
},
|
||||
toggleCheckboxes(event) {
|
||||
const { target } = event;
|
||||
|
|
@ -105,6 +117,21 @@ export default {
|
|||
this.$emit('descriptionUpdated', newDescriptionText);
|
||||
}
|
||||
},
|
||||
truncateLongDescription() {
|
||||
/* Truncate when description is > 40% viewport height or 512px.
|
||||
Update `.work-item-description .truncated` max height if value changes. */
|
||||
const defaultMaxHeight = document.documentElement.clientHeight * 0.4;
|
||||
let maxHeight = defaultMaxHeight;
|
||||
if (defaultMaxHeight > 512) {
|
||||
maxHeight = 512;
|
||||
} else if (defaultMaxHeight < 256) {
|
||||
maxHeight = 256;
|
||||
}
|
||||
this.truncated = this.$refs['gfm-content'].clientHeight > maxHeight;
|
||||
},
|
||||
showAll() {
|
||||
this.truncated = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -130,11 +157,32 @@ export default {
|
|||
<div v-if="showEmptyDescription" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
|
||||
<div
|
||||
v-else-if="!descriptionEmpty"
|
||||
ref="gfm-content"
|
||||
v-safe-html="descriptionHtml"
|
||||
class="md gl-mb-5 gl-min-h-8 gl-clearfix"
|
||||
data-testid="work-item-description"
|
||||
@change="toggleCheckboxes"
|
||||
></div>
|
||||
ref="description"
|
||||
class="work-item-description md gl-mb-5 gl-min-h-8 gl-clearfix gl-relative"
|
||||
>
|
||||
<div
|
||||
ref="gfm-content"
|
||||
v-safe-html="descriptionHtml"
|
||||
data-testid="work-item-description"
|
||||
:class="{ truncated: isTruncated }"
|
||||
@change="toggleCheckboxes"
|
||||
></div>
|
||||
<div
|
||||
v-if="isTruncated"
|
||||
class="description-more gl-display-block gl-w-full"
|
||||
data-test-id="description-read-more"
|
||||
>
|
||||
<div class="show-all-btn gl-w-full gl--flex-center">
|
||||
<gl-button
|
||||
variant="confirm"
|
||||
category="tertiary"
|
||||
class="gl-mx-4"
|
||||
data-testid="show-all-btn"
|
||||
@click="showAll"
|
||||
>{{ __('Read more') }}</gl-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -388,3 +388,46 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
|
|||
max-width: $fixed-layout-width;
|
||||
}
|
||||
}
|
||||
|
||||
.work-item-description .truncated{
|
||||
max-height: clamp(16rem, 40vh, 32rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.description-more{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 3rem;
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, $white 100%);
|
||||
|
||||
.gl-dark & {
|
||||
background: linear-gradient(180deg, rgba(31, 30, 36, 0.00) 0%, $gray-950 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.show-all-btn {
|
||||
pointer-events: auto;
|
||||
background-color: $white;
|
||||
|
||||
.gl-dark & {
|
||||
background-color: $gray-950;
|
||||
}
|
||||
|
||||
&::before, &::after {
|
||||
content: '';
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
border-top: 1px solid $gray-50;
|
||||
|
||||
.gl-dark & {
|
||||
border-top: 1px solid $gray-900;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,11 +58,12 @@ Audit event types belong to the following product categories.
|
|||
| [`audit_events_streaming_instance_headers_destroy`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127228) | Triggered when a streaming header for instance level external audit event destination is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/417433) | Instance |
|
||||
| [`audit_events_streaming_instance_headers_update`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127228) | Triggered when a streaming header for instance level external audit event destination is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/417433) | Instance |
|
||||
| [`create_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74632) | Event triggered when an external audit event destination is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) | Group |
|
||||
| [`create_group_audit_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147888) | Event triggered when an external audit event destination for a top-level group is created.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/436610) | Group |
|
||||
| [`create_http_namespace_filter`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136047) | Event triggered when a namespace filter for an external audit event destination for a top-level group is created.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.6](https://gitlab.com/gitlab-org/gitlab/-/issues/424176) | Group |
|
||||
| [`create_instance_audit_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148383) | Event triggered when an external audit event destination for a GitLab instance is created.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/436615) | Instance |
|
||||
| [`create_instance_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123882) | Event triggered when an instance level external audit event destination is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.2](https://gitlab.com/gitlab-org/gitlab/-/issues/404730) | Instance |
|
||||
| [`created_group_audit_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147888) | Event triggered when an external audit event destination for a top-level group is created.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/436610) | Group |
|
||||
| [`delete_http_namespace_filter`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136302) | Event triggered when a namespace filter for an external audit event destination for a top-level group is deleted.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.7](https://gitlab.com/gitlab-org/gitlab/-/issues/424177) | Group |
|
||||
| [`deleted_group_audit_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148738) | Event triggered when an external audit event destination for a top-level group is deleted.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/436610) | Group |
|
||||
| [`destroy_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74632) | Event triggered when an external audit event destination is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) | Group |
|
||||
| [`destroy_instance_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125846) | Event triggered when an instance level external audit event destination is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.2](https://gitlab.com/gitlab-org/gitlab/-/issues/404730) | Instance |
|
||||
| [`event_type_filters_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113081) | Event triggered when a new audit events streaming event type filter is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/344848) | Group |
|
||||
|
|
|
|||
|
|
@ -4761,6 +4761,28 @@ Input type: `GroupAuditEventStreamingDestinationsCreateInput`
|
|||
| <a id="mutationgroupauditeventstreamingdestinationscreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationgroupauditeventstreamingdestinationscreateexternalauditeventdestination"></a>`externalAuditEventDestination` | [`GroupAuditEventStreamingDestination`](#groupauditeventstreamingdestination) | Destination created. |
|
||||
|
||||
### `Mutation.groupAuditEventStreamingDestinationsDelete`
|
||||
|
||||
DETAILS:
|
||||
**Introduced** in GitLab 16.11.
|
||||
**Status**: Experiment.
|
||||
|
||||
Input type: `GroupAuditEventStreamingDestinationsDeleteInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationgroupauditeventstreamingdestinationsdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationgroupauditeventstreamingdestinationsdeleteid"></a>`id` | [`AuditEventsGroupExternalStreamingDestinationID!`](#auditeventsgroupexternalstreamingdestinationid) | ID of the audit events external streaming destination to delete. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationgroupauditeventstreamingdestinationsdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationgroupauditeventstreamingdestinationsdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
|
||||
### `Mutation.groupMemberBulkUpdate`
|
||||
|
||||
Input type: `GroupMemberBulkUpdateInput`
|
||||
|
|
@ -34153,6 +34175,12 @@ A `AuditEventsGoogleCloudLoggingConfigurationID` is a global ID. It is encoded a
|
|||
|
||||
An example `AuditEventsGoogleCloudLoggingConfigurationID` is: `"gid://gitlab/AuditEvents::GoogleCloudLoggingConfiguration/1"`.
|
||||
|
||||
### `AuditEventsGroupExternalStreamingDestinationID`
|
||||
|
||||
A `AuditEventsGroupExternalStreamingDestinationID` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `AuditEventsGroupExternalStreamingDestinationID` is: `"gid://gitlab/AuditEvents::Group::ExternalStreamingDestination/1"`.
|
||||
|
||||
### `AuditEventsInstanceAmazonS3ConfigurationID`
|
||||
|
||||
A `AuditEventsInstanceAmazonS3ConfigurationID` is a global ID. It is encoded as a string.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,370 @@
|
|||
---
|
||||
stage: Manage
|
||||
group: Import and Integrate
|
||||
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
|
||||
---
|
||||
|
||||
# Adding a new relation to the direct transfer importer
|
||||
|
||||
At a high level, to add a new relation to the direct transfer importer, you must:
|
||||
|
||||
1. Add a new relation to the list of exported data.
|
||||
1. Add a new ETL (Extract/Transform/Load) Pipeline on the import side with data processing instructions.
|
||||
1. Add newly-created pipeline to the list of importing stages.
|
||||
1. Ensure sufficient test coverage.
|
||||
|
||||
## Export from source
|
||||
|
||||
There are a few types of relations we export:
|
||||
|
||||
- ActiveRecord associations. Read from `import_export.yml` file, serialized to JSON, written to a NDJSON file. Each relation is exported to either a `.gz` file, or `.tar.gz`
|
||||
file if a collection, uploaded, and served using the REST API of destination instance of GitLab to download and import.
|
||||
- Binary files. For example, uploads or LFS objects.
|
||||
- A handful of relations that are not exported but are read from the GraphQL API directly during import.
|
||||
|
||||
For ActiveRecord associations, you should use NDJSON over GraphQL API for performance reasons. Heavily-nested associations can produce a lot of network
|
||||
requests which can slow down the overall migration.
|
||||
|
||||
### Exporting an ActiveRecord relation
|
||||
|
||||
The direct transfer importer's underlying behavior is heavily based on file-based importer,
|
||||
which uses the [`import_export.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/import_export/project/import_export.yml) file that
|
||||
describes a list of `Project` associations to be included in the export.
|
||||
A similar [`import_export.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/import_export/group/import_export.yml) is available for `Group`.
|
||||
|
||||
For example, let's say we have a new `Project` association called `documents`. To add support for importing that new association, we must:
|
||||
|
||||
1. Add it to `import_export.yml` file.
|
||||
1. Add test coverage for the new relation.
|
||||
1. Verify that the added relation is exporting as expected.
|
||||
|
||||
#### Add it to `import_export.yml` file
|
||||
|
||||
NOTE:
|
||||
Associations listed in this file are imported from top to bottom. If you have an association that is order-dependent, put the dependencies before the
|
||||
associations that require them. For example, documents must be imported before merge requests, otherwise they are not valid.
|
||||
|
||||
1. Add your association to `tree.project` within the `import_export.yml`.
|
||||
|
||||
```diff
|
||||
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
|
||||
index 43d66e0e67b7..0880a27dfce2 100644
|
||||
--- a/lib/gitlab/import_export/project/import_export.yml
|
||||
+++ b/lib/gitlab/import_export/project/import_export.yml
|
||||
@@ -122,6 +122,7 @@ tree:
|
||||
- label:
|
||||
- :priorities
|
||||
- :service_desk_setting
|
||||
+ - :documents
|
||||
group_members:
|
||||
- :user
|
||||
```
|
||||
|
||||
NOTE:
|
||||
If your association is relates to an Enterprise Edition-only feature, add it to the `ee.tree.project` tree at the end of the file so that it is only exported
|
||||
and imported in Enterprise Edition instances of GitLab.
|
||||
|
||||
If your association doesn't need to include any sub-relations, then this is enough. But if it needs more sub-relations to be included (for example, notes),
|
||||
you must list them out. Let's say documents can have notes (with award emojis on notes) and award emojis (on documents), which we want to migrate. In this
|
||||
case, our relation becomes the following:
|
||||
|
||||
```diff
|
||||
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
|
||||
index 43d66e0e67b7..0880a27dfce2 100644
|
||||
--- a/lib/gitlab/import_export/project/import_export.yml
|
||||
+++ b/lib/gitlab/import_export/project/import_export.yml
|
||||
@@ -122,6 +122,7 @@ tree:
|
||||
- label:
|
||||
- :priorities
|
||||
- :service_desk_setting
|
||||
+ - documents:
|
||||
- :award_emoji
|
||||
- notes:
|
||||
- :award_emoji
|
||||
group_members:
|
||||
- :user
|
||||
```
|
||||
|
||||
1. Add `included_attributes` of the relation. By default, any relation attribute that is not listed in `included_attributes` of the YAML file are filtered
|
||||
out on both export and import. To include the attributes you need, you must add them to `included_attributes` list as following:
|
||||
|
||||
```diff
|
||||
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
|
||||
index 43d66e0e67b7..dbf0e1275ecf 100644
|
||||
--- a/lib/gitlab/import_export/project/import_export.yml
|
||||
+++ b/lib/gitlab/import_export/project/import_export.yml
|
||||
@@ -142,6 +142,9 @@ import_only_tree:
|
||||
|
||||
# Only include the following attributes for the models specified.
|
||||
included_attributes:
|
||||
+ documents:
|
||||
+ - :title
|
||||
+ - :description
|
||||
user:
|
||||
- :id
|
||||
- :public_email
|
||||
```
|
||||
|
||||
1. Add `excluded_attributes` of the relation. We also have `excluded_attributes` list present in the file. You don't need to add excluded attributes for
|
||||
`Project`, but you do still need to do it for `Group`. This list represent attributes that should not be included in the export and should be ignored
|
||||
on import. These attributes usually are:
|
||||
|
||||
- Anything that ends on `_id` or `_ids`
|
||||
- Anything that includes `attributes` (except `custom_attributes`)
|
||||
- Anything that ends on `_html`
|
||||
- Anything sensitive (e.g. tokens, encrypted data)
|
||||
|
||||
See a full list of prohibited references [here](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/import_export/attribute_cleaner.rb#L14-21).
|
||||
|
||||
1. Add `methods` of the relation. If your relation has a method (for example, `document.signature`) that must also be exported, you can add it in the `methods` section.
|
||||
The exported value will be present in the export and you can do something with it on import. For example, assigning it to a field.
|
||||
|
||||
For example, we export return value of `note_diff_file.diff_export` [method](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/import_export/project/import_export.yml#L1161-1161) and on import
|
||||
[set `note_diff_file.diff`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/import_export/project/relation_factory.rb#L149-151) to the exported value of this method.
|
||||
|
||||
#### Add test coverage for new relation
|
||||
|
||||
Because the direct transfer uses the file-based importer under the hood, we must add test coverage for a new relation with tests in the scope of the file-based
|
||||
importer, which also covers the export side of the direct transfer importer. Add tests to:
|
||||
|
||||
1. `spec/lib/gitlab/import_export/project/tree_saver_spec.rb`. A similar file is available for `Group`.
|
||||
1. `ee/spec/lib/ee/gitlab/import_export/project/tree_saver_spec.rb` for EE-specific relations.
|
||||
|
||||
Follow other relations example to add the new tests.
|
||||
|
||||
#### Verifying added relation is exporting as expected
|
||||
|
||||
Any newly-added relation specified in `import_export.yml` is automatically added to the export files written on disk, so no extra actions are required.
|
||||
|
||||
Once the relation is added and tests are added, we can manually check that the relation is exported. It should automatically be included in both:
|
||||
|
||||
- File-based imports and exports. Use the [project export functionality](../../user/project/settings/import_export.md#export-a-project-and-its-data) to export,
|
||||
download, and inspect the exported data.
|
||||
- Direct transfer exports. Use the [`export_relations` API](../../api/project_relations_export.md) to export, download, and inspect exported relations
|
||||
(it might be exported in batches).
|
||||
|
||||
### Export a binary relation
|
||||
|
||||
If adding support for a binary relation:
|
||||
|
||||
1. Create a new export service that performs export on disk. See example
|
||||
[`BulkImports::LfsObjectsExportService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/bulk_imports/lfs_objects_export_service.rb).
|
||||
1. Add the relation to the
|
||||
[list of `file_relations`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/bulk_imports/file_transfer/project_config.rb).
|
||||
1. Add the relation to `BulkImports::FileExportService`.
|
||||
|
||||
[Example](https://gitlab.com/gitlab-org/gitlab/-/commit/7867db2c22fb9c9850e1dcb49f26fa2b89a665c6)
|
||||
|
||||
## Import on destination
|
||||
|
||||
As mentioned above, there are three kinds of relations in direct transfer imports:
|
||||
|
||||
1. NDJSON-exported relations, downloaded from the `export_relations` API. For example, `documents.ndjson.gz`.
|
||||
1. GraphQL API relations. For example, `members` information is fetched using GraphQL to import groupand project user memberships.
|
||||
1. Binary relations, downloaded from the `export_relations` API. For example, `lfs_objects.tar.gz`.
|
||||
|
||||
Because the direct transfer importer is based on the Extract/Transform/Load data processing technique, to start importing a relation we must define:
|
||||
|
||||
- A new relation importing pipeline. For example, `DocumentsPipeline`.
|
||||
- A data extractor for the pipeline to know where and how to extract the data. For example, `NdjsonPipeline`.
|
||||
- A list of transformers, which is a set of classes that are going to transform the data to the format you need.
|
||||
- A loader, which is going to persist data somewhere. For example, save a row in the database or create a new LFS object.
|
||||
|
||||
No matter what type of relation is being imported, the Pipeline class structure is the same:
|
||||
|
||||
```ruby
|
||||
module BulkImports
|
||||
module Common
|
||||
module Pipelines
|
||||
class DocumentsPipeline
|
||||
include Pipeline
|
||||
|
||||
def extract(context)
|
||||
BulkImports::Pipeline::ExtractedData.new(data: file_paths)
|
||||
end
|
||||
|
||||
def transform(context, object)
|
||||
...
|
||||
end
|
||||
|
||||
def load(context, object)
|
||||
document.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Importing a relation from NDJSON
|
||||
|
||||
#### Defining a pipeline
|
||||
|
||||
From the previous example, our `documents` relation is exported to NDJSON file, in which case we can use both:
|
||||
|
||||
- `NdjsonPipeline`, which includes automatic data transformation from a JSON to an ActiveRecord object (which is using file-based importer under the hood).
|
||||
- `NdjsonExtractor`, which downloads the `.ndjson.gz` file from source instance using the `/export_relations/download` REST API endpoint.
|
||||
|
||||
Each step of the ETL pipeline can be defined as a method or a class.
|
||||
|
||||
```ruby
|
||||
class DocumentsPipeline
|
||||
include NdjsonPipeline
|
||||
|
||||
relation_name 'documents'
|
||||
|
||||
extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
|
||||
end
|
||||
```
|
||||
|
||||
This new pipeline will now:
|
||||
|
||||
1. Download the `documents.ndjson.gz` file from the source instance.
|
||||
1. Read the contents of the NDJSON file and deserialize JSON to convert to an ActiveRecord object.
|
||||
1. Save it in the database in scope of a project.
|
||||
|
||||
A pipeline can be placed under either:
|
||||
|
||||
- The `BulkImports::Common::Pipelines` namespace if it's shared and to be used in both Group and Project migrations. For example, `LabelsPipeline` is a common
|
||||
pipeline and is referenced in both Group and Project stage lists.
|
||||
- The `BulkImports::Projects::Pipelines` namespace if a pipeline belongs to a Project migration.
|
||||
- The `BulkImports::Groups::Pipelines` namespace if a pipeline belongs to a Group migration.
|
||||
|
||||
#### Adding a new pipeline to stages
|
||||
|
||||
The direct transfer importer performs migration of groups and projects in stages. The list of stages is defined in:
|
||||
|
||||
- For `Project`: `lib/bulk_imports/projects/stage.rb`.
|
||||
- For `Group`: `lib/bulk_imports/groups/stage.rb`.
|
||||
|
||||
Each stage:
|
||||
|
||||
- Can have multiple pipelines that run in parallel.
|
||||
- Must fully complete before moving to the next stage.
|
||||
|
||||
Let's add our pipeline to the `Project` stage:
|
||||
|
||||
```ruby
|
||||
module BulkImports
|
||||
module Projects
|
||||
class Stage < ::BulkImports::Stage
|
||||
private
|
||||
|
||||
def config
|
||||
{
|
||||
project: {
|
||||
pipeline: BulkImports::Projects::Pipelines::ProjectPipeline,
|
||||
stage: 0
|
||||
},
|
||||
repository: {
|
||||
pipeline: BulkImports::Projects::Pipelines::RepositoryPipeline,
|
||||
maximum_source_version: '15.0.0',
|
||||
stage: 1
|
||||
},
|
||||
documents: {
|
||||
pipeline: BulkImports::Projects::Pipelines::DocumentsPipeline,
|
||||
minimum_source_version: '16.11.0',
|
||||
stage: 2
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
We specified:
|
||||
|
||||
- `stage: 2`, so project and repository stages must complete first before our pipeline is run in stage 2.
|
||||
- `minimum_source_version: '16.11.0'`. Because we introduced `documents` relation for exports in this milestone, it's not available in previous GitLab versions. Therefore
|
||||
so this pipeline only runs if source version is 16.11 or later.
|
||||
|
||||
NOTE:
|
||||
If a relation is deprecated and need only to run the pipeline up to a certain version, we can specify `maximum_source_version` attribute.
|
||||
|
||||
#### Covering a pipeline with tests
|
||||
|
||||
Because we already covered the export side with tests, we must do the same for the import side. For the direct transfer importer, each pipeline has a separate spec
|
||||
file that would look something like [this example](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb).
|
||||
|
||||
[Example](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb)
|
||||
|
||||
### Importing a relation from GraphQL API
|
||||
|
||||
If your relation is available through GraphQL API, you can use `GraphQlExtractor` and perform transformations and loading within the pipeline class.
|
||||
|
||||
`MembersPipeline` example:
|
||||
|
||||
```ruby
|
||||
module BulkImports
|
||||
module Common
|
||||
module Pipelines
|
||||
class MembersPipeline
|
||||
include Pipeline
|
||||
|
||||
transformer Common::Transformers::ProhibitedAttributesTransformer
|
||||
transformer Common::Transformers::MemberAttributesTransformer
|
||||
|
||||
def extract(context)
|
||||
graphql_extractor.extract(context)
|
||||
end
|
||||
|
||||
def load(_context, data)
|
||||
...
|
||||
|
||||
member.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def graphql_extractor
|
||||
@graphql_extractor ||= BulkImports::Common::Extractors::GraphqlExtractor
|
||||
.new(query: BulkImports::Common::Graphql::GetMembersQuery)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
The rest of the steps are identical to the steps above.
|
||||
|
||||
### Import a binary relation
|
||||
|
||||
A binary relation pipeline has the same structure as other pipelines, all you need to do is define what happens during extract/transform/load steps.
|
||||
|
||||
`LfsObjectsPipeline` example:
|
||||
|
||||
```ruby
|
||||
module BulkImports
|
||||
module Common
|
||||
module Pipelines
|
||||
class LfsObjectsPipeline
|
||||
include Pipeline
|
||||
|
||||
file_extraction_pipeline!
|
||||
|
||||
def extract(_context)
|
||||
download_service.execute
|
||||
decompression_service.execute
|
||||
extraction_service.execute
|
||||
|
||||
...
|
||||
end
|
||||
|
||||
def load(_context, file_path)
|
||||
...
|
||||
|
||||
lfs_object.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
There are a number of helper service classes to assist with data download:
|
||||
|
||||
- `BulkImports::FileDownloadService`: Downloads a file from a given location.
|
||||
- `BulkImports::FileDecompressionService`: Gzip decompression service with required validations.
|
||||
- `BulkImports::ArchiveExtractionService`: Tar extraction service.
|
||||
|
|
@ -147,12 +147,12 @@ In GitLab 15.4, we [removed the deprecated analyzers](https://gitlab.com/gitlab-
|
|||
|
||||
To preview the upcoming changes to the CI/CD configuration in GitLab 15.3 or earlier:
|
||||
|
||||
1. Open an MR to switch from the Stable CI/CD template, `SAST.gitlab-ci.yaml`, to [the Latest template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml), `SAST.latest.gitlab-ci.yaml`.
|
||||
1. Open an MR to switch from the Stable CI/CD template, `SAST.gitlab-ci.yml`, to [the Latest template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml), `SAST.latest.gitlab-ci.yml`.
|
||||
- On GitLab.com, use the latest template directly:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
template: 'Jobs/SAST.latest.gitlab-ci.yaml'
|
||||
template: 'Jobs/SAST.latest.gitlab-ci.yml'
|
||||
```
|
||||
|
||||
- On a self-managed instance, download the template from GitLab.com:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def run
|
||||
created = 0
|
||||
mrs_created_count = 0
|
||||
|
||||
git.with_clean_state do
|
||||
@keeps.each do |keep_class|
|
||||
|
|
@ -80,14 +80,27 @@ module Gitlab
|
|||
|
||||
create(change, branch_name) unless @dry_run
|
||||
|
||||
created += 1
|
||||
break if created >= @max_mrs
|
||||
mrs_created_count += 1
|
||||
break if mrs_created_count >= @max_mrs
|
||||
end
|
||||
break if created >= @max_mrs
|
||||
break if mrs_created_count >= @max_mrs
|
||||
end
|
||||
end
|
||||
|
||||
puts "Housekeeper created #{created} MRs"
|
||||
print_completion_message(mrs_created_count)
|
||||
end
|
||||
|
||||
def print_completion_message(mrs_created_count)
|
||||
mr_count_string = "#{mrs_created_count} #{'MR'.pluralize(mrs_created_count)}"
|
||||
|
||||
completion_message = if @dry_run
|
||||
"Dry run complete. Housekeeper would have created #{mr_count_string} on an actual run."
|
||||
else
|
||||
"Housekeeper created #{mr_count_string}."
|
||||
end
|
||||
|
||||
puts completion_message.yellowish
|
||||
puts
|
||||
end
|
||||
|
||||
def add_standard_change_data(change)
|
||||
|
|
|
|||
|
|
@ -269,6 +269,16 @@ RSpec.describe ::Gitlab::Housekeeper::Runner do
|
|||
described_class.new(max_mrs: 2, keeps: [fake_keep]).run
|
||||
end
|
||||
end
|
||||
|
||||
context 'on dry run' do
|
||||
context 'for completion message' do
|
||||
it 'prints the expected message' do
|
||||
expect do
|
||||
described_class.new(max_mrs: 1, keeps: [fake_keep], dry_run: true).run
|
||||
end.to output(/Dry run complete. Housekeeper would have created 1 MR on an actual run./).to_stdout
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#housekeeper_fork_project_id' do
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ module Backup
|
|||
previous_backup = options.previous_backup || options.backup_id
|
||||
|
||||
unpack(previous_backup) if options.incremental?
|
||||
run_all_create_tasks
|
||||
create_all_tasks_result = run_all_create_tasks
|
||||
|
||||
puts_time "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \
|
||||
"and are not included in this backup. You will need these files to restore a backup.\n" \
|
||||
"Please back them up manually.".color(:red)
|
||||
puts_time "Backup #{backup_id} is done."
|
||||
true
|
||||
create_all_tasks_result
|
||||
end
|
||||
|
||||
# @param [Gitlab::Backup::Tasks::Task] task
|
||||
|
|
@ -138,8 +138,8 @@ module Backup
|
|||
end
|
||||
|
||||
build_backup_information
|
||||
|
||||
backup_tasks.each_value { |task| run_create_task(task) }
|
||||
create_task_result = []
|
||||
backup_tasks.each_value { |task| create_task_result << run_create_task(task) }
|
||||
|
||||
write_backup_information
|
||||
|
||||
|
|
@ -149,6 +149,8 @@ module Backup
|
|||
remove_old
|
||||
end
|
||||
|
||||
create_task_result.all?
|
||||
|
||||
ensure
|
||||
cleanup unless options.skippable_operations.archive
|
||||
remove_tmp
|
||||
|
|
|
|||
|
|
@ -40131,9 +40131,6 @@ msgstr ""
|
|||
msgid "ProjectSettings|Infrastructure"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Instrument your application"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Instrumentation details"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40449,9 +40446,6 @@ msgstr ""
|
|||
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab. GitLab Pages uses a caching mechanism for efficiency. Your changes may not take effect until that cache is invalidated, which usually takes less than a minute."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|You need to %{linkStart}set up product analytics%{linkEnd} before your application can be instrumented."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Your project is set up. %{linkStart}View instrumentation instructions%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/fonts": "^1.3.0",
|
||||
"@gitlab/svgs": "3.95.0",
|
||||
"@gitlab/ui": "78.10.1",
|
||||
"@gitlab/ui": "78.12.0",
|
||||
"@gitlab/visual-review-tools": "1.7.3",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20240226152102",
|
||||
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ module QA
|
|||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
it 'user rebases source branch of merge request', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347735' do
|
||||
it 'user rebases source branch of merge request', :blocking, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347735' do
|
||||
merge_request.project.visit!
|
||||
|
||||
Page::Project::Menu.perform(&:go_to_merge_request_settings)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module QA
|
||||
RSpec.describe 'Create' do
|
||||
describe 'File templates', product_group: :source_code do
|
||||
describe 'File templates', :blocking, product_group: :source_code do
|
||||
include Runtime::Fixtures
|
||||
|
||||
let(:project) do
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ module QA
|
|||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
it 'user creates a project snippet', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347798' do
|
||||
it 'user creates a project snippet', :blocking, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347798' do
|
||||
snippet
|
||||
|
||||
Page::Dashboard::Snippet::Show.perform do |snippet|
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ exports[`Alert integration settings form default state should match the default
|
|||
id="reference-1"
|
||||
items="[object Object]"
|
||||
noresultstext="No results found"
|
||||
placement="left"
|
||||
placement="bottom-start"
|
||||
positioningstrategy="absolute"
|
||||
resetbuttonlabel=""
|
||||
searchplaceholder="Search"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ describe('ProjectSelect', () => {
|
|||
loading: false,
|
||||
multiple: false,
|
||||
noResultsText: 'No matching results',
|
||||
placement: 'left',
|
||||
placement: 'bottom-start',
|
||||
searchPlaceholder: 'Search projects',
|
||||
searchable: true,
|
||||
searching: false,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ exports[`packages_list_row renders 1`] = `
|
|||
icon="ellipsis_v"
|
||||
items=""
|
||||
nocaret="true"
|
||||
placement="left"
|
||||
placement="bottom-start"
|
||||
positioningstrategy="absolute"
|
||||
size="medium"
|
||||
textsronly="true"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ describe('WorkItemDescription', () => {
|
|||
workItemDescription = defaultWorkItemDescription,
|
||||
canEdit = false,
|
||||
disableInlineEditing = false,
|
||||
mockComputed = {},
|
||||
hasWorkItemsMvc2 = false,
|
||||
} = {}) => {
|
||||
wrapper = shallowMount(WorkItemDescriptionRendered, {
|
||||
propsData: {
|
||||
|
|
@ -28,6 +30,10 @@ describe('WorkItemDescription', () => {
|
|||
canEdit,
|
||||
disableInlineEditing,
|
||||
},
|
||||
computed: mockComputed,
|
||||
provide: {
|
||||
workItemsMvc2: hasWorkItemsMvc2,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -39,6 +45,44 @@ describe('WorkItemDescription', () => {
|
|||
expect(renderGFM).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('with truncation', () => {
|
||||
it('shows the untruncate action', () => {
|
||||
createComponent({
|
||||
workItemDescription: {
|
||||
description: 'This is a long description',
|
||||
descriptionHtml: '<p>This is a long description</p>',
|
||||
},
|
||||
mockComputed: {
|
||||
isTruncated() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
hasWorkItemsMvc2: true,
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-id="description-read-more"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without truncation', () => {
|
||||
it('does not show the untruncate action', () => {
|
||||
createComponent({
|
||||
workItemDescription: {
|
||||
description: 'This is a long description',
|
||||
descriptionHtml: '<p>This is a long description</p>',
|
||||
},
|
||||
mockComputed: {
|
||||
isTruncated() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
hasWorkItemsMvc2: true,
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-id="description-read-more"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with checkboxes', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
|
|
|
|||
|
|
@ -88,6 +88,36 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
|
|||
|
||||
subject.run_create_task(backup_tasks)
|
||||
end
|
||||
|
||||
context 'when the task succeeds' do
|
||||
it 'returns true' do
|
||||
expect(target).to receive(:dump)
|
||||
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping database ... ')
|
||||
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping database ... done')
|
||||
expect(subject.run_create_task(backup_tasks)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the task fails with a known error' do
|
||||
it 'returns false' do
|
||||
allow(target).to receive(:dump).and_raise(Backup::DatabaseBackupError.new({ host: 'foo', port: 'bar', database: 'baz' }, 'foo'))
|
||||
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping database ... ')
|
||||
expect(Gitlab::BackupLogger).to receive(:info).with(message: match('Dumping database failed: Failed to create compressed file '))
|
||||
|
||||
expect(subject.run_create_task(backup_tasks)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the task fails with an unknown error' do
|
||||
it 'returns false' do
|
||||
allow(target).to receive(:dump).and_raise(StandardError)
|
||||
expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping database ... ')
|
||||
|
||||
expect do
|
||||
subject.run_create_task(backup_tasks)
|
||||
end.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#run_restore_task' do
|
||||
|
|
@ -997,6 +1027,22 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a single task fails' do
|
||||
before do
|
||||
stub_env('SKIP', 'tar') # avoiding an error during #pack
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(Dir.glob(backup_path.join('*')), secure: true)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
allow(lfs).to receive(:backup!).and_raise(Backup::FileBackupError.new('foo', 'bar'))
|
||||
|
||||
expect(subject.create).to be_falsey # rubocop:disable Rails/SaveBang -- not a Rails create method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#restore' do
|
||||
|
|
|
|||
|
|
@ -365,6 +365,12 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
|
|||
expect { run_rake_task(rake_task) }.to raise_error(SystemExit)
|
||||
end.to output(Regexp.new(error.message)).to_stdout_from_any_process
|
||||
end
|
||||
|
||||
it "raises an error with message when subtask fails" do
|
||||
expect do
|
||||
run_rake_task('gitlab:backup:create')
|
||||
end.to raise_error(Backup::Error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1326,10 +1326,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.95.0.tgz#80b128efbfb7ad4a46d94ac97f0bc326433ed256"
|
||||
integrity sha512-UQ91tQNRtsVnEoR/sd6Lk0dNZKLAIeTyadwg0BhW7i6KL7V9XN3UQkvoo4K1KIDwBSnOSm1WPEy4Xvm4Vx/60Q==
|
||||
|
||||
"@gitlab/ui@78.10.1":
|
||||
version "78.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-78.10.1.tgz#7f5dead96ad0ba26ad2e2fd50f4acf74b46999f6"
|
||||
integrity sha512-H6tJ4UTUKn/LY1g7tRwKoV31fmDyr/MMW22r8dRDpwrhq2s4bBaT2SU8QKyQ9pvCUQvaOU67ogooBc8fkuOPLA==
|
||||
"@gitlab/ui@78.12.0":
|
||||
version "78.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-78.12.0.tgz#d5d6a3fff4eff8cf5dcc19a90fd70275e2802f8e"
|
||||
integrity sha512-1mck+oEiZuAc5goyojezh8B5GngpkUO3pTV5xeuH1nctCQatjj7RaSCvcZmFjH1bzTeKz66J530t/5BO3gOwHw==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "1.4.3"
|
||||
bootstrap-vue "2.23.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue