Commit 28132cea authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 530ae15c 83888b9b
f69cea16bcc88ddf29fb6c4c67a5d788fbc00f9a 780588a55b9219f3157cc984f7e1b7aa9f9124f2
<script> <script>
import { GlLink, GlSprintf } from '@gitlab/ui'; import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { PackageType } from '~/packages/shared/constants'; import {
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
} from '~/packages_and_registries/package_registry/constants';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
export default { export default {
...@@ -25,12 +29,18 @@ export default { ...@@ -25,12 +29,18 @@ export default {
}, },
computed: { computed: {
showMetadata() { showMetadata() {
const visibilityConditions = { return [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes(
[PackageType.NUGET]: this.packageEntity.nuget_metadatum, this.packageEntity.packageType,
[PackageType.CONAN]: this.packageEntity.conan_metadatum, );
[PackageType.MAVEN]: this.packageEntity.maven_metadatum, },
}; showNugetMetadata() {
return visibilityConditions[this.packageEntity.package_type]; return this.packageEntity.packageType === PACKAGE_TYPE_NUGET;
},
showConanMetadata() {
return this.packageEntity.packageType === PACKAGE_TYPE_CONAN;
},
showMavenMetadata() {
return this.packageEntity.packageType === PACKAGE_TYPE_MAVEN;
}, },
}, },
}; };
...@@ -41,12 +51,12 @@ export default { ...@@ -41,12 +51,12 @@ export default {
<h3 class="gl-font-lg" data-testid="title">{{ __('Additional Metadata') }}</h3> <h3 class="gl-font-lg" data-testid="title">{{ __('Additional Metadata') }}</h3>
<div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main"> <div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main">
<template v-if="packageEntity.nuget_metadatum"> <template v-if="showNugetMetadata">
<details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source"> <details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
<gl-sprintf :message="$options.i18n.sourceText"> <gl-sprintf :message="$options.i18n.sourceText">
<template #link> <template #link>
<gl-link :href="packageEntity.nuget_metadatum.project_url" target="_blank">{{ <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{
packageEntity.nuget_metadatum.project_url packageEntity.metadata.projectUrl
}}</gl-link> }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
...@@ -54,8 +64,8 @@ export default { ...@@ -54,8 +64,8 @@ export default {
<details-row icon="license" padding="gl-p-4" data-testid="nuget-license"> <details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
<gl-sprintf :message="$options.i18n.licenseText"> <gl-sprintf :message="$options.i18n.licenseText">
<template #link> <template #link>
<gl-link :href="packageEntity.nuget_metadatum.license_url" target="_blank">{{ <gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{
packageEntity.nuget_metadatum.license_url packageEntity.metadata.licenseUrl
}}</gl-link> }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
...@@ -63,28 +73,28 @@ export default { ...@@ -63,28 +73,28 @@ export default {
</template> </template>
<details-row <details-row
v-else-if="packageEntity.conan_metadatum" v-else-if="showConanMetadata"
icon="information-o" icon="information-o"
padding="gl-p-4" padding="gl-p-4"
data-testid="conan-recipe" data-testid="conan-recipe"
> >
<gl-sprintf :message="$options.i18n.recipeText"> <gl-sprintf :message="$options.i18n.recipeText">
<template #recipe>{{ packageEntity.name }}</template> <template #recipe>{{ packageEntity.metadata.recipe }}</template>
</gl-sprintf> </gl-sprintf>
</details-row> </details-row>
<template v-else-if="packageEntity.maven_metadatum"> <template v-else-if="showMavenMetadata">
<details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app"> <details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app">
<gl-sprintf :message="$options.i18n.appName"> <gl-sprintf :message="$options.i18n.appName">
<template #name> <template #name>
<strong>{{ packageEntity.maven_metadatum.app_name }}</strong> <strong>{{ packageEntity.metadata.appName }}</strong>
</template> </template>
</gl-sprintf> </gl-sprintf>
</details-row> </details-row>
<details-row icon="information-o" padding="gl-p-4" data-testid="maven-group"> <details-row icon="information-o" padding="gl-p-4" data-testid="maven-group">
<gl-sprintf :message="$options.i18n.appGroup"> <gl-sprintf :message="$options.i18n.appGroup">
<template #group> <template #group>
<strong>{{ packageEntity.maven_metadatum.app_group }}</strong> <strong>{{ packageEntity.metadata.appGroup }}</strong>
</template> </template>
</gl-sprintf> </gl-sprintf>
</details-row> </details-row>
......
...@@ -19,13 +19,13 @@ import { convertToGraphQLId } from '~/graphql_shared/utils'; ...@@ -19,13 +19,13 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility'; import { objectToQuery } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
// import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
// import DependencyRow from '~/packages/details/components/dependency_row.vue'; // import DependencyRow from '~/packages/details/components/dependency_row.vue';
// import InstallationCommands from '~/packages/details/components/installation_commands.vue'; // import InstallationCommands from '~/packages/details/components/installation_commands.vue';
// import PackageFiles from '~/packages/details/components/package_files.vue'; // import PackageFiles from '~/packages/details/components/package_files.vue';
// import PackageListRow from '~/packages/shared/components/package_list_row.vue'; // import PackageListRow from '~/packages/shared/components/package_list_row.vue';
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue'; import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
import { packageTypeToTrackCategory } from '~/packages/shared/utils'; import { packageTypeToTrackCategory } from '~/packages/shared/utils';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue'; import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import { import {
PACKAGE_TYPE_NUGET, PACKAGE_TYPE_NUGET,
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
// PackageListRow, // PackageListRow,
// DependencyRow, // DependencyRow,
PackageHistory, PackageHistory,
// AdditionalMetadata, AdditionalMetadata,
// InstallationCommands, // InstallationCommands,
// PackageFiles, // PackageFiles,
}, },
...@@ -244,9 +244,9 @@ export default { ...@@ -244,9 +244,9 @@ export default {
:package-entity="packageEntity" :package-entity="packageEntity"
:npm-path="npmPath" :npm-path="npmPath"
:npm-help-path="npmHelpPath" :npm-help-path="npmHelpPath"
/> /> -->
<additional-metadata :package-entity="packageEntity" /> --> <additional-metadata :package-entity="packageEntity" />
</div> </div>
<!-- <package-files <!-- <package-files
......
{
"__schema": {
"types": [
{
"kind": "UNION",
"name": "PackageMetadata",
"possibleTypes": [
{ "name": "ComposerMetadata" },
{ "name": "ConanMetadata" },
{ "name": "MavenMetadata" },
{ "name": "NugetMetadata" },
{ "name": "PypiMetadata" }
]
}
]
}
}
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './fragmentTypes.json';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -8,6 +14,9 @@ export const apolloProvider = new VueApollo({ ...@@ -8,6 +14,9 @@ export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient( defaultClient: createDefaultClient(
{}, {},
{ {
cacheConfig: {
fragmentMatcher,
},
assumeImmutableResults: true, assumeImmutableResults: true,
}, },
), ),
......
...@@ -40,5 +40,35 @@ query getPackageDetails($id: ID!) { ...@@ -40,5 +40,35 @@ query getPackageDetails($id: ID!) {
size size
} }
} }
metadata {
... on ComposerMetadata {
targetSha
composerJson {
license
version
}
}
... on PypiMetadata {
requiredPython
}
... on ConanMetadata {
packageChannel
packageUsername
recipe
recipePath
}
... on MavenMetadata {
appName
appGroup
appVersion
path
}
... on NugetMetadata {
iconUrl
licenseUrl
projectUrl
}
}
} }
} }
...@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> Introduced in GitLab 9.4. > Introduced in GitLab 9.4.
NOTE: NOTE:
We intend to [rename IP whitelist as `IP allowlist`](https://gitlab.com/gitlab-org/gitlab/-/issues/7554). We intend to [rename IP whitelist as `IP allowlist`](https://gitlab.com/groups/gitlab-org/-/epics/3478).
GitLab provides some [monitoring endpoints](../../user/admin_area/monitoring/health_check.md) GitLab provides some [monitoring endpoints](../../user/admin_area/monitoring/health_check.md)
that provide health check information when probed. that provide health check information when probed.
......
...@@ -183,8 +183,8 @@ A paragraph that explains what the tutorial does, and the expected outcome. ...@@ -183,8 +183,8 @@ A paragraph that explains what the tutorial does, and the expected outcome.
To create a website: To create a website:
- [Step 1: Do the first task](#do-the-first-task) 1. [Do the first task](#do-the-first-task)
- [Step 2: Do the second task](#do-the-second-task) 1. [Do the second task](#do-the-second-task)
Prerequisites (optional): Prerequisites (optional):
...@@ -197,8 +197,8 @@ Prerequisites (optional): ...@@ -197,8 +197,8 @@ Prerequisites (optional):
To do step 1: To do step 1:
1. First step. 1. First step.
2. Another step. 1. Another step.
3. Another step. 1. Another step.
## Do the second task ## Do the second task
...@@ -207,8 +207,8 @@ Before you begin, make sure you have [done the first task](#do-the-first-task). ...@@ -207,8 +207,8 @@ Before you begin, make sure you have [done the first task](#do-the-first-task).
To do step 2: To do step 2:
1. First step. 1. First step.
2. Another step. 1. Another step.
3. Another step. 1. Another step.
``` ```
### Get started ### Get started
......
...@@ -148,7 +148,7 @@ from those IPs and allow them. ...@@ -148,7 +148,7 @@ from those IPs and allow them.
GitLab.com is fronted by Cloudflare. For incoming connections to GitLab.com, you might need to allow CIDR blocks of Cloudflare ([IPv4](https://www.cloudflare.com/ips-v4) and [IPv6](https://www.cloudflare.com/ips-v6)). GitLab.com is fronted by Cloudflare. For incoming connections to GitLab.com, you might need to allow CIDR blocks of Cloudflare ([IPv4](https://www.cloudflare.com/ips-v4) and [IPv6](https://www.cloudflare.com/ips-v6)).
For outgoing connections from CI/CD runners, we are not providing static IP For outgoing connections from CI/CD runners, we are not providing static IP
addresses. All GitLab runners are deployed into Google Cloud Platform (GCP). Any addresses. All GitLab.com shared runners are deployed into Google Cloud Platform (GCP). Any
IP-based firewall can be configured by looking up all IP-based firewall can be configured by looking up all
[IP address ranges or CIDR blocks for GCP](https://cloud.google.com/compute/docs/faq#find_ip_range). [IP address ranges or CIDR blocks for GCP](https://cloud.google.com/compute/docs/faq#find_ip_range).
......
# frozen_string_literal: true
module Gitlab
module Pagination
module Keyset
class ColumnConditionBuilder
# This class builds the WHERE conditions for the keyset pagination library.
# It produces WHERE conditions for one column at a time.
#
# Requisite 1: Only the last column (columns.last) is non-nullable and distinct.
# Requisite 2: Only one column is distinct and non-nullable.
#
# Scenario: We want to order by columns named X, Y and Z and build the conditions
# used in the WHERE clause of a pagination query using a set of cursor values.
# X is the column definition for a nullable column
# Y is the column definition for a non-nullable but not distinct column
# Z is the column definition for a distinct, non-nullable column used as a tie breaker.
#
# Then the method is initially invoked with these arguments:
# columns = [ColumnDefinition for X, ColumnDefinition for Y, ColumnDefinition for Z]
# values = { X: x, Y: y, Z: z } => these represent cursor values for pagination
# (x could be nil since X is nullable)
# current_conditions is initialized to [] to store the result during the iteration calls
# invoked within the Order#build_where_values method.
#
# The elements of current_conditions are instances of Arel::Nodes and -
# will be concatenated using OR or UNION to be used in the WHERE clause.
#
# Example: Let's say we want to build WHERE clause conditions for
# ORDER BY X DESC NULLS LAST, Y ASC, Z DESC
#
# Iteration 1:
# columns = [X, Y, Z]
# At the end, current_conditions should be:
# [(Z < z)]
#
# Iteration 2:
# columns = [X, Y]
# At the end, current_conditions should be:
# [(Y > y) OR (Y = y AND Z < z)]
#
# Iteration 3:
# columns = [X]
# At the end, current_conditions should be:
# [((X IS NOT NULL AND Y > y) OR (X IS NOT NULL AND Y = y AND Z < z))
# OR
# ((x IS NULL) OR (X IS NULL))]
#
# Parameters:
#
# - columns: instance of ColumnOrderDefinition
# - value: cursor value for the column
def initialize(column, value)
@column = column
@value = value
end
def where_conditions(current_conditions)
return not_nullable_conditions(current_conditions) if column.not_nullable?
return nulls_first_conditions(current_conditions) if column.nulls_first?
# Here we are dealing with the case of column_definition.nulls_last?
# Suppose ORDER BY X DESC NULLS FIRST, Y ASC, Z DESC is the ordering clause
# and we already have built the conditions for columns Y and Z.
#
# We first need a set of conditions to use when x (the value for X) is NULL:
# null_conds = [
# (x IS NULL AND X IS NULL AND Y<y),
# (x IS NULL AND X IS NULL AND Y=y AND Z<z),
null_conds = current_conditions.map do |conditional|
Arel::Nodes::And.new([value_is_null, column_is_null, conditional])
end
# We then need a set of conditions to use when m has an actual value:
# non_null_conds = [
# (x IS NOT NULL AND X IS NULL),
# (x IS NOT NULL AND X < x)
# (x IS NOT NULL AND X = x AND Y > y),
# (x IS NOT NULL AND X = x AND Y = y AND Z < z),
tie_breaking_conds = current_conditions.map do |conditional|
Arel::Nodes::And.new([column_equals_to_value, conditional])
end
non_null_conds = [column_is_null, compare_column_with_value, *tie_breaking_conds].map do |conditional|
Arel::Nodes::And.new([value_is_not_null, conditional])
end
[*null_conds, *non_null_conds]
end
private
# WHEN THE COLUMN IS NON-NULLABLE AND DISTINCT
# Per Assumption 1, only the last column can be non-nullable and distinct
# (column Z is non-nullable/distinct and comes last in the example).
# So the Order#build_where_conditions is being called for the first time with current_conditions = [].
#
# At the end of the call, we should expect:
# current_conditions should be [(Z < z)]
#
# WHEN THE COLUMN IS NON-NULLABLE BUT NOT DISTINCT
# Let's say Z has been processed and we are about to process the column Y next.
# (per requisite 1, if a non-nullable but not distinct column is being processed,
# at the least, the conditional for the non-nullable/distinct column exists)
#
# At the start of the method call:
# current_conditions = [(Z < z)]
# comparison_node = (Y < y)
# eqaulity_node = (Y = y)
#
# We should add a comparison node for the next column Y, (Y < y)
# then break a tie using the previous conditionals, (Y = y AND Z < z)
#
# At the end of the call, we should expect:
# current_conditions = [(Y < y), (Y = y AND Z < z)]
def not_nullable_conditions(current_conditions)
tie_break_conds = current_conditions.map do |conditional|
Arel::Nodes::And.new([column_equals_to_value, conditional])
end
[compare_column_with_value, *tie_break_conds]
end
def nulls_first_conditions(current_conditions)
# Using the same scenario described earlier,
# suppose the ordering clause is ORDER BY X DESC NULLS FIRST, Y ASC, Z DESC
# and we have built the conditions for columns Y and Z in previous iterations:
#
# current_conditions = [(Y > y), (Y = y AND Z < z)]
#
# In this branch of the iteration,
# we first need a set of conditions to use when m (the value for M) is NULL:
# null_conds = [
# (x IS NULL AND X IS NULL AND Y > y),
# (x IS NULL AND X IS NULL AND Y = y AND Z < z),
# (x IS NULL AND X IS NOT NULL)]
#
# Note that when x has an actual value, say x = 3, null_conds evalutes to FALSE.
tie_breaking_conds = current_conditions.map do |conditional|
Arel::Nodes::And.new([column_is_null, conditional])
end
null_conds = [*tie_breaking_conds, column_is_not_null].map do |conditional|
Arel::Nodes::And.new([value_is_null, conditional])
end
# We then need a set of conditions to use when m has an actual value:
# non_null_conds = [
# (x IS NOT NULL AND X < x),
# (x IS NOT NULL AND X = x AND Y > y),
# (x IS NOT NULL AND X = x AND Y = y AND Z < z)]
#
# Note again that when x IS NULL, non_null_conds evaluates to FALSE.
tie_breaking_conds = current_conditions.map do |conditional|
Arel::Nodes::And.new([column_equals_to_value, conditional])
end
# The combined OR condition (null_where_cond OR non_null_where_cond) will return a correct result -
# without having to account for whether x is nil or an actual value at the application level.
non_null_conds = [compare_column_with_value, *tie_breaking_conds].map do |conditional|
Arel::Nodes::And.new([value_is_not_null, conditional])
end
[*null_conds, *non_null_conds]
end
def column_equals_to_value
@equality_node ||= column.column_expression.eq(value)
end
def column_is_null
@column_is_null ||= column.column_expression.eq(nil)
end
def column_is_not_null
@column_is_not_null ||= column.column_expression.not_eq(nil)
end
def value_is_null
@value_is_null ||= build_quoted_value.eq(nil)
end
def value_is_not_null
@value_is_not_null ||= build_quoted_value.not_eq(nil)
end
def compare_column_with_value
if column.descending_order?
column.column_expression.lt(value)
else
column.column_expression.gt(value)
end
end
# Turns the given value to an SQL literal by casting it to the proper format.
def build_quoted_value
return value if value.instance_of?(Arel::Nodes::SqlLiteral)
Arel::Nodes.build_quoted(value, column.column_expression)
end
attr_reader :column, :value
end
end
end
end
...@@ -141,24 +141,10 @@ module Gitlab ...@@ -141,24 +141,10 @@ module Gitlab
return use_composite_row_comparison(values) if composite_row_comparison_possible? return use_composite_row_comparison(values) if composite_row_comparison_possible?
where_values = [] column_definitions
.map { ColumnConditionBuilder.new(_1, values[_1.attribute_name]) }
reversed_column_definitions = column_definitions.reverse .reverse
reversed_column_definitions.each_with_index do |column_definition, i| .reduce([]) { |where_conditions, column| column.where_conditions(where_conditions) }
value = values[column_definition.attribute_name]
conditions_for_column(column_definition, value).each do |condition|
column_definitions_after_index = reversed_column_definitions.last(column_definitions.reverse.size - i - 1)
equal_conditon_for_rest = column_definitions_after_index.map do |definition|
definition.column_expression.eq(values[definition.attribute_name])
end
where_values << Arel::Nodes::Grouping.new(Arel::Nodes::And.new([condition, *equal_conditon_for_rest].compact))
end
end
where_values
end end
def where_values_with_or_query(values) def where_values_with_or_query(values)
...@@ -222,32 +208,6 @@ module Gitlab ...@@ -222,32 +208,6 @@ module Gitlab
scope scope
end end
def conditions_for_column(column_definition, value)
conditions = []
# Depending on the order, build a query condition fragment for taking the next rows
if column_definition.distinct? || (!column_definition.distinct? && value.present?)
conditions << compare_column_with_value(column_definition, value)
end
# When the column is nullable, additional conditions for NULL a NOT NULL values are necessary.
# This depends on the position of the nulls (top or bottom of the resultset).
if column_definition.nulls_first? && value.blank?
conditions << column_definition.column_expression.not_eq(nil)
elsif column_definition.nulls_last? && value.present?
conditions << column_definition.column_expression.eq(nil)
end
conditions
end
def compare_column_with_value(column_definition, value)
if column_definition.descending_order?
column_definition.column_expression.lt(value)
else
column_definition.column_expression.gt(value)
end
end
def build_or_query(expressions) def build_or_query(expressions)
return [] if expressions.blank? return [] if expressions.blank?
......
import { GlLink, GlSprintf } from '@gitlab/ui'; import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mavenPackage, conanPackage, nugetPackage, npmPackage } from 'jest/packages/mock_data'; import {
conanMetadata,
mavenMetadata,
nugetMetadata,
packageData,
} from 'jest/packages_and_registries/package_registry/mock_data';
import component from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import component from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import {
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_NPM,
} from '~/packages_and_registries/package_registry/constants';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
const mavenPackage = { packageType: PACKAGE_TYPE_MAVEN, metadata: mavenMetadata() };
const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() };
const nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() };
const npmPackage = { packageType: PACKAGE_TYPE_NPM, metadata: {} };
describe('Package Additional Metadata', () => { describe('Package Additional Metadata', () => {
let wrapper; let wrapper;
const defaultProps = { const defaultProps = {
packageEntity: { ...mavenPackage }, packageEntity: {
...packageData(mavenPackage),
},
}; };
const mountComponent = (props) => { const mountComponent = (props) => {
wrapper = shallowMount(component, { wrapper = shallowMountExtended(component, {
propsData: { ...defaultProps, ...props }, propsData: { ...defaultProps, ...props },
stubs: { stubs: {
DetailsRow, DetailsRow,
...@@ -25,14 +43,14 @@ describe('Package Additional Metadata', () => { ...@@ -25,14 +43,14 @@ describe('Package Additional Metadata', () => {
wrapper = null; wrapper = null;
}); });
const findTitle = () => wrapper.find('[data-testid="title"]'); const findTitle = () => wrapper.findByTestId('title');
const findMainArea = () => wrapper.find('[data-testid="main"]'); const findMainArea = () => wrapper.findByTestId('main');
const findNugetSource = () => wrapper.find('[data-testid="nuget-source"]'); const findNugetSource = () => wrapper.findByTestId('nuget-source');
const findNugetLicense = () => wrapper.find('[data-testid="nuget-license"]'); const findNugetLicense = () => wrapper.findByTestId('nuget-license');
const findConanRecipe = () => wrapper.find('[data-testid="conan-recipe"]'); const findConanRecipe = () => wrapper.findByTestId('conan-recipe');
const findMavenApp = () => wrapper.find('[data-testid="maven-app"]'); const findMavenApp = () => wrapper.findByTestId('maven-app');
const findMavenGroup = () => wrapper.find('[data-testid="maven-group"]'); const findMavenGroup = () => wrapper.findByTestId('maven-group');
const findElementLink = (container) => container.find(GlLink); const findElementLink = (container) => container.findComponent(GlLink);
it('has the correct title', () => { it('has the correct title', () => {
mountComponent(); mountComponent();
...@@ -43,27 +61,21 @@ describe('Package Additional Metadata', () => { ...@@ -43,27 +61,21 @@ describe('Package Additional Metadata', () => {
expect(title.text()).toBe('Additional Metadata'); expect(title.text()).toBe('Additional Metadata');
}); });
describe.each` it.each`
packageEntity | visible | metadata packageEntity | visible | packageType
${mavenPackage} | ${true} | ${'maven_metadatum'} ${mavenPackage} | ${true} | ${PACKAGE_TYPE_MAVEN}
${conanPackage} | ${true} | ${'conan_metadatum'} ${conanPackage} | ${true} | ${PACKAGE_TYPE_CONAN}
${nugetPackage} | ${true} | ${'nuget_metadatum'} ${nugetPackage} | ${true} | ${PACKAGE_TYPE_NUGET}
${npmPackage} | ${false} | ${null} ${npmPackage} | ${false} | ${PACKAGE_TYPE_NPM}
`('Component visibility', ({ packageEntity, visible, metadata }) => { `(
it(`Is ${visible} that the component markup is visible when the package is ${packageEntity.package_type}`, () => { `It is $visible that the component is visible when the package is $packageType`,
({ packageEntity, visible }) => {
mountComponent({ packageEntity }); mountComponent({ packageEntity });
expect(findTitle().exists()).toBe(visible); expect(findTitle().exists()).toBe(visible);
expect(findMainArea().exists()).toBe(visible); expect(findMainArea().exists()).toBe(visible);
}); },
);
it(`The component is hidden if ${metadata} is missing`, () => {
mountComponent({ packageEntity: { ...packageEntity, [metadata]: null } });
expect(findTitle().exists()).toBe(false);
expect(findMainArea().exists()).toBe(false);
});
});
describe('nuget metadata', () => { describe('nuget metadata', () => {
beforeEach(() => { beforeEach(() => {
...@@ -72,14 +84,14 @@ describe('Package Additional Metadata', () => { ...@@ -72,14 +84,14 @@ describe('Package Additional Metadata', () => {
it.each` it.each`
name | finderFunction | text | link | icon name | finderFunction | text | link | icon
${'source'} | ${findNugetSource} | ${'Source project located at project-foo-url'} | ${'project_url'} | ${'project'} ${'source'} | ${findNugetSource} | ${'Source project located at projectUrl'} | ${'projectUrl'} | ${'project'}
${'license'} | ${findNugetLicense} | ${'License information located at license-foo-url'} | ${'license_url'} | ${'license'} ${'license'} | ${findNugetLicense} | ${'License information located at licenseUrl'} | ${'licenseUrl'} | ${'license'}
`('$name element', ({ finderFunction, text, link, icon }) => { `('$name element', ({ finderFunction, text, link, icon }) => {
const element = finderFunction(); const element = finderFunction();
expect(element.exists()).toBe(true); expect(element.exists()).toBe(true);
expect(element.text()).toBe(text); expect(element.text()).toBe(text);
expect(element.props('icon')).toBe(icon); expect(element.props('icon')).toBe(icon);
expect(findElementLink(element).attributes('href')).toBe(nugetPackage.nuget_metadatum[link]); expect(findElementLink(element).attributes('href')).toBe(nugetPackage.metadata[link]);
}); });
}); });
...@@ -90,7 +102,7 @@ describe('Package Additional Metadata', () => { ...@@ -90,7 +102,7 @@ describe('Package Additional Metadata', () => {
it.each` it.each`
name | finderFunction | text | icon name | finderFunction | text | icon
${'recipe'} | ${findConanRecipe} | ${'Recipe: conan-package/1.0.0@conan+conan-package/stable'} | ${'information-o'} ${'recipe'} | ${findConanRecipe} | ${'Recipe: package-8/1.0.0@gitlab-org+gitlab-test/stable'} | ${'information-o'}
`('$name element', ({ finderFunction, text, icon }) => { `('$name element', ({ finderFunction, text, icon }) => {
const element = finderFunction(); const element = finderFunction();
expect(element.exists()).toBe(true); expect(element.exists()).toBe(true);
...@@ -106,8 +118,8 @@ describe('Package Additional Metadata', () => { ...@@ -106,8 +118,8 @@ describe('Package Additional Metadata', () => {
it.each` it.each`
name | finderFunction | text | icon name | finderFunction | text | icon
${'app'} | ${findMavenApp} | ${'App name: test-app'} | ${'information-o'} ${'app'} | ${findMavenApp} | ${'App name: appName'} | ${'information-o'}
${'group'} | ${findMavenGroup} | ${'App group: com.test.app'} | ${'information-o'} ${'group'} | ${findMavenGroup} | ${'App group: appGroup'} | ${'information-o'}
`('$name element', ({ finderFunction, text, icon }) => { `('$name element', ({ finderFunction, text, icon }) => {
const element = finderFunction(); const element = finderFunction();
expect(element.exists()).toBe(true); expect(element.exists()).toBe(true);
......
...@@ -5,6 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; ...@@ -5,6 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash'; import createFlash from '~/flash';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue'; import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue'; import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue'; import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
...@@ -48,6 +49,7 @@ describe('PackagesApp', () => { ...@@ -48,6 +49,7 @@ describe('PackagesApp', () => {
const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findPackageTitle = () => wrapper.findComponent(PackageTitle); const findPackageTitle = () => wrapper.findComponent(PackageTitle);
const findPackageHistory = () => wrapper.findComponent(PackageHistory); const findPackageHistory = () => wrapper.findComponent(PackageHistory);
const findAdditionalMetadata = () => wrapper.findComponent(AdditionalMetadata);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -95,4 +97,15 @@ describe('PackagesApp', () => { ...@@ -95,4 +97,15 @@ describe('PackagesApp', () => {
projectName: provide.projectName, projectName: provide.projectName,
}); });
}); });
it('renders additional metadata and has the right props', async () => {
createComponent();
await waitForPromises();
expect(findAdditionalMetadata().exists()).toBe(true);
expect(findAdditionalMetadata().props()).toMatchObject({
packageEntity: expect.objectContaining(packageData()),
});
});
}); });
...@@ -58,10 +58,49 @@ export const packageData = (extend) => ({ ...@@ -58,10 +58,49 @@ export const packageData = (extend) => ({
...extend, ...extend,
}); });
export const conanMetadata = () => ({
packageChannel: 'stable',
packageUsername: 'gitlab-org+gitlab-test',
recipe: 'package-8/1.0.0@gitlab-org+gitlab-test/stable',
recipePath: 'package-8/1.0.0/gitlab-org+gitlab-test/stable',
});
export const composerMetadata = () => ({
targetSha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
composerJson: {
license: 'MIT',
version: '1.0.0',
},
});
export const pypyMetadata = () => ({
requiredPython: '1.0.0',
});
export const mavenMetadata = () => ({
appName: 'appName',
appGroup: 'appGroup',
appVersion: 'appVersion',
path: 'path',
});
export const nugetMetadata = () => ({
iconUrl: 'iconUrl',
licenseUrl: 'licenseUrl',
projectUrl: 'projectUrl',
});
export const packageDetailsQuery = () => ({ export const packageDetailsQuery = () => ({
data: { data: {
package: { package: {
...packageData(), ...packageData(),
metadata: {
...conanMetadata(),
...composerMetadata(),
...pypyMetadata(),
...mavenMetadata(),
...nugetMetadata(),
},
tags: { tags: {
nodes: packageTags(), nodes: packageTags(),
__typename: 'PackageTagConnection', __typename: 'PackageTagConnection',
......
...@@ -6,32 +6,67 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do ...@@ -6,32 +6,67 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
describe 'paginate over items correctly' do describe 'paginate over items correctly' do
let(:table) { Arel::Table.new(:my_table) } let(:table) { Arel::Table.new(:my_table) }
let(:order) { nil } let(:order) { nil }
let(:default_limit) { 999 }
let(:query_building_method) { :build_query }
def run_query(query) def run_query(query)
ApplicationRecord.connection.execute(query).to_a ApplicationRecord.connection.execute(query).to_a
end end
def build_query(order:, where_conditions: nil, limit: nil) def where_conditions_as_sql(where_conditions)
"WHERE #{Array(where_conditions).map(&:to_sql).join(' OR ')}"
end
def build_query(order:, where_conditions: [], limit: nil)
where_string = where_conditions_as_sql(where_conditions)
<<-SQL <<-SQL
SELECT id, year, month SELECT id, year, month
FROM (#{table_data}) my_table (id, year, month) FROM (#{table_data}) my_table (id, year, month)
WHERE #{where_conditions || '1=1'} #{where_string if where_conditions.present?}
ORDER BY #{order}
LIMIT #{limit || default_limit};
SQL
end
def build_union_query(order:, where_conditions: [], limit: nil)
return build_query(order: order, where_conditions: where_conditions, limit: limit) if where_conditions.blank?
union_queries = Array(where_conditions).map do |where_condition|
<<-SQL
(SELECT id, year, month
FROM (#{table_data}) my_table (id, year, month)
WHERE #{where_condition.to_sql}
ORDER BY #{order}
LIMIT #{limit || default_limit})
SQL
end
union_query = union_queries.join(" UNION ALL ")
<<-SQL
SELECT id, year, month
FROM (#{union_query}) as my_table
ORDER BY #{order} ORDER BY #{order}
LIMIT #{limit || 999}; LIMIT #{limit || default_limit};
SQL SQL
end end
def cursor_attributes_for_node(node)
order.cursor_attributes_for_node(node)
end
def iterate_and_collect(order:, page_size:, where_conditions: nil) def iterate_and_collect(order:, page_size:, where_conditions: nil)
all_items = [] all_items = []
loop do loop do
paginated_items = run_query(build_query(order: order, where_conditions: where_conditions, limit: page_size)) paginated_items = run_query(send(query_building_method, order: order, where_conditions: where_conditions, limit: page_size))
break if paginated_items.empty? break if paginated_items.empty?
all_items.concat(paginated_items) all_items.concat(paginated_items)
last_item = paginated_items.last last_item = paginated_items.last
cursor_attributes = order.cursor_attributes_for_node(last_item) cursor_attributes = cursor_attributes_for_node(last_item)
where_conditions = order.where_values_with_or_query(cursor_attributes).to_sql where_conditions = order.build_where_values(cursor_attributes)
end end
all_items all_items
...@@ -54,15 +89,41 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do ...@@ -54,15 +89,41 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
it { expect(subject).to eq(expected) } it { expect(subject).to eq(expected) }
end end
context 'when using the conditions in an UNION query' do
let(:query_building_method) { :build_union_query }
it { expect(subject).to eq(expected) }
end
context 'when the cursor attributes are SQL literals' do
def cursor_attributes_for_node(node)
# Simulate the scenario where the cursor attributes are SQL literals
order.cursor_attributes_for_node(node).transform_values.each_with_index do |value, i|
index = i + 1
value_sql = value.nil? ? 'NULL::integer' : value
values = [value_sql] * index
Arel.sql("(ARRAY[#{values.join(',')}])[#{index}]") # example: ARRAY[cursor_value][1] will return cursor_value
end
end
it { expect(subject).to eq(expected) }
context 'when using the conditions in an UNION query' do
let(:query_building_method) { :build_union_query }
it { expect(subject).to eq(expected) }
end
end
end end
context 'when paginating backwards' do context 'when paginating backwards' do
subject do subject do
last_item = expected.last last_item = expected.last
cursor_attributes = order.cursor_attributes_for_node(last_item) cursor_attributes = order.cursor_attributes_for_node(last_item)
where_conditions = order.reversed_order.where_values_with_or_query(cursor_attributes) where_conditions = order.reversed_order.build_where_values(cursor_attributes)
iterate_and_collect(order: order.reversed_order, page_size: 2, where_conditions: where_conditions.to_sql) iterate_and_collect(order: order.reversed_order, page_size: 2, where_conditions: where_conditions)
end end
it do it do
...@@ -371,7 +432,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do ...@@ -371,7 +432,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
reversed = order.reversed_order reversed = order.reversed_order
before_conditions = reversed.where_values_with_or_query(before_cursor) before_conditions = reversed.where_values_with_or_query(before_cursor)
query = build_query(order: order, where_conditions: "(#{after_conditions.to_sql}) AND (#{before_conditions.to_sql})", limit: 100) query = build_query(order: order, where_conditions: [Arel::Nodes::And.new([after_conditions, before_conditions])], limit: 100)
expect(run_query(query)).to eq([ expect(run_query(query)).to eq([
{ "id" => 2, "year" => 2011, "month" => 0 }, { "id" => 2, "year" => 2011, "month" => 0 },
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment