Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5bd4297fd7
commit
af28a89d5e
|
@ -854,10 +854,6 @@ Rails/SaveBang:
|
|||
Exclude:
|
||||
- 'ee/spec/controllers/projects/merge_requests_controller_spec.rb'
|
||||
- 'ee/spec/controllers/subscriptions_controller_spec.rb'
|
||||
- 'ee/spec/factories/ci/job_artifacts.rb'
|
||||
- 'ee/spec/factories/epics.rb'
|
||||
- 'ee/spec/factories/licenses.rb'
|
||||
- 'ee/spec/factories/merge_requests.rb'
|
||||
- 'ee/spec/features/admin/admin_users_spec.rb'
|
||||
- 'ee/spec/features/admin/geo/admin_geo_nodes_spec.rb'
|
||||
- 'ee/spec/features/admin/licenses/admin_views_license_spec.rb'
|
||||
|
@ -1099,19 +1095,6 @@ Rails/SaveBang:
|
|||
- 'spec/controllers/sent_notifications_controller_spec.rb'
|
||||
- 'spec/controllers/sessions_controller_spec.rb'
|
||||
- 'spec/controllers/users_controller_spec.rb'
|
||||
- 'spec/factories/alert_management/alerts.rb'
|
||||
- 'spec/factories/boards.rb'
|
||||
- 'spec/factories/ci/pipelines.rb'
|
||||
- 'spec/factories/design_management/designs.rb'
|
||||
- 'spec/factories/design_management/versions.rb'
|
||||
- 'spec/factories/emails.rb'
|
||||
- 'spec/factories/issues.rb'
|
||||
- 'spec/factories/labels.rb'
|
||||
- 'spec/factories/merge_requests.rb'
|
||||
- 'spec/factories/plans.rb'
|
||||
- 'spec/factories/projects.rb'
|
||||
- 'spec/factories/services.rb'
|
||||
- 'spec/factories/wiki_pages.rb'
|
||||
- 'spec/factories_spec.rb'
|
||||
- 'spec/features/admin/admin_appearance_spec.rb'
|
||||
- 'spec/features/admin/admin_labels_spec.rb'
|
||||
|
|
|
@ -1 +1 @@
|
|||
521bb978da8780aca690136e78a3ad388726c8ad
|
||||
d3caef18a88838486d64a427b00c40ac70f5c378
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
<script>
|
||||
import { GlAvatar, GlTooltipDirective, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { __, s__ } from '~/locale';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
|
||||
export default {
|
||||
name: 'PackageActivity',
|
||||
components: {
|
||||
ClipboardButton,
|
||||
GlAvatar,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
data() {
|
||||
return {
|
||||
showDescription: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['packageEntity']),
|
||||
...mapGetters(['packagePipeline']),
|
||||
publishedDate() {
|
||||
return formatDate(this.packageEntity.created_at, 'HH:MM yyyy-mm-dd');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleShowDescription() {
|
||||
this.showDescription = !this.showDescription;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
showCommit: __('Show commit description'),
|
||||
pipelineText: s__(
|
||||
'PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}',
|
||||
),
|
||||
publishText: s__('PackageRegistry|Published to the repository at %{timestamp}'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<h3 class="gl-font-lg">{{ __('Activity') }}</h3>
|
||||
|
||||
<div ref="commit-info" class="info-well">
|
||||
<div v-if="packagePipeline" class="well-segment">
|
||||
<div class="d-flex align-items-center">
|
||||
<gl-icon name="commit" class="d-none d-sm-block" />
|
||||
|
||||
<button
|
||||
v-if="packagePipeline.git_commit_message"
|
||||
ref="commit-message-toggle"
|
||||
v-gl-tooltip
|
||||
:title="$options.i18n.showCommit"
|
||||
:aria-label="$options.i18n.showCommit"
|
||||
class="text-expander mr-2 d-none d-sm-flex"
|
||||
type="button"
|
||||
@click="toggleShowDescription"
|
||||
>
|
||||
<gl-icon name="ellipsis_h" :size="12" />
|
||||
</button>
|
||||
|
||||
<gl-link :href="`../../commit/${packagePipeline.sha}`">{{ packagePipeline.sha }}</gl-link>
|
||||
|
||||
<clipboard-button
|
||||
:text="packagePipeline.sha"
|
||||
:title="__('Copy commit SHA')"
|
||||
css-class="border-0 text-secondary py-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="showDescription" ref="commit-message" class="mt-2 d-none d-sm-block">
|
||||
<pre class="commit-row-description mb-0 pl-2">{{
|
||||
packagePipeline.git_commit_message
|
||||
}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="packagePipeline" ref="pipeline-info" class="well-segment">
|
||||
<div class="d-flex align-items-center">
|
||||
<gl-icon name="pipeline" class="mr-2 d-none d-sm-block" />
|
||||
|
||||
<gl-sprintf :message="$options.i18n.pipelineText">
|
||||
<template #link>
|
||||
|
||||
<gl-link :href="`../../pipelines/${packagePipeline.id}`"
|
||||
>#{{ packagePipeline.id }}</gl-link
|
||||
>
|
||||
|
||||
</template>
|
||||
|
||||
<template #timestamp>
|
||||
<span v-gl-tooltip :title="tooltipTitle(packagePipeline.created_at)">
|
||||
{{ timeFormatted(packagePipeline.created_at) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #author
|
||||
>{{ packagePipeline.user.name }}
|
||||
<gl-avatar
|
||||
class="ml-2 d-none d-sm-block"
|
||||
:src="packagePipeline.user.avatar_url"
|
||||
:size="24"
|
||||
/></template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="well-segment d-flex align-items-center">
|
||||
<gl-icon name="clock" class="mr-2 d-none d-sm-block" />
|
||||
|
||||
<gl-sprintf :message="$options.i18n.publishText">
|
||||
<template #timestamp>
|
||||
{{ publishedDate }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,343 @@
|
|||
<script>
|
||||
import {
|
||||
GlBadge,
|
||||
GlDeprecatedButton,
|
||||
GlIcon,
|
||||
GlModal,
|
||||
GlModalDirective,
|
||||
GlTooltipDirective,
|
||||
GlLink,
|
||||
GlEmptyState,
|
||||
GlTab,
|
||||
GlTabs,
|
||||
GlTable,
|
||||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import PackageActivity from './activity.vue';
|
||||
import PackageInformation from './information.vue';
|
||||
import PackageTitle from './package_title.vue';
|
||||
import ConanInstallation from './conan_installation.vue';
|
||||
import MavenInstallation from './maven_installation.vue';
|
||||
import NpmInstallation from './npm_installation.vue';
|
||||
import NugetInstallation from './nuget_installation.vue';
|
||||
import PypiInstallation from './pypi_installation.vue';
|
||||
import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
|
||||
import PackageListRow from '../../shared/components/package_list_row.vue';
|
||||
import DependencyRow from './dependency_row.vue';
|
||||
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import { generatePackageInfo } from '../utils';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { PackageType, TrackingActions } from '../../shared/constants';
|
||||
import { packageTypeToTrackCategory } from '../../shared/utils';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'PackagesApp',
|
||||
components: {
|
||||
GlBadge,
|
||||
GlDeprecatedButton,
|
||||
GlEmptyState,
|
||||
GlLink,
|
||||
GlModal,
|
||||
GlTab,
|
||||
GlTabs,
|
||||
GlTable,
|
||||
GlIcon,
|
||||
GlSprintf,
|
||||
PackageActivity,
|
||||
PackageInformation,
|
||||
PackageTitle,
|
||||
ConanInstallation,
|
||||
MavenInstallation,
|
||||
NpmInstallation,
|
||||
NugetInstallation,
|
||||
PypiInstallation,
|
||||
PackagesListLoader,
|
||||
PackageListRow,
|
||||
DependencyRow,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
mixins: [timeagoMixin, Tracking.mixin()],
|
||||
trackingActions: { ...TrackingActions },
|
||||
computed: {
|
||||
...mapState([
|
||||
'packageEntity',
|
||||
'packageFiles',
|
||||
'isLoading',
|
||||
'canDelete',
|
||||
'destroyPath',
|
||||
'svgPath',
|
||||
'npmPath',
|
||||
'npmHelpPath',
|
||||
]),
|
||||
installationComponent() {
|
||||
switch (this.packageEntity.package_type) {
|
||||
case PackageType.CONAN:
|
||||
return ConanInstallation;
|
||||
case PackageType.MAVEN:
|
||||
return MavenInstallation;
|
||||
case PackageType.NPM:
|
||||
return NpmInstallation;
|
||||
case PackageType.NUGET:
|
||||
return NugetInstallation;
|
||||
case PackageType.PYPI:
|
||||
return PypiInstallation;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
isValidPackage() {
|
||||
return Boolean(this.packageEntity.name);
|
||||
},
|
||||
canDeletePackage() {
|
||||
return this.canDelete && this.destroyPath;
|
||||
},
|
||||
packageInformation() {
|
||||
return generatePackageInfo(this.packageEntity);
|
||||
},
|
||||
packageMetadataTitle() {
|
||||
switch (this.packageEntity.package_type) {
|
||||
case PackageType.MAVEN:
|
||||
return s__('Maven Metadata');
|
||||
default:
|
||||
return s__('Package information');
|
||||
}
|
||||
},
|
||||
packageMetadata() {
|
||||
switch (this.packageEntity.package_type) {
|
||||
case PackageType.MAVEN:
|
||||
return [
|
||||
{
|
||||
label: s__('Group ID'),
|
||||
value: this.packageEntity.maven_metadatum.app_group,
|
||||
},
|
||||
{
|
||||
label: s__('Artifact ID'),
|
||||
value: this.packageEntity.maven_metadatum.app_name,
|
||||
},
|
||||
{
|
||||
label: s__('Version'),
|
||||
value: this.packageEntity.maven_metadatum.app_version,
|
||||
},
|
||||
];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
filesTableRows() {
|
||||
return this.packageFiles.map(x => ({
|
||||
name: x.file_name,
|
||||
downloadPath: x.download_path,
|
||||
size: this.formatSize(x.size),
|
||||
created: x.created_at,
|
||||
}));
|
||||
},
|
||||
tracking() {
|
||||
return {
|
||||
category: packageTypeToTrackCategory(this.packageEntity.package_type),
|
||||
};
|
||||
},
|
||||
hasVersions() {
|
||||
return this.packageEntity.versions?.length > 0;
|
||||
},
|
||||
packageDependencies() {
|
||||
return this.packageEntity.dependency_links || [];
|
||||
},
|
||||
showDependencies() {
|
||||
return this.packageEntity.package_type === PackageType.NUGET;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchPackageVersions']),
|
||||
formatSize(size) {
|
||||
return numberToHumanSize(size);
|
||||
},
|
||||
cancelDelete() {
|
||||
this.$refs.deleteModal.hide();
|
||||
},
|
||||
getPackageVersions() {
|
||||
if (!this.packageEntity.versions) {
|
||||
this.fetchPackageVersions();
|
||||
}
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
deleteModalTitle: s__(`PackageRegistry|Delete Package Version`),
|
||||
deleteModalContent: s__(
|
||||
`PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`,
|
||||
),
|
||||
},
|
||||
filesTableHeaderFields: [
|
||||
{
|
||||
key: 'name',
|
||||
label: __('Name'),
|
||||
tdClass: 'd-flex align-items-center',
|
||||
},
|
||||
{
|
||||
key: 'size',
|
||||
label: __('Size'),
|
||||
},
|
||||
{
|
||||
key: 'created',
|
||||
label: __('Created'),
|
||||
class: 'text-right',
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-empty-state
|
||||
v-if="!isValidPackage"
|
||||
:title="s__('PackageRegistry|Unable to load package')"
|
||||
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
|
||||
:svg-path="svgPath"
|
||||
/>
|
||||
|
||||
<div v-else class="packages-app">
|
||||
<div class="detail-page-header d-flex justify-content-between flex-column flex-sm-row">
|
||||
<package-title />
|
||||
|
||||
<div class="mt-sm-2">
|
||||
<gl-deprecated-button
|
||||
v-if="canDeletePackage"
|
||||
v-gl-modal="'delete-modal'"
|
||||
class="js-delete-button"
|
||||
variant="danger"
|
||||
data-qa-selector="delete_button"
|
||||
>{{ __('Delete') }}</gl-deprecated-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gl-tabs>
|
||||
<gl-tab :title="__('Detail')">
|
||||
<div class="row" data-qa-selector="package_information_content">
|
||||
<div class="col-sm-6">
|
||||
<package-information :information="packageInformation" />
|
||||
<package-information
|
||||
v-if="packageMetadata"
|
||||
:heading="packageMetadataTitle"
|
||||
:information="packageMetadata"
|
||||
:show-copy="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<component
|
||||
:is="installationComponent"
|
||||
v-if="installationComponent"
|
||||
:name="packageEntity.name"
|
||||
:registry-url="npmPath"
|
||||
:help-url="npmHelpPath"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<package-activity />
|
||||
|
||||
<gl-table
|
||||
:fields="$options.filesTableHeaderFields"
|
||||
:items="filesTableRows"
|
||||
tbody-tr-class="js-file-row"
|
||||
>
|
||||
<template #cell(name)="items">
|
||||
<gl-icon name="doc-code" class="space-right" />
|
||||
<gl-link
|
||||
:href="items.item.downloadPath"
|
||||
class="js-file-download"
|
||||
@click="track($options.trackingActions.PULL_PACKAGE)"
|
||||
>
|
||||
{{ items.item.name }}
|
||||
</gl-link>
|
||||
</template>
|
||||
|
||||
<template #cell(created)="items">
|
||||
<span v-gl-tooltip :title="tooltipTitle(items.item.created)">{{
|
||||
timeFormatted(items.item.created)
|
||||
}}</span>
|
||||
</template>
|
||||
</gl-table>
|
||||
</gl-tab>
|
||||
|
||||
<gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab">
|
||||
<template #title>
|
||||
<span>{{ __('Dependencies') }}</span>
|
||||
<gl-badge size="sm" data-testid="dependencies-badge">{{
|
||||
packageDependencies.length
|
||||
}}</gl-badge>
|
||||
</template>
|
||||
|
||||
<template v-if="packageDependencies.length > 0">
|
||||
<dependency-row
|
||||
v-for="(dep, index) in packageDependencies"
|
||||
:key="index"
|
||||
:dependency="dep"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<p v-else class="gl-mt-3" data-testid="no-dependencies-message">
|
||||
{{ s__('PackageRegistry|This NuGet package has no dependencies.') }}
|
||||
</p>
|
||||
</gl-tab>
|
||||
|
||||
<gl-tab
|
||||
:title="__('Versions')"
|
||||
title-item-class="js-versions-tab"
|
||||
@click="getPackageVersions"
|
||||
>
|
||||
<template v-if="isLoading && !hasVersions">
|
||||
<packages-list-loader />
|
||||
</template>
|
||||
|
||||
<template v-else-if="hasVersions">
|
||||
<package-list-row
|
||||
v-for="v in packageEntity.versions"
|
||||
:key="v.id"
|
||||
:package-entity="{ name: packageEntity.name, ...v }"
|
||||
:package-link="v.id.toString()"
|
||||
:disable-delete="true"
|
||||
:show-package-type="false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<p v-else class="gl-mt-3" data-testid="no-versions-message">
|
||||
{{ s__('PackageRegistry|There are no other versions of this package.') }}
|
||||
</p>
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
|
||||
<gl-modal ref="deleteModal" class="js-delete-modal" modal-id="delete-modal">
|
||||
<template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
|
||||
<gl-sprintf :message="$options.i18n.deleteModalContent">
|
||||
<template #version>
|
||||
<strong>{{ packageEntity.version }}</strong>
|
||||
</template>
|
||||
|
||||
<template #name>
|
||||
<strong>{{ packageEntity.name }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
|
||||
<div slot="modal-footer" class="w-100">
|
||||
<div class="float-right">
|
||||
<gl-deprecated-button @click="cancelDelete()">{{ __('Cancel') }}</gl-deprecated-button>
|
||||
<gl-deprecated-button
|
||||
ref="modal-delete-button"
|
||||
data-method="delete"
|
||||
:to="destroyPath"
|
||||
variant="danger"
|
||||
data-qa-selector="delete_modal_button"
|
||||
@click="track($options.trackingActions.DELETE_PACKAGE)"
|
||||
>{{ __('Delete') }}</gl-deprecated-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import Tracking from '~/tracking';
|
||||
import { TrackingLabels } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'CodeInstruction',
|
||||
components: {
|
||||
ClipboardButton,
|
||||
},
|
||||
mixins: [
|
||||
Tracking.mixin({
|
||||
label: TrackingLabels.CODE_INSTRUCTION,
|
||||
}),
|
||||
],
|
||||
props: {
|
||||
instruction: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
copyText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
multiline: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
trackingAction: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
trackCopy() {
|
||||
if (this.trackingAction) {
|
||||
this.track(this.trackingAction);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!multiline" class="input-group append-bottom-10">
|
||||
<input
|
||||
:value="instruction"
|
||||
type="text"
|
||||
class="form-control monospace js-instruction-input"
|
||||
readonly
|
||||
@copy="trackCopy"
|
||||
/>
|
||||
<span class="input-group-append js-instruction-button" @click="trackCopy">
|
||||
<clipboard-button :text="instruction" :title="copyText" class="input-group-text" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<pre class="js-instruction-pre" @copy="trackCopy">{{ instruction }}</pre>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import { GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import CodeInstruction from './code_instruction.vue';
|
||||
import { TrackingActions, TrackingLabels } from '../constants';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import InstallationTabs from './installation_tabs.vue';
|
||||
|
||||
export default {
|
||||
name: 'ConanInstallation',
|
||||
components: {
|
||||
CodeInstruction,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
InstallationTabs,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['conanHelpPath']),
|
||||
...mapGetters(['conanInstallationCommand', 'conanSetupCommand']),
|
||||
},
|
||||
i18n: {
|
||||
helpText: s__(
|
||||
'PackageRegistry|For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}.',
|
||||
),
|
||||
},
|
||||
trackingActions: { ...TrackingActions },
|
||||
trackingLabel: TrackingLabels.CONAN_INSTALLATION,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<installation-tabs :tracking-label="$options.trackingLabel">
|
||||
<template #installation>
|
||||
<p class="gl-mt-3 font-weight-bold">{{ s__('PackageRegistry|Conan Command') }}</p>
|
||||
<code-instruction
|
||||
:instruction="conanInstallationCommand"
|
||||
:copy-text="s__('PackageRegistry|Copy Conan Command')"
|
||||
class="js-conan-command"
|
||||
:tracking-action="$options.trackingActions.COPY_CONAN_COMMAND"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #setup>
|
||||
<p class="gl-mt-3 font-weight-bold">
|
||||
{{ s__('PackageRegistry|Add Conan Remote') }}
|
||||
</p>
|
||||
<code-instruction
|
||||
:instruction="conanSetupCommand"
|
||||
:copy-text="s__('PackageRegistry|Copy Conan Setup Command')"
|
||||
class="js-conan-setup"
|
||||
:tracking-action="$options.trackingActions.COPY_CONAN_SETUP_COMMAND"
|
||||
/>
|
||||
<gl-sprintf :message="$options.i18n.helpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="conanHelpPath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</installation-tabs>
|
||||
</template>
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'DependencyRow',
|
||||
props: {
|
||||
dependency: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showVersion() {
|
||||
return Boolean(this.dependency.version_pattern);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-responsive-table-row">
|
||||
<div class="table-section section-50">
|
||||
<strong class="gl-text-black-normal">{{ dependency.name }}</strong>
|
||||
<span v-if="dependency.target_framework" data-testid="target-framework"
|
||||
>({{ dependency.target_framework }})</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showVersion"
|
||||
class="table-section section-50 gl-display-flex justify-content-md-end"
|
||||
data-testid="version-pattern"
|
||||
>
|
||||
<span class="gl-text-black-normal">{{ dependency.version_pattern }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,64 @@
|
|||
<script>
|
||||
import { s__ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { GlLink } from '@gitlab/ui';
|
||||
import { InformationType } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'PackageInformation',
|
||||
components: {
|
||||
ClipboardButton,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
heading: {
|
||||
type: String,
|
||||
default: s__('Package information'),
|
||||
required: false,
|
||||
},
|
||||
information: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
showCopy: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
informationType: InformationType,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<strong>{{ heading }}</strong>
|
||||
</div>
|
||||
|
||||
<ul class="content-list">
|
||||
<li v-for="(item, index) in information" :key="index">
|
||||
<span class="text-secondary">{{ item.label }}</span>
|
||||
<div class="pull-right w-75 gl-text-right">
|
||||
<gl-link
|
||||
v-if="item.type === $options.informationType.LINK"
|
||||
:href="item.value"
|
||||
target="_blank"
|
||||
>
|
||||
{{ item.value }}
|
||||
</gl-link>
|
||||
|
||||
<span v-else>{{ item.value }}</span>
|
||||
|
||||
<clipboard-button
|
||||
v-if="showCopy"
|
||||
:text="item.value"
|
||||
:title="sprintf(__('Copy %{field}'), { field: item.label })"
|
||||
css-class="border-0 text-secondary py-0"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import Tracking from '~/tracking';
|
||||
import { trackInstallationTabChange } from '../utils';
|
||||
import { GlTab, GlTabs } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
name: 'TabWrapper',
|
||||
components: {
|
||||
GlTab,
|
||||
GlTabs,
|
||||
},
|
||||
mixins: [Tracking.mixin(), trackInstallationTabChange],
|
||||
props: {
|
||||
trackingLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-mb-3">
|
||||
<gl-tabs @input="trackInstallationTabChange">
|
||||
<gl-tab :title="s__('PackageRegistry|Installation')" title-item-class="js-installation-tab">
|
||||
<div class="gl-ml-3 gl-mr-3">
|
||||
<slot name="installation"></slot>
|
||||
</div>
|
||||
</gl-tab>
|
||||
<gl-tab :title="s__('PackageRegistry|Registry Setup')" title-item-class="js-setup-tab">
|
||||
<div class="gl-ml-3 gl-mr-3">
|
||||
<slot name="setup"></slot>
|
||||
</div>
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,89 @@
|
|||
<script>
|
||||
import { GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import CodeInstruction from './code_instruction.vue';
|
||||
import { TrackingActions, TrackingLabels } from '../constants';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import InstallationTabs from './installation_tabs.vue';
|
||||
|
||||
export default {
|
||||
name: 'MavenInstallation',
|
||||
components: {
|
||||
CodeInstruction,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
InstallationTabs,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['mavenHelpPath']),
|
||||
...mapGetters(['mavenInstallationXml', 'mavenInstallationCommand', 'mavenSetupXml']),
|
||||
},
|
||||
i18n: {
|
||||
xmlText: s__(
|
||||
`PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block.`,
|
||||
),
|
||||
setupText: s__(
|
||||
`PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file.`,
|
||||
),
|
||||
helpText: s__(
|
||||
'PackageRegistry|For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}.',
|
||||
),
|
||||
},
|
||||
trackingActions: { ...TrackingActions },
|
||||
trackingLabel: TrackingLabels.MAVEN_INSTALLATION,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<installation-tabs :tracking-label="$options.trackingLabel">
|
||||
<template #installation>
|
||||
<p class="gl-mt-3 font-weight-bold">{{ s__('PackageRegistry|Maven XML') }}</p>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.xmlText">
|
||||
<template #code="{ content }">
|
||||
<code>{{ content }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<code-instruction
|
||||
:instruction="mavenInstallationXml"
|
||||
:copy-text="s__('PackageRegistry|Copy Maven XML')"
|
||||
class="js-maven-xml"
|
||||
multiline
|
||||
:tracking-action="$options.trackingActions.COPY_MAVEN_XML"
|
||||
/>
|
||||
|
||||
<p class="gl-mt-3 font-weight-bold">
|
||||
{{ s__('PackageRegistry|Maven Command') }}
|
||||
</p>
|
||||
<code-instruction
|
||||
:instruction="mavenInstallationCommand"
|
||||
:copy-text="s__('PackageRegistry|Copy Maven command')"
|
||||
class="js-maven-command"
|
||||
:tracking-action="$options.trackingActions.COPY_MAVEN_COMMAND"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #setup>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.setupText">
|
||||
<template #code="{ content }">
|
||||
<code>{{ content }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<code-instruction
|
||||
:instruction="mavenSetupXml"
|
||||
:copy-text="s__('PackageRegistry|Copy Maven registry XML')"
|
||||
class="js-maven-setup-xml"
|
||||
multiline
|
||||
:tracking-action="$options.trackingActions.COPY_MAVEN_SETUP"
|
||||
/>
|
||||
<gl-sprintf :message="$options.i18n.helpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="mavenHelpPath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</installation-tabs>
|
||||
</template>
|
|
@ -0,0 +1,87 @@
|
|||
<script>
|
||||
import { GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import CodeInstruction from './code_instruction.vue';
|
||||
import { NpmManager, TrackingActions, TrackingLabels } from '../constants';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import InstallationTabs from './installation_tabs.vue';
|
||||
|
||||
export default {
|
||||
name: 'NpmInstallation',
|
||||
components: {
|
||||
CodeInstruction,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
InstallationTabs,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['npmHelpPath']),
|
||||
...mapGetters(['npmInstallationCommand', 'npmSetupCommand']),
|
||||
npmCommand() {
|
||||
return this.npmInstallationCommand(NpmManager.NPM);
|
||||
},
|
||||
npmSetup() {
|
||||
return this.npmSetupCommand(NpmManager.NPM);
|
||||
},
|
||||
yarnCommand() {
|
||||
return this.npmInstallationCommand(NpmManager.YARN);
|
||||
},
|
||||
yarnSetupCommand() {
|
||||
return this.npmSetupCommand(NpmManager.YARN);
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
helpText: s__(
|
||||
'PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more.',
|
||||
),
|
||||
},
|
||||
trackingActions: { ...TrackingActions },
|
||||
trackingLabel: TrackingLabels.NPM_INSTALLATION,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<installation-tabs :tracking-label="$options.trackingLabel">
|
||||
<template #installation>
|
||||
<p class="gl-mt-3 font-weight-bold">{{ s__('PackageRegistry|npm') }}</p>
|
||||
<code-instruction
|
||||
:instruction="npmCommand"
|
||||
:copy-text="s__('PackageRegistry|Copy npm command')"
|
||||
class="js-npm-install"
|
||||
:tracking-action="$options.trackingActions.COPY_NPM_INSTALL_COMMAND"
|
||||
/>
|
||||
|
||||
<p class="gl-mt-3 font-weight-bold">{{ s__('PackageRegistry|yarn') }}</p>
|
||||
<code-instruction
|
||||
:instruction="yarnCommand"
|
||||
:copy-text="s__('PackageRegistry|Copy yarn command')"
|
||||
class="js-yarn-install"
|
||||
:tracking-action="$options.trackingActions.COPY_YARN_INSTALL_COMMAND"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #setup>
|
||||
<p class="gl-mt-3 font-weight-bold">{{ s__('PackageRegistry|npm') }}</p>
|
||||
<code-instruction
|
||||
:instruction="npmSetup"
|
||||
:copy-text="s__('PackageRegistry|Copy npm setup command')"
|
||||
class="js-npm-setup"
|
||||
:tracking-action="$options.trackingActions.COPY_NPM_SETUP_COMMAND"
|
||||
/>
|
||||
|
||||
<p class="gl-mt-3 font-weight-bold">{{ s__('PackageRegistry|yarn') }}</p>
|
||||
<code-instruction
|
||||
:instruction="yarnSetupCommand"
|
||||
:copy-text="s__('PackageRegistry|Copy yarn setup command')"
|
||||
class="js-yarn-setup"
|
||||
:tracking-action="$options.trackingActions.COPY_YARN_SETUP_COMMAND"
|
||||
/>
|
||||
|
||||
<gl-sprintf :message="$options.i18n.helpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="npmHelpPath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</installation-tabs>
|
||||
</template>
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import { GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import CodeInstruction from './code_instruction.vue';
|
||||
import { TrackingActions, TrackingLabels } from '../constants';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import InstallationTabs from './installation_tabs.vue';
|
||||
|
||||
export default {
|
||||
name: 'NugetInstallation',
|
||||
components: {
|
||||
CodeInstruction,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
InstallationTabs,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['nugetHelpPath']),
|
||||
...mapGetters(['nugetInstallationCommand', 'nugetSetupCommand']),
|
||||
},
|
||||
i18n: {
|
||||
helpText: s__(
|
||||
'PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}.',
|
||||
),
|
||||
},
|
||||
trackingActions: { ...TrackingActions },
|
||||
trackingLabel: TrackingLabels.NUGET_INSTALLATION,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<installation-tabs :tracking-label="$options.trackingLabel">
|
||||
<template #installation>
|
||||
<p class="gl-mt-3 font-weight-bold">{{ s__('PackageRegistry|NuGet Command') }}</p>
|
||||
<code-instruction
|
||||
:instruction="nugetInstallationCommand"
|
||||
:copy-text="s__('PackageRegistry|Copy NuGet Command')"
|
||||
class="js-nuget-command"
|
||||
:tracking-action="$options.trackingActions.COPY_NUGET_INSTALL_COMMAND"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #setup>
|
||||
<p class="gl-mt-3 font-weight-bold">
|
||||
{{ s__('PackageRegistry|Add NuGet Source') }}
|
||||
</p>
|
||||
<code-instruction
|
||||
:instruction="nugetSetupCommand"
|
||||
:copy-text="s__('PackageRegistry|Copy NuGet Setup Command')"
|
||||
class="js-nuget-setup"
|
||||
:tracking-action="$options.trackingActions.COPY_NUGET_SETUP_COMMAND"
|
||||
/>
|
||||
<gl-sprintf :message="$options.i18n.helpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="nugetHelpPath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</installation-tabs>
|
||||
</template>
|
|
@ -0,0 +1,112 @@
|
|||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import { GlAvatar, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
|
||||
import PackageTags from '../../shared/components/package_tags.vue';
|
||||
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'PackageTitle',
|
||||
components: {
|
||||
GlAvatar,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
PackageTags,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
computed: {
|
||||
...mapState(['packageEntity', 'packageFiles']),
|
||||
...mapGetters(['packageTypeDisplay', 'packagePipeline', 'packageIcon']),
|
||||
hasTagsToDisplay() {
|
||||
return Boolean(this.packageEntity.tags && this.packageEntity.tags.length);
|
||||
},
|
||||
totalSize() {
|
||||
return numberToHumanSize(this.packageFiles.reduce((acc, p) => acc + p.size, 0));
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
packageInfo: __('v%{version} published %{timeAgo}'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-flex-direction-column">
|
||||
<div class="gl-display-flex">
|
||||
<gl-avatar
|
||||
v-if="packageIcon"
|
||||
:src="packageIcon"
|
||||
shape="rect"
|
||||
class="gl-align-self-center gl-mr-4"
|
||||
data-testid="package-icon"
|
||||
/>
|
||||
|
||||
<div class="gl-display-flex gl-flex-direction-column">
|
||||
<h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2">
|
||||
{{ packageEntity.name }}
|
||||
</h1>
|
||||
|
||||
<div class="gl-display-flex gl-align-items-center gl-text-gray-700">
|
||||
<gl-icon name="eye" class="gl-mr-3" />
|
||||
<gl-sprintf :message="$options.i18n.packageInfo">
|
||||
<template #version>
|
||||
{{ packageEntity.version }}
|
||||
</template>
|
||||
|
||||
<template #timeAgo>
|
||||
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
|
||||
{{ timeFormatted(packageEntity.created_at) }}
|
||||
</span>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3">
|
||||
<div v-if="packageTypeDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
|
||||
<gl-icon name="package" class="gl-text-gray-700 gl-mr-3" />
|
||||
<span data-testid="package-type" class="gl-font-weight-bold">{{ packageTypeDisplay }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="hasTagsToDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
|
||||
<package-tags :tag-display-limit="1" :tags="packageEntity.tags" />
|
||||
</div>
|
||||
|
||||
<div v-if="packagePipeline" class="gl-display-flex gl-align-items-center gl-mr-5">
|
||||
<gl-icon name="review-list" class="gl-text-gray-700 gl-mr-3" />
|
||||
<gl-link
|
||||
data-testid="pipeline-project"
|
||||
:href="packagePipeline.project.web_url"
|
||||
class="gl-font-weight-bold text-truncate"
|
||||
>
|
||||
{{ packagePipeline.project.name }}
|
||||
</gl-link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="packagePipeline"
|
||||
data-testid="package-ref"
|
||||
class="gl-display-flex gl-align-items-center gl-mr-5"
|
||||
>
|
||||
<gl-icon name="branch" class="gl-text-gray-700 gl-mr-3" />
|
||||
<span
|
||||
v-gl-tooltip
|
||||
class="gl-font-weight-bold text-truncate mw-xs"
|
||||
:title="packagePipeline.ref"
|
||||
>{{ packagePipeline.ref }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="gl-display-flex gl-align-items-center gl-mr-5">
|
||||
<gl-icon name="disk" class="gl-text-gray-700 gl-mr-3" />
|
||||
<span data-testid="package-size" class="gl-font-weight-bold">{{ totalSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import { GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import CodeInstruction from './code_instruction.vue';
|
||||
import { TrackingActions, TrackingLabels } from '../constants';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import InstallationTabs from './installation_tabs.vue';
|
||||
|
||||
export default {
|
||||
name: 'PyPiInstallation',
|
||||
components: {
|
||||
CodeInstruction,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
InstallationTabs,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['pypiHelpPath']),
|
||||
...mapGetters(['pypiPipCommand', 'pypiSetupCommand']),
|
||||
},
|
||||
i18n: {
|
||||
setupText: s__(
|
||||
`PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file.`,
|
||||
),
|
||||
helpText: s__(
|
||||
'PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}.',
|
||||
),
|
||||
},
|
||||
trackingActions: { ...TrackingActions },
|
||||
trackingLabel: TrackingLabels.PYPI_INSTALLATION,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<installation-tabs :tracking-label="$options.trackingLabel">
|
||||
<template #installation>
|
||||
<p class="gl-mt-3 font-weight-bold">
|
||||
{{ s__('PackageRegistry|Pip Command') }}
|
||||
</p>
|
||||
<code-instruction
|
||||
:instruction="pypiPipCommand"
|
||||
:copy-text="s__('PackageRegistry|Copy Pip command')"
|
||||
data-testid="pip-command"
|
||||
:tracking-action="$options.trackingActions.COPY_PIP_INSTALL_COMMAND"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #setup>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.setupText">
|
||||
<template #code="{ content }">
|
||||
<code>{{ content }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<code-instruction
|
||||
:instruction="pypiSetupCommand"
|
||||
:copy-text="s__('PackageRegistry|Copy .pypirc content')"
|
||||
data-testid="pypi-setup-content"
|
||||
multiline
|
||||
:tracking-action="$options.trackingActions.COPY_PYPI_SETUP_COMMAND"
|
||||
/>
|
||||
<gl-sprintf :message="$options.i18n.helpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="pypiHelpPath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</installation-tabs>
|
||||
</template>
|
|
@ -0,0 +1,47 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export const TrackingLabels = {
|
||||
CODE_INSTRUCTION: 'code_instruction',
|
||||
CONAN_INSTALLATION: 'conan_installation',
|
||||
MAVEN_INSTALLATION: 'maven_installation',
|
||||
NPM_INSTALLATION: 'npm_installation',
|
||||
NUGET_INSTALLATION: 'nuget_installation',
|
||||
PYPI_INSTALLATION: 'pypi_installation',
|
||||
};
|
||||
|
||||
export const TrackingActions = {
|
||||
INSTALLATION: 'installation',
|
||||
REGISTRY_SETUP: 'registry_setup',
|
||||
|
||||
COPY_CONAN_COMMAND: 'copy_conan_command',
|
||||
COPY_CONAN_SETUP_COMMAND: 'copy_conan_setup_command',
|
||||
|
||||
COPY_MAVEN_XML: 'copy_maven_xml',
|
||||
COPY_MAVEN_COMMAND: 'copy_maven_command',
|
||||
COPY_MAVEN_SETUP: 'copy_maven_setup_xml',
|
||||
|
||||
COPY_NPM_INSTALL_COMMAND: 'copy_npm_install_command',
|
||||
COPY_NPM_SETUP_COMMAND: 'copy_npm_setup_command',
|
||||
|
||||
COPY_YARN_INSTALL_COMMAND: 'copy_yarn_install_command',
|
||||
COPY_YARN_SETUP_COMMAND: 'copy_yarn_setup_command',
|
||||
|
||||
COPY_NUGET_INSTALL_COMMAND: 'copy_nuget_install_command',
|
||||
COPY_NUGET_SETUP_COMMAND: 'copy_nuget_setup_command',
|
||||
|
||||
COPY_PIP_INSTALL_COMMAND: 'copy_pip_install_command',
|
||||
COPY_PYPI_SETUP_COMMAND: 'copy_pypi_setup_command',
|
||||
};
|
||||
|
||||
export const NpmManager = {
|
||||
NPM: 'npm',
|
||||
YARN: 'yarn',
|
||||
};
|
||||
|
||||
export const FETCH_PACKAGE_VERSIONS_ERROR = s__(
|
||||
'PackageRegistry|Unable to fetch package version information.',
|
||||
);
|
||||
|
||||
export const InformationType = {
|
||||
LINK: 'link',
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
import Vue from 'vue';
|
||||
import PackagesApp from './components/app.vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import createStore from './store';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('#js-vue-packages-detail');
|
||||
const { package: packageJson, canDelete: canDeleteStr, ...rest } = el.dataset;
|
||||
const packageEntity = JSON.parse(packageJson);
|
||||
const canDelete = canDeleteStr === 'true';
|
||||
|
||||
const store = createStore({
|
||||
packageEntity,
|
||||
packageFiles: packageEntity.package_files,
|
||||
canDelete,
|
||||
...rest,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
components: {
|
||||
PackagesApp,
|
||||
},
|
||||
store,
|
||||
render(createElement) {
|
||||
return createElement('packages-app');
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import Api from '~/api';
|
||||
import createFlash from '~/flash';
|
||||
import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export default ({ commit, state }) => {
|
||||
commit(types.SET_LOADING, true);
|
||||
|
||||
const { project_id, id } = state.packageEntity;
|
||||
|
||||
return Api.projectPackage(project_id, id)
|
||||
.then(({ data }) => {
|
||||
if (data.versions) {
|
||||
commit(types.SET_PACKAGE_VERSIONS, data.versions.reverse());
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
createFlash(FETCH_PACKAGE_VERSIONS_ERROR);
|
||||
})
|
||||
.finally(() => {
|
||||
commit(types.SET_LOADING, false);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,106 @@
|
|||
import { generateConanRecipe } from '../utils';
|
||||
import { PackageType } from '../../shared/constants';
|
||||
import { getPackageTypeLabel } from '../../shared/utils';
|
||||
import { NpmManager } from '../constants';
|
||||
|
||||
export const packagePipeline = ({ packageEntity }) => {
|
||||
return packageEntity?.pipeline || null;
|
||||
};
|
||||
|
||||
export const packageTypeDisplay = ({ packageEntity }) => {
|
||||
return getPackageTypeLabel(packageEntity.package_type);
|
||||
};
|
||||
|
||||
export const packageIcon = ({ packageEntity }) => {
|
||||
if (packageEntity.package_type === PackageType.NUGET) {
|
||||
return packageEntity.nuget_metadatum?.icon_url || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const conanInstallationCommand = ({ packageEntity }) => {
|
||||
const recipe = generateConanRecipe(packageEntity);
|
||||
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return `conan install ${recipe} --remote=gitlab`;
|
||||
};
|
||||
|
||||
export const conanSetupCommand = ({ conanPath }) =>
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
`conan remote add gitlab ${conanPath}`;
|
||||
|
||||
export const mavenInstallationXml = ({ packageEntity = {} }) => {
|
||||
const {
|
||||
app_group: appGroup = '',
|
||||
app_name: appName = '',
|
||||
app_version: appVersion = '',
|
||||
} = packageEntity.maven_metadatum;
|
||||
|
||||
return `<dependency>
|
||||
<groupId>${appGroup}</groupId>
|
||||
<artifactId>${appName}</artifactId>
|
||||
<version>${appVersion}</version>
|
||||
</dependency>`;
|
||||
};
|
||||
|
||||
export const mavenInstallationCommand = ({ packageEntity = {} }) => {
|
||||
const {
|
||||
app_group: group = '',
|
||||
app_name: name = '',
|
||||
app_version: version = '',
|
||||
} = packageEntity.maven_metadatum;
|
||||
|
||||
return `mvn dependency:get -Dartifact=${group}:${name}:${version}`;
|
||||
};
|
||||
|
||||
export const mavenSetupXml = ({ mavenPath }) => `<repositories>
|
||||
<repository>
|
||||
<id>gitlab-maven</id>
|
||||
<url>${mavenPath}</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>gitlab-maven</id>
|
||||
<url>${mavenPath}</url>
|
||||
</repository>
|
||||
|
||||
<snapshotRepository>
|
||||
<id>gitlab-maven</id>
|
||||
<url>${mavenPath}</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>`;
|
||||
|
||||
export const npmInstallationCommand = ({ packageEntity }) => (type = NpmManager.NPM) => {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
const instruction = type === NpmManager.NPM ? 'npm i' : 'yarn add';
|
||||
|
||||
return `${instruction} ${packageEntity.name}`;
|
||||
};
|
||||
|
||||
export const npmSetupCommand = ({ packageEntity, npmPath }) => (type = NpmManager.NPM) => {
|
||||
const scope = packageEntity.name.substring(0, packageEntity.name.indexOf('/'));
|
||||
|
||||
if (type === NpmManager.NPM) {
|
||||
return `echo ${scope}:registry=${npmPath} >> .npmrc`;
|
||||
}
|
||||
|
||||
return `echo \\"${scope}:registry\\" \\"${npmPath}\\" >> .yarnrc`;
|
||||
};
|
||||
|
||||
export const nugetInstallationCommand = ({ packageEntity }) =>
|
||||
`nuget install ${packageEntity.name} -Source "GitLab"`;
|
||||
|
||||
export const nugetSetupCommand = ({ nugetPath }) =>
|
||||
`nuget source Add -Name "GitLab" -Source "${nugetPath}" -UserName <your_username> -Password <your_token>`;
|
||||
|
||||
export const pypiPipCommand = ({ pypiPath, packageEntity }) =>
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
`pip install ${packageEntity.name} --index-url ${pypiPath}`;
|
||||
|
||||
export const pypiSetupCommand = ({ pypiSetupPath }) => `[gitlab]
|
||||
repository = ${pypiSetupPath}
|
||||
username = __token__
|
||||
password = <your personal access token>`;
|
|
@ -0,0 +1,20 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import fetchPackageVersions from './actions';
|
||||
import * as getters from './getters';
|
||||
import mutations from './mutations';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default (initialState = {}) =>
|
||||
new Vuex.Store({
|
||||
actions: {
|
||||
fetchPackageVersions,
|
||||
},
|
||||
getters,
|
||||
mutations,
|
||||
state: {
|
||||
isLoading: false,
|
||||
...initialState,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
export const SET_LOADING = 'SET_LOADING';
|
||||
export const SET_PACKAGE_VERSIONS = 'SET_PACKAGE_VERSIONS';
|
|
@ -0,0 +1,14 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_LOADING](state, isLoading) {
|
||||
state.isLoading = isLoading;
|
||||
},
|
||||
|
||||
[types.SET_PACKAGE_VERSIONS](state, versions) {
|
||||
state.packageEntity = {
|
||||
...state.packageEntity,
|
||||
versions,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
import { __ } from '~/locale';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
import { TrackingActions, InformationType } from './constants';
|
||||
import { PackageType } from '../shared/constants';
|
||||
import { orderBy } from 'lodash';
|
||||
|
||||
export const trackInstallationTabChange = {
|
||||
methods: {
|
||||
trackInstallationTabChange(tabIndex) {
|
||||
const action = tabIndex === 0 ? TrackingActions.INSTALLATION : TrackingActions.REGISTRY_SETUP;
|
||||
this.track(action, { label: this.trackingLabel });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function generateConanRecipe(packageEntity = {}) {
|
||||
const {
|
||||
name = '',
|
||||
version = '',
|
||||
conan_metadatum: {
|
||||
package_username: packageUsername = '',
|
||||
package_channel: packageChannel = '',
|
||||
} = {},
|
||||
} = packageEntity;
|
||||
|
||||
return `${name}/${version}@${packageUsername}/${packageChannel}`;
|
||||
}
|
||||
|
||||
export function generatePackageInfo(packageEntity = {}) {
|
||||
const information = [];
|
||||
|
||||
if (packageEntity.package_type === PackageType.CONAN) {
|
||||
information.push({
|
||||
order: 1,
|
||||
label: __('Recipe'),
|
||||
value: generateConanRecipe(packageEntity),
|
||||
});
|
||||
} else {
|
||||
information.push({
|
||||
order: 1,
|
||||
label: __('Name'),
|
||||
value: packageEntity.name || '',
|
||||
});
|
||||
}
|
||||
|
||||
if (packageEntity.package_type === PackageType.NUGET) {
|
||||
const {
|
||||
nuget_metadatum: { project_url: projectUrl, license_url: licenseUrl } = {},
|
||||
} = packageEntity;
|
||||
|
||||
if (projectUrl) {
|
||||
information.push({
|
||||
order: 3,
|
||||
label: __('Project URL'),
|
||||
value: projectUrl,
|
||||
type: InformationType.LINK,
|
||||
});
|
||||
}
|
||||
|
||||
if (licenseUrl) {
|
||||
information.push({
|
||||
order: 4,
|
||||
label: __('License URL'),
|
||||
value: licenseUrl,
|
||||
type: InformationType.LINK,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return orderBy(
|
||||
[
|
||||
...information,
|
||||
{
|
||||
order: 2,
|
||||
label: __('Version'),
|
||||
value: packageEntity.version || '',
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
label: __('Created on'),
|
||||
value: formatDate(packageEntity.created_at),
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
label: __('Updated at'),
|
||||
value: formatDate(packageEntity.updated_at),
|
||||
},
|
||||
],
|
||||
['order'],
|
||||
);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Context:
|
||||
* https://gitlab.com/gitlab-org/gitlab/-/issues/198524
|
||||
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29491
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*
|
||||
* LABEL_NAMES - an array of labels to filter issues in the GraphQL query
|
||||
* WORKFLOW_PREFIX - the prefix for workflow labels
|
||||
* ACCEPTING_CONTRIBUTIONS_TITLE - the accepting contributions label
|
||||
*/
|
||||
export const LABEL_NAMES = ['Package::Coming soon'];
|
||||
const WORKFLOW_PREFIX = 'workflow::';
|
||||
const ACCEPTING_CONTRIBUTIONS_TITLE = 'accepting merge requests';
|
||||
|
||||
const setScoped = (label, scoped) => (label ? { ...label, scoped } : label);
|
||||
|
||||
/**
|
||||
* Finds workflow:: scoped labels and returns the first or null.
|
||||
* @param {Object[]} labels Labels from the issue
|
||||
*/
|
||||
export const findWorkflowLabel = (labels = []) =>
|
||||
labels.find(l => l.title.toLowerCase().includes(WORKFLOW_PREFIX.toLowerCase()));
|
||||
|
||||
/**
|
||||
* Determines if an issue is accepting community contributions by checking if
|
||||
* the "Accepting merge requests" label is present.
|
||||
* @param {Object[]} labels
|
||||
*/
|
||||
export const findAcceptingContributionsLabel = (labels = []) =>
|
||||
labels.find(l => l.title.toLowerCase() === ACCEPTING_CONTRIBUTIONS_TITLE.toLowerCase());
|
||||
|
||||
/**
|
||||
* Formats the GraphQL response into the format that the view template expects.
|
||||
* @param {Object} data GraphQL response
|
||||
*/
|
||||
export const toViewModel = data => {
|
||||
// This just flatterns the issues -> nodes and labels -> nodes hierarchy
|
||||
// into an array of objects.
|
||||
const issues = (data.project?.issues?.nodes || []).map(i => ({
|
||||
...i,
|
||||
labels: (i.labels?.nodes || []).map(node => node),
|
||||
}));
|
||||
|
||||
return issues.map(x => ({
|
||||
...x,
|
||||
labels: [
|
||||
setScoped(findWorkflowLabel(x.labels), true),
|
||||
setScoped(findAcceptingContributionsLabel(x.labels), false),
|
||||
].filter(Boolean),
|
||||
}));
|
||||
};
|
|
@ -0,0 +1,172 @@
|
|||
<script>
|
||||
import {
|
||||
GlAlert,
|
||||
GlEmptyState,
|
||||
GlIcon,
|
||||
GlLabel,
|
||||
GlLink,
|
||||
GlSkeletonLoader,
|
||||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import { TrackingActions } from '../../shared/constants';
|
||||
import { s__ } from '~/locale';
|
||||
import { ApolloQuery } from 'vue-apollo';
|
||||
import comingSoonIssuesQuery from './queries/issues.graphql';
|
||||
import { toViewModel, LABEL_NAMES } from './helpers';
|
||||
|
||||
export default {
|
||||
name: 'ComingSoon',
|
||||
components: {
|
||||
GlAlert,
|
||||
GlEmptyState,
|
||||
GlIcon,
|
||||
GlLabel,
|
||||
GlLink,
|
||||
GlSkeletonLoader,
|
||||
GlSprintf,
|
||||
ApolloQuery,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
illustration: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
suggestedContributionsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
labelNames: LABEL_NAMES,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.track(TrackingActions.COMING_SOON_REQUESTED);
|
||||
},
|
||||
methods: {
|
||||
onIssueLinkClick(issueIid, label) {
|
||||
this.track(TrackingActions.COMING_SOON_LIST, {
|
||||
label,
|
||||
value: issueIid,
|
||||
});
|
||||
},
|
||||
onDocsLinkClick() {
|
||||
this.track(TrackingActions.COMING_SOON_HELP);
|
||||
},
|
||||
},
|
||||
loadingRows: 5,
|
||||
i18n: {
|
||||
alertTitle: s__('PackageRegistry|Upcoming package managers'),
|
||||
alertIntro: s__(
|
||||
"PackageRegistry|Is your favorite package manager missing? We'd love your help in building first-class support for it into GitLab! %{contributionLinkStart}Visit the contribution documentation%{contributionLinkEnd} to learn more about how to build support for new package managers into GitLab. Below is a list of package managers that are on our radar.",
|
||||
),
|
||||
emptyStateTitle: s__('PackageRegistry|No upcoming issues'),
|
||||
emptyStateDescription: s__('PackageRegistry|There are no upcoming issues to display.'),
|
||||
},
|
||||
comingSoonIssuesQuery,
|
||||
toViewModel,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<apollo-query
|
||||
:query="$options.comingSoonIssuesQuery"
|
||||
:variables="variables"
|
||||
:update="$options.toViewModel"
|
||||
>
|
||||
<template #default="{ result: { data }, isLoading }">
|
||||
<div>
|
||||
<gl-alert :title="$options.i18n.alertTitle" :dismissible="false" variant="tip">
|
||||
<gl-sprintf :message="$options.i18n.alertIntro">
|
||||
<template #contributionLink="{ content }">
|
||||
<gl-link
|
||||
:href="suggestedContributionsPath"
|
||||
target="_blank"
|
||||
@click="onDocsLinkClick"
|
||||
>{{ content }}</gl-link
|
||||
>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="gl-display-flex gl-flex-direction-column">
|
||||
<gl-skeleton-loader
|
||||
v-for="index in $options.loadingRows"
|
||||
:key="index"
|
||||
:width="1000"
|
||||
:height="80"
|
||||
preserve-aspect-ratio="xMinYMax meet"
|
||||
>
|
||||
<rect width="700" height="10" x="0" y="16" rx="4" />
|
||||
<rect width="60" height="10" x="0" y="45" rx="4" />
|
||||
<rect width="60" height="10" x="70" y="45" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</div>
|
||||
|
||||
<template v-else-if="data && data.length">
|
||||
<div
|
||||
v-for="issue in data"
|
||||
:key="issue.iid"
|
||||
data-testid="issue-row"
|
||||
class="gl-responsive-table-row gl-flex-direction-column gl-align-items-baseline"
|
||||
>
|
||||
<div class="table-section section-100 gl-white-space-normal text-truncate">
|
||||
<gl-link
|
||||
data-testid="issue-title-link"
|
||||
:href="issue.webUrl"
|
||||
class="gl-text-gray-900 gl-font-weight-bold"
|
||||
@click="onIssueLinkClick(issue.iid, issue.title)"
|
||||
>
|
||||
{{ issue.title }}
|
||||
</gl-link>
|
||||
</div>
|
||||
|
||||
<div class="table-section section-100 gl-white-space-normal mt-md-3">
|
||||
<div class="gl-display-flex gl-text-gray-600">
|
||||
<gl-icon name="issues" class="gl-mr-2" />
|
||||
<gl-link
|
||||
data-testid="issue-id-link"
|
||||
:href="issue.webUrl"
|
||||
class="gl-text-gray-600 gl-mr-5"
|
||||
@click="onIssueLinkClick(issue.iid, issue.title)"
|
||||
>#{{ issue.iid }}</gl-link
|
||||
>
|
||||
|
||||
<div v-if="issue.milestone" class="gl-display-flex gl-align-items-center gl-mr-5">
|
||||
<gl-icon name="clock" class="gl-mr-2" />
|
||||
<span data-testid="milestone">{{ issue.milestone.title }}</span>
|
||||
</div>
|
||||
|
||||
<gl-label
|
||||
v-for="label in issue.labels"
|
||||
:key="label.title"
|
||||
class="gl-mr-3"
|
||||
size="sm"
|
||||
:background-color="label.color"
|
||||
:title="label.title"
|
||||
:scoped="Boolean(label.scoped)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<gl-empty-state v-else :title="$options.i18n.emptyStateTitle" :svg-path="illustration">
|
||||
<template #description>
|
||||
<p>{{ $options.i18n.emptyStateDescription }}</p>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</template>
|
||||
</apollo-query>
|
||||
</template>
|
|
@ -0,0 +1,20 @@
|
|||
query getComingSoonIssues($projectPath: ID!, $labelNames: [String]) {
|
||||
project(fullPath: $projectPath) {
|
||||
issues(state: opened, labelName: $labelNames) {
|
||||
nodes {
|
||||
iid
|
||||
title
|
||||
webUrl
|
||||
labels {
|
||||
nodes {
|
||||
title
|
||||
color
|
||||
}
|
||||
}
|
||||
milestone {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import { GlSearchBoxByClick } from '@gitlab/ui';
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlSearchBoxByClick,
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setFilter']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-search-box-by-click
|
||||
:placeholder="s__('PackageRegistry|Filter by name')"
|
||||
@submit="$emit('filter')"
|
||||
@input="setFilter"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,129 @@
|
|||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import { s__ } from '~/locale';
|
||||
import { TrackingActions } from '../../shared/constants';
|
||||
import { packageTypeToTrackCategory } from '../../shared/utils';
|
||||
import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
|
||||
import PackagesListRow from '../../shared/components/package_list_row.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlPagination,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
PackagesListLoader,
|
||||
PackagesListRow,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
data() {
|
||||
return {
|
||||
itemToBeDeleted: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
perPage: state => state.pagination.perPage,
|
||||
totalItems: state => state.pagination.total,
|
||||
page: state => state.pagination.page,
|
||||
isGroupPage: state => state.config.isGroupPage,
|
||||
isLoading: 'isLoading',
|
||||
}),
|
||||
...mapGetters({ list: 'getList' }),
|
||||
currentPage: {
|
||||
get() {
|
||||
return this.page;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('page:changed', value);
|
||||
},
|
||||
},
|
||||
isListEmpty() {
|
||||
return !this.list || this.list.length === 0;
|
||||
},
|
||||
modalAction() {
|
||||
return s__('PackageRegistry|Delete package');
|
||||
},
|
||||
deletePackageName() {
|
||||
return this.itemToBeDeleted?.name ?? '';
|
||||
},
|
||||
tracking() {
|
||||
const category = this.itemToBeDeleted
|
||||
? packageTypeToTrackCategory(this.itemToBeDeleted.package_type)
|
||||
: undefined;
|
||||
return {
|
||||
category,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setItemToBeDeleted(item) {
|
||||
this.itemToBeDeleted = { ...item };
|
||||
this.track(TrackingActions.REQUEST_DELETE_PACKAGE);
|
||||
this.$refs.packageListDeleteModal.show();
|
||||
},
|
||||
deleteItemConfirmation() {
|
||||
this.$emit('package:delete', this.itemToBeDeleted);
|
||||
this.track(TrackingActions.DELETE_PACKAGE);
|
||||
this.itemToBeDeleted = null;
|
||||
},
|
||||
deleteItemCanceled() {
|
||||
this.track(TrackingActions.CANCEL_DELETE_PACKAGE);
|
||||
this.itemToBeDeleted = null;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
deleteModalContent: s__(
|
||||
'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column">
|
||||
<slot v-if="isListEmpty && !isLoading" name="empty-state"></slot>
|
||||
|
||||
<div v-else-if="isLoading">
|
||||
<packages-list-loader :is-group="isGroupPage" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div data-qa-selector="packages-table">
|
||||
<packages-list-row
|
||||
v-for="packageEntity in list"
|
||||
:key="packageEntity.id"
|
||||
:package-entity="packageEntity"
|
||||
:package-link="packageEntity._links.web_path"
|
||||
:is-group="isGroupPage"
|
||||
@packageToDelete="setItemToBeDeleted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<gl-pagination
|
||||
v-model="currentPage"
|
||||
:per-page="perPage"
|
||||
:total-items="totalItems"
|
||||
align="center"
|
||||
class="w-100 mt-2"
|
||||
/>
|
||||
|
||||
<gl-modal
|
||||
ref="packageListDeleteModal"
|
||||
modal-id="confirm-delete-pacakge"
|
||||
ok-variant="danger"
|
||||
@ok="deleteItemConfirmation"
|
||||
@cancel="deleteItemCanceled"
|
||||
>
|
||||
<template #modal-title>{{ modalAction }}</template>
|
||||
<template #modal-ok>{{ modalAction }}</template>
|
||||
<gl-sprintf :message="$options.i18n.deleteModalContent">
|
||||
<template #name>
|
||||
<strong>{{ deletePackageName }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-modal>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,110 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import PackageFilter from './packages_filter.vue';
|
||||
import PackageList from './packages_list.vue';
|
||||
import PackageSort from './packages_sort.vue';
|
||||
import { PACKAGE_REGISTRY_TABS } from '../constants';
|
||||
import PackagesComingSoon from '../coming_soon/packages_coming_soon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlEmptyState,
|
||||
GlTab,
|
||||
GlTabs,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
PackageFilter,
|
||||
PackageList,
|
||||
PackageSort,
|
||||
PackagesComingSoon,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
emptyListIllustration: state => state.config.emptyListIllustration,
|
||||
emptyListHelpUrl: state => state.config.emptyListHelpUrl,
|
||||
comingSoon: state => state.config.comingSoon,
|
||||
filterQuery: state => state.filterQuery,
|
||||
}),
|
||||
tabsToRender() {
|
||||
return PACKAGE_REGISTRY_TABS;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.requestPackagesList();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']),
|
||||
onPageChanged(page) {
|
||||
return this.requestPackagesList({ page });
|
||||
},
|
||||
onPackageDeleteRequest(item) {
|
||||
return this.requestDeletePackage(item);
|
||||
},
|
||||
tabChanged(e) {
|
||||
const selectedType = PACKAGE_REGISTRY_TABS[e];
|
||||
|
||||
if (selectedType) {
|
||||
this.setSelectedType(selectedType);
|
||||
this.requestPackagesList();
|
||||
}
|
||||
},
|
||||
emptyStateTitle({ title, type }) {
|
||||
if (this.filterQuery) {
|
||||
return s__('PackageRegistry|Sorry, your filter produced no results');
|
||||
}
|
||||
|
||||
if (type) {
|
||||
return sprintf(s__('PackageRegistry|There are no %{packageType} packages yet'), {
|
||||
packageType: title,
|
||||
});
|
||||
}
|
||||
|
||||
return s__('PackageRegistry|There are no packages yet');
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
|
||||
noResults: s__(
|
||||
'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.',
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-tabs @input="tabChanged">
|
||||
<template #tabs-end>
|
||||
<div class="d-flex align-self-center ml-md-auto py-1 py-md-0">
|
||||
<package-filter class="mr-1" @filter="requestPackagesList" />
|
||||
<package-sort @sort:changed="requestPackagesList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<gl-tab v-for="(tab, index) in tabsToRender" :key="index" :title="tab.title">
|
||||
<package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
|
||||
<template #empty-state>
|
||||
<gl-empty-state :title="emptyStateTitle(tab)" :svg-path="emptyListIllustration">
|
||||
<template #description>
|
||||
<gl-sprintf v-if="filterQuery" :message="$options.i18n.widenFilters" />
|
||||
<gl-sprintf v-else :message="$options.i18n.noResults">
|
||||
<template #noPackagesLink="{content}">
|
||||
<gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</template>
|
||||
</package-list>
|
||||
</gl-tab>
|
||||
|
||||
<gl-tab v-if="comingSoon" :title="__('Coming soon')" lazy>
|
||||
<packages-coming-soon
|
||||
:illustration="emptyListIllustration"
|
||||
:project-path="comingSoon.projectPath"
|
||||
:suggested-contributions-path="comingSoon.suggestedContributions"
|
||||
/>
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
</template>
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import { GlSorting, GlSortingItem } from '@gitlab/ui';
|
||||
import { ASCENDING_ODER, DESCENDING_ORDER } from '../constants';
|
||||
import getTableHeaders from '../utils';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'PackageSort',
|
||||
components: {
|
||||
GlSorting,
|
||||
GlSortingItem,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isGroupPage: state => state.config.isGroupPage,
|
||||
orderBy: state => state.sorting.orderBy,
|
||||
sort: state => state.sorting.sort,
|
||||
}),
|
||||
sortText() {
|
||||
const field = this.sortableFields.find(s => s.orderBy === this.orderBy);
|
||||
return field ? field.label : '';
|
||||
},
|
||||
sortableFields() {
|
||||
return getTableHeaders(this.isGroupPage);
|
||||
},
|
||||
isSortAscending() {
|
||||
return this.sort === ASCENDING_ODER;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setSorting']),
|
||||
onDirectionChange() {
|
||||
const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
|
||||
this.setSorting({ sort });
|
||||
this.$emit('sort:changed');
|
||||
},
|
||||
onSortItemClick(item) {
|
||||
this.setSorting({ orderBy: item });
|
||||
this.$emit('sort:changed');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-sorting
|
||||
:text="sortText"
|
||||
:is-ascending="isSortAscending"
|
||||
@sortDirectionChange="onDirectionChange"
|
||||
>
|
||||
<gl-sorting-item
|
||||
v-for="item in sortableFields"
|
||||
ref="packageListSortItem"
|
||||
:key="item.key"
|
||||
@click="onSortItemClick(item.orderBy)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</gl-sorting-item>
|
||||
</gl-sorting>
|
||||
</template>
|
|
@ -0,0 +1,96 @@
|
|||
import { __, s__ } from '~/locale';
|
||||
import { PackageType } from '../shared/constants';
|
||||
|
||||
export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __(
|
||||
'Something went wrong while fetching the packages list.',
|
||||
);
|
||||
export const FETCH_PACKAGE_ERROR_MESSAGE = __('Something went wrong while fetching the package.');
|
||||
export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.');
|
||||
export const DELETE_PACKAGE_SUCCESS_MESSAGE = __('Package deleted successfully');
|
||||
|
||||
export const DEFAULT_PAGE = 1;
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
export const GROUP_PAGE_TYPE = 'groups';
|
||||
|
||||
export const LIST_KEY_NAME = 'name';
|
||||
export const LIST_KEY_PROJECT = 'project_path';
|
||||
export const LIST_KEY_VERSION = 'version';
|
||||
export const LIST_KEY_PACKAGE_TYPE = 'package_type';
|
||||
export const LIST_KEY_CREATED_AT = 'created_at';
|
||||
export const LIST_KEY_ACTIONS = 'actions';
|
||||
|
||||
export const LIST_LABEL_NAME = __('Name');
|
||||
export const LIST_LABEL_PROJECT = __('Project');
|
||||
export const LIST_LABEL_VERSION = __('Version');
|
||||
export const LIST_LABEL_PACKAGE_TYPE = __('Type');
|
||||
export const LIST_LABEL_CREATED_AT = __('Created');
|
||||
export const LIST_LABEL_ACTIONS = '';
|
||||
|
||||
export const LIST_ORDER_BY_PACKAGE_TYPE = 'type';
|
||||
|
||||
export const ASCENDING_ODER = 'asc';
|
||||
export const DESCENDING_ORDER = 'desc';
|
||||
|
||||
// The following is not translated because it is used to build a JavaScript exception error message
|
||||
export const MISSING_DELETE_PATH_ERROR = 'Missing delete_api_path link';
|
||||
|
||||
export const TABLE_HEADER_FIELDS = [
|
||||
{
|
||||
key: LIST_KEY_NAME,
|
||||
label: LIST_LABEL_NAME,
|
||||
orderBy: LIST_KEY_NAME,
|
||||
class: ['text-left'],
|
||||
},
|
||||
{
|
||||
key: LIST_KEY_PROJECT,
|
||||
label: LIST_LABEL_PROJECT,
|
||||
orderBy: LIST_KEY_PROJECT,
|
||||
class: ['text-left'],
|
||||
},
|
||||
{
|
||||
key: LIST_KEY_VERSION,
|
||||
label: LIST_LABEL_VERSION,
|
||||
orderBy: LIST_KEY_VERSION,
|
||||
class: ['text-center'],
|
||||
},
|
||||
{
|
||||
key: LIST_KEY_PACKAGE_TYPE,
|
||||
label: LIST_LABEL_PACKAGE_TYPE,
|
||||
orderBy: LIST_ORDER_BY_PACKAGE_TYPE,
|
||||
class: ['text-center'],
|
||||
},
|
||||
{
|
||||
key: LIST_KEY_CREATED_AT,
|
||||
label: LIST_LABEL_CREATED_AT,
|
||||
orderBy: LIST_KEY_CREATED_AT,
|
||||
class: ['text-center'],
|
||||
},
|
||||
];
|
||||
|
||||
export const PACKAGE_REGISTRY_TABS = [
|
||||
{
|
||||
title: __('All'),
|
||||
type: null,
|
||||
},
|
||||
{
|
||||
title: s__('PackageRegistry|Conan'),
|
||||
type: PackageType.CONAN,
|
||||
},
|
||||
{
|
||||
title: s__('PackageRegistry|Maven'),
|
||||
type: PackageType.MAVEN,
|
||||
},
|
||||
{
|
||||
title: s__('PackageRegistry|NPM'),
|
||||
type: PackageType.NPM,
|
||||
},
|
||||
{
|
||||
title: s__('PackageRegistry|NuGet'),
|
||||
type: PackageType.NUGET,
|
||||
},
|
||||
{
|
||||
title: s__('PackageRegistry|PyPi'),
|
||||
type: PackageType.PYPI,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,31 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import { createStore } from './stores';
|
||||
import PackagesListApp from './components/packages_list_app.vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(Translate);
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-vue-packages-list');
|
||||
const store = createStore();
|
||||
store.dispatch('setInitialState', el.dataset);
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
apolloProvider,
|
||||
components: {
|
||||
PackagesListApp,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('packages-list-app');
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
import Api from '~/api';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import createFlash from '~/flash';
|
||||
import * as types from './mutation_types';
|
||||
import {
|
||||
FETCH_PACKAGES_LIST_ERROR_MESSAGE,
|
||||
DELETE_PACKAGE_ERROR_MESSAGE,
|
||||
DELETE_PACKAGE_SUCCESS_MESSAGE,
|
||||
DEFAULT_PAGE,
|
||||
DEFAULT_PAGE_SIZE,
|
||||
MISSING_DELETE_PATH_ERROR,
|
||||
} from '../constants';
|
||||
import { getNewPaginationPage } from '../utils';
|
||||
|
||||
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
|
||||
export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data);
|
||||
export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);
|
||||
export const setSelectedType = ({ commit }, data) => commit(types.SET_SELECTED_TYPE, data);
|
||||
export const setFilter = ({ commit }, data) => commit(types.SET_FILTER, data);
|
||||
|
||||
export const receivePackagesListSuccess = ({ commit }, { data, headers }) => {
|
||||
commit(types.SET_PACKAGE_LIST_SUCCESS, data);
|
||||
commit(types.SET_PAGINATION, headers);
|
||||
};
|
||||
|
||||
export const requestPackagesList = ({ dispatch, state }, params = {}) => {
|
||||
dispatch('setLoading', true);
|
||||
|
||||
const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params;
|
||||
const { sort, orderBy } = state.sorting;
|
||||
|
||||
const type = state.selectedType?.type?.toLowerCase();
|
||||
const nameFilter = state.filterQuery?.toLowerCase();
|
||||
const packageFilters = { package_type: type, package_name: nameFilter };
|
||||
|
||||
const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages';
|
||||
|
||||
return Api[apiMethod](state.config.resourceId, {
|
||||
params: { page, per_page, sort, order_by: orderBy, ...packageFilters },
|
||||
})
|
||||
.then(({ data, headers }) => {
|
||||
dispatch('receivePackagesListSuccess', { data, headers });
|
||||
})
|
||||
.catch(() => {
|
||||
createFlash(FETCH_PACKAGES_LIST_ERROR_MESSAGE);
|
||||
})
|
||||
.finally(() => {
|
||||
dispatch('setLoading', false);
|
||||
});
|
||||
};
|
||||
|
||||
export const requestDeletePackage = ({ dispatch, state }, { _links }) => {
|
||||
if (!_links || !_links.delete_api_path) {
|
||||
createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
|
||||
const error = new Error(MISSING_DELETE_PATH_ERROR);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
dispatch('setLoading', true);
|
||||
return axios
|
||||
.delete(_links.delete_api_path)
|
||||
.then(() => {
|
||||
const { page: currentPage, perPage, total } = state.pagination;
|
||||
const page = getNewPaginationPage(currentPage, perPage, total - 1);
|
||||
|
||||
dispatch('requestPackagesList', { page });
|
||||
createFlash(DELETE_PACKAGE_SUCCESS_MESSAGE, 'success');
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch('setLoading', false);
|
||||
createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
import { LIST_KEY_PROJECT } from '../constants';
|
||||
import { beautifyPath } from '../../shared/utils';
|
||||
|
||||
export default state =>
|
||||
state.packages.map(p => ({ ...p, projectPathName: beautifyPath(p[LIST_KEY_PROJECT]) }));
|
|
@ -0,0 +1,20 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import * as actions from './actions';
|
||||
import getList from './getters';
|
||||
import mutations from './mutations';
|
||||
import state from './state';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export const createStore = () =>
|
||||
new Vuex.Store({
|
||||
state,
|
||||
getters: {
|
||||
getList,
|
||||
},
|
||||
actions,
|
||||
mutations,
|
||||
});
|
||||
|
||||
export default createStore();
|
|
@ -0,0 +1,8 @@
|
|||
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
|
||||
|
||||
export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS';
|
||||
export const SET_PAGINATION = 'SET_PAGINATION';
|
||||
export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
|
||||
export const SET_SORTING = 'SET_SORTING';
|
||||
export const SET_SELECTED_TYPE = 'SET_SELECTED_TYPE';
|
||||
export const SET_FILTER = 'SET_FILTER';
|
|
@ -0,0 +1,45 @@
|
|||
import * as types from './mutation_types';
|
||||
import {
|
||||
parseIntPagination,
|
||||
normalizeHeaders,
|
||||
convertObjectPropsToCamelCase,
|
||||
} from '~/lib/utils/common_utils';
|
||||
import { GROUP_PAGE_TYPE } from '../constants';
|
||||
|
||||
export default {
|
||||
[types.SET_INITIAL_STATE](state, config) {
|
||||
const { comingSoonJson, ...rest } = config;
|
||||
const comingSoonObj = JSON.parse(comingSoonJson);
|
||||
|
||||
state.config = {
|
||||
...rest,
|
||||
comingSoon: comingSoonObj && convertObjectPropsToCamelCase(comingSoonObj),
|
||||
isGroupPage: config.pageType === GROUP_PAGE_TYPE,
|
||||
};
|
||||
},
|
||||
|
||||
[types.SET_PACKAGE_LIST_SUCCESS](state, packages) {
|
||||
state.packages = packages;
|
||||
},
|
||||
|
||||
[types.SET_MAIN_LOADING](state, isLoading) {
|
||||
state.isLoading = isLoading;
|
||||
},
|
||||
|
||||
[types.SET_PAGINATION](state, headers) {
|
||||
const normalizedHeaders = normalizeHeaders(headers);
|
||||
state.pagination = parseIntPagination(normalizedHeaders);
|
||||
},
|
||||
|
||||
[types.SET_SORTING](state, sorting) {
|
||||
state.sorting = { ...state.sorting, ...sorting };
|
||||
},
|
||||
|
||||
[types.SET_SELECTED_TYPE](state, type) {
|
||||
state.selectedType = type;
|
||||
},
|
||||
|
||||
[types.SET_FILTER](state, query) {
|
||||
state.filterQuery = query;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
export default () => ({
|
||||
/**
|
||||
* Determine if the component is loading data from the API
|
||||
*/
|
||||
isLoading: false,
|
||||
/**
|
||||
* configuration object, set once at store creation with the following structure
|
||||
* {
|
||||
* resourceId: String,
|
||||
* pageType: String,
|
||||
* emptyListIllustration: String,
|
||||
* emptyListHelpUrl: String,
|
||||
* comingSoon: { projectPath: String, suggestedContributions : String } | null;
|
||||
* }
|
||||
*/
|
||||
config: {},
|
||||
/**
|
||||
* Each object in `packages` has the following structure:
|
||||
* {
|
||||
* id: String
|
||||
* name: String,
|
||||
* version: String,
|
||||
* package_type: String // endpoint to request the list
|
||||
* }
|
||||
*/
|
||||
packages: [],
|
||||
/**
|
||||
* Pagination object has the following structure:
|
||||
* {
|
||||
* perPage: Number,
|
||||
* page: Number
|
||||
* total: Number
|
||||
* }
|
||||
*/
|
||||
pagination: {},
|
||||
/**
|
||||
* Sorting object has the following structure:
|
||||
* {
|
||||
* sort: String,
|
||||
* orderBy: String
|
||||
* }
|
||||
*/
|
||||
sorting: {
|
||||
sort: 'desc',
|
||||
orderBy: 'created_at',
|
||||
},
|
||||
/**
|
||||
* The search query that is used to filter packages by name
|
||||
*/
|
||||
filterQuery: '',
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { LIST_KEY_PROJECT, TABLE_HEADER_FIELDS } from './constants';
|
||||
|
||||
export default isGroupPage =>
|
||||
TABLE_HEADER_FIELDS.filter(f => f.key !== LIST_KEY_PROJECT || isGroupPage);
|
||||
|
||||
/**
|
||||
* A small util function that works out if the delete action has deleted the
|
||||
* last item on the current paginated page and if so, returns the previous
|
||||
* page. This ensures the user won't end up on an empty paginated page.
|
||||
*
|
||||
* @param {number} currentPage The current page the user is on
|
||||
* @param {number} perPage Number of items to display per page
|
||||
* @param {number} totalPackages The total number of items
|
||||
*/
|
||||
export const getNewPaginationPage = (currentPage, perPage, totalItems) => {
|
||||
if (totalItems <= perPage) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (currentPage > 1 && (currentPage - 1) * perPage >= totalItems) {
|
||||
return currentPage - 1;
|
||||
}
|
||||
|
||||
return currentPage;
|
||||
};
|
|
@ -0,0 +1,142 @@
|
|||
<script>
|
||||
import PackageTags from './package_tags.vue';
|
||||
import PublishMethod from './publish_method.vue';
|
||||
import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { getPackageTypeLabel } from '../utils';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
|
||||
export default {
|
||||
name: 'PackageListRow',
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
PackageTags,
|
||||
PublishMethod,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
props: {
|
||||
packageEntity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
packageLink: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
disableDelete: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
showPackageType: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
packageType() {
|
||||
return getPackageTypeLabel(this.packageEntity.package_type);
|
||||
},
|
||||
hasPipeline() {
|
||||
return Boolean(this.packageEntity.pipeline);
|
||||
},
|
||||
hasProjectLink() {
|
||||
return Boolean(this.packageEntity.project_path);
|
||||
},
|
||||
deleteAvailable() {
|
||||
return !this.disableDelete && !this.isGroup;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-responsive-table-row" data-qa-selector="packages-row">
|
||||
<div class="table-section section-50 d-flex flex-md-column justify-content-between flex-wrap">
|
||||
<div class="d-flex align-items-center mr-2">
|
||||
<gl-link
|
||||
:href="packageLink"
|
||||
data-qa-selector="package_link"
|
||||
class="text-dark font-weight-bold mb-md-1"
|
||||
>
|
||||
{{ packageEntity.name }}
|
||||
</gl-link>
|
||||
|
||||
<package-tags
|
||||
v-if="packageEntity.tags && packageEntity.tags.length"
|
||||
class="gl-ml-3"
|
||||
:tags="packageEntity.tags"
|
||||
hide-label
|
||||
:tag-display-limit="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex text-secondary text-truncate mt-md-2">
|
||||
<span>{{ packageEntity.version }}</span>
|
||||
|
||||
<div v-if="hasPipeline" class="d-none d-md-inline-block ml-1">
|
||||
<gl-sprintf :message="s__('PackageRegistry|published by %{author}')">
|
||||
<template #author>{{ packageEntity.pipeline.user.name }}</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
|
||||
<div v-if="hasProjectLink" class="d-flex align-items-center">
|
||||
<gl-icon name="review-list" class="text-secondary ml-2 mr-1" />
|
||||
|
||||
<gl-link
|
||||
data-testid="packages-row-project"
|
||||
:href="`/${packageEntity.project_path}`"
|
||||
class="text-secondary"
|
||||
>{{ packageEntity.projectPathName }}</gl-link
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="showPackageType" class="d-flex align-items-center" data-testid="package-type">
|
||||
<gl-icon name="package" class="text-secondary ml-2 mr-1" />
|
||||
<span>{{ packageType }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="table-section d-flex flex-md-column justify-content-between align-items-md-end"
|
||||
:class="!deleteAvailable ? 'section-50' : 'section-40'"
|
||||
>
|
||||
<publish-method :package-entity="packageEntity" :is-group="isGroup" />
|
||||
|
||||
<div class="text-secondary order-0 order-md-1 mt-md-2">
|
||||
<gl-sprintf :message="__('Created %{timestamp}')">
|
||||
<template #timestamp>
|
||||
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
|
||||
{{ timeFormatted(packageEntity.created_at) }}
|
||||
</span>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="deleteAvailable" class="table-section section-10 d-flex justify-content-end">
|
||||
<gl-button
|
||||
data-testid="action-delete"
|
||||
icon="remove"
|
||||
category="primary"
|
||||
variant="danger"
|
||||
:title="s__('PackageRegistry|Remove package')"
|
||||
:aria-label="s__('PackageRegistry|Remove package')"
|
||||
:disabled="!packageEntity._links.delete_api_path"
|
||||
@click="$emit('packageToDelete', packageEntity)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,108 @@
|
|||
<script>
|
||||
import { GlBadge, GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { n__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'PackageTags',
|
||||
components: {
|
||||
GlBadge,
|
||||
GlIcon,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
tagDisplayLimit: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 2,
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
hideLabel: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tagCount() {
|
||||
return this.tags.length;
|
||||
},
|
||||
tagsToRender() {
|
||||
return this.tags.slice(0, this.tagDisplayLimit);
|
||||
},
|
||||
moreTagsDisplay() {
|
||||
return Math.max(0, this.tags.length - this.tagDisplayLimit);
|
||||
},
|
||||
moreTagsTooltip() {
|
||||
if (this.moreTagsDisplay) {
|
||||
return this.tags
|
||||
.slice(this.tagDisplayLimit)
|
||||
.map(x => x.name)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
tagsDisplay() {
|
||||
return n__('%d tag', '%d tags', this.tagCount);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tagBadgeClass(index) {
|
||||
return {
|
||||
'gl-display-none': true,
|
||||
'gl-display-flex': this.tagCount === 1,
|
||||
'd-md-flex': this.tagCount > 1,
|
||||
'gl-mr-2': index !== this.tagsToRender.length - 1,
|
||||
'gl-ml-3': !this.hideLabel && index === 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<div v-if="!hideLabel" data-testid="tagLabel" class="gl-display-flex gl-align-items-center">
|
||||
<gl-icon name="labels" class="gl-text-gray-700 gl-mr-3" />
|
||||
<span class="gl-font-weight-bold">{{ tagsDisplay }}</span>
|
||||
</div>
|
||||
|
||||
<gl-badge
|
||||
v-for="(tag, index) in tagsToRender"
|
||||
:key="index"
|
||||
data-testid="tagBadge"
|
||||
:class="tagBadgeClass(index)"
|
||||
variant="info"
|
||||
>{{ tag.name }}</gl-badge
|
||||
>
|
||||
|
||||
<gl-badge
|
||||
v-if="moreTagsDisplay"
|
||||
v-gl-tooltip
|
||||
data-testid="moreBadge"
|
||||
variant="muted"
|
||||
:title="moreTagsTooltip"
|
||||
class="gl-display-none d-md-flex gl-ml-2"
|
||||
><gl-sprintf :message="__('+%{tags} more')">
|
||||
<template #tags>
|
||||
{{ moreTagsDisplay }}
|
||||
</template>
|
||||
</gl-sprintf></gl-badge
|
||||
>
|
||||
|
||||
<gl-badge
|
||||
v-if="moreTagsDisplay && hideLabel"
|
||||
data-testid="moreBadge"
|
||||
variant="muted"
|
||||
class="d-md-none gl-ml-2"
|
||||
>{{ tagsDisplay }}</gl-badge
|
||||
>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlSkeletonLoader,
|
||||
},
|
||||
props: {
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
desktopShapes() {
|
||||
return this.isGroup ? this.$options.shapes.groups : this.$options.shapes.projects;
|
||||
},
|
||||
desktopHeight() {
|
||||
return this.isGroup ? 38 : 54;
|
||||
},
|
||||
mobileHeight() {
|
||||
return this.isGroup ? 160 : 170;
|
||||
},
|
||||
},
|
||||
shapes: {
|
||||
groups: [
|
||||
{ type: 'rect', width: '100', height: '10', x: '0', y: '15' },
|
||||
{ type: 'rect', width: '100', height: '10', x: '195', y: '15' },
|
||||
{ type: 'rect', width: '60', height: '10', x: '475', y: '15' },
|
||||
{ type: 'rect', width: '60', height: '10', x: '675', y: '15' },
|
||||
{ type: 'rect', width: '100', height: '10', x: '900', y: '15' },
|
||||
],
|
||||
projects: [
|
||||
{ type: 'rect', width: '220', height: '10', x: '0', y: '20' },
|
||||
{ type: 'rect', width: '60', height: '10', x: '305', y: '20' },
|
||||
{ type: 'rect', width: '60', height: '10', x: '535', y: '20' },
|
||||
{ type: 'rect', width: '100', height: '10', x: '760', y: '20' },
|
||||
{ type: 'rect', width: '30', height: '30', x: '970', y: '10', ref: 'button-loader' },
|
||||
],
|
||||
},
|
||||
rowsToRender: {
|
||||
mobile: 5,
|
||||
desktop: 20,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-xs-flex flex-column d-md-none">
|
||||
<gl-skeleton-loader
|
||||
v-for="index in $options.rowsToRender.mobile"
|
||||
:key="index"
|
||||
:width="500"
|
||||
:height="mobileHeight"
|
||||
preserve-aspect-ratio="xMinYMax meet"
|
||||
>
|
||||
<rect width="500" height="10" x="0" y="15" rx="4" />
|
||||
<rect width="500" height="10" x="0" y="45" rx="4" />
|
||||
<rect width="500" height="10" x="0" y="75" rx="4" />
|
||||
<rect width="500" height="10" x="0" y="105" rx="4" />
|
||||
<rect v-if="isGroup" width="500" height="10" x="0" y="135" rx="4" />
|
||||
<rect v-else width="30" height="30" x="470" y="135" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</div>
|
||||
|
||||
<div class="d-none d-md-flex flex-column">
|
||||
<gl-skeleton-loader
|
||||
v-for="index in $options.rowsToRender.desktop"
|
||||
:key="index"
|
||||
:width="1000"
|
||||
:height="desktopHeight"
|
||||
preserve-aspect-ratio="xMinYMax meet"
|
||||
>
|
||||
<component
|
||||
:is="r.type"
|
||||
v-for="(r, rIndex) in desktopShapes"
|
||||
:key="rIndex"
|
||||
rx="4"
|
||||
v-bind="r"
|
||||
/>
|
||||
</gl-skeleton-loader>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,61 @@
|
|||
<script>
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { GlIcon, GlLink } from '@gitlab/ui';
|
||||
import { getCommitLink } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'PublishMethod',
|
||||
components: {
|
||||
ClipboardButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
packageEntity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasPipeline() {
|
||||
return Boolean(this.packageEntity.pipeline);
|
||||
},
|
||||
packageShaShort() {
|
||||
return this.packageEntity.pipeline?.sha.substring(0, 8);
|
||||
},
|
||||
linkToCommit() {
|
||||
return getCommitLink(this.packageEntity, this.isGroup);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex align-items-center text-secondary order-1 order-md-0 mb-md-1">
|
||||
<template v-if="hasPipeline">
|
||||
<gl-icon name="git-merge" class="mr-1" />
|
||||
<strong ref="pipeline-ref" class="mr-1 text-dark">{{ packageEntity.pipeline.ref }}</strong>
|
||||
|
||||
<gl-icon name="commit" class="mr-1" />
|
||||
<gl-link ref="pipeline-sha" :href="linkToCommit" class="mr-1">{{ packageShaShort }}</gl-link>
|
||||
|
||||
<clipboard-button
|
||||
:text="packageEntity.pipeline.sha"
|
||||
:title="__('Copy commit SHA')"
|
||||
css-class="border-0 text-secondary py-0 px-1"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<gl-icon name="upload" class="mr-1" />
|
||||
<strong ref="manual-ref" class="text-dark">{{
|
||||
s__('PackageRegistry|Manually Published')
|
||||
}}</strong>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,23 @@
|
|||
export const PackageType = {
|
||||
CONAN: 'conan',
|
||||
MAVEN: 'maven',
|
||||
NPM: 'npm',
|
||||
NUGET: 'nuget',
|
||||
PYPI: 'pypi',
|
||||
};
|
||||
|
||||
export const TrackingActions = {
|
||||
DELETE_PACKAGE: 'delete_package',
|
||||
REQUEST_DELETE_PACKAGE: 'request_delete_package',
|
||||
CANCEL_DELETE_PACKAGE: 'cancel_delete_package',
|
||||
PULL_PACKAGE: 'pull_package',
|
||||
COMING_SOON_REQUESTED: 'activate_coming_soon_requested',
|
||||
COMING_SOON_LIST: 'click_coming_soon_issue_link',
|
||||
COMING_SOON_HELP: 'click_coming_soon_documentation_link',
|
||||
};
|
||||
|
||||
export const TrackingCategories = {
|
||||
[PackageType.MAVEN]: 'MavenPackages',
|
||||
[PackageType.NPM]: 'NpmPackages',
|
||||
[PackageType.CONAN]: 'ConanPackages',
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import { s__ } from '~/locale';
|
||||
import { PackageType, TrackingCategories } from './constants';
|
||||
|
||||
export const packageTypeToTrackCategory = type =>
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
`UI::${TrackingCategories[type]}`;
|
||||
|
||||
export const beautifyPath = path => (path ? path.split('/').join(' / ') : '');
|
||||
|
||||
export const getPackageTypeLabel = packageType => {
|
||||
switch (packageType) {
|
||||
case PackageType.CONAN:
|
||||
return s__('PackageType|Conan');
|
||||
case PackageType.MAVEN:
|
||||
return s__('PackageType|Maven');
|
||||
case PackageType.NPM:
|
||||
return s__('PackageType|NPM');
|
||||
case PackageType.NUGET:
|
||||
return s__('PackageType|NuGet');
|
||||
case PackageType.PYPI:
|
||||
return s__('PackageType|PyPi');
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getCommitLink = ({ project_path: projectPath, pipeline = {} }, isGroup = false) => {
|
||||
if (isGroup) {
|
||||
return `/${projectPath}/commit/${pipeline.sha}`;
|
||||
}
|
||||
|
||||
return `../commit/${pipeline.sha}`;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import initPackageList from '~/packages/list/packages_list_app_bundle';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.getElementById('js-vue-packages-list')) {
|
||||
initPackageList();
|
||||
}
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import initPackageList from '~/packages/list/packages_list_app_bundle';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.getElementById('js-vue-packages-list')) {
|
||||
initPackageList();
|
||||
}
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import initPackageDetail from '~/packages/details/';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initPackageDetail);
|
|
@ -0,0 +1,11 @@
|
|||
.commit-row-description {
|
||||
border: 0;
|
||||
border-left: 3px solid $white-dark;
|
||||
}
|
||||
|
||||
.package-list-table[aria-busy='true'] {
|
||||
td {
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PackagesAccess
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :verify_packages_enabled!
|
||||
before_action :verify_read_package!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_packages_enabled!
|
||||
render_404 unless Gitlab.config.packages.enabled
|
||||
end
|
||||
|
||||
def verify_read_package!
|
||||
authorize_read_package!(project)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Groups
|
||||
class PackagesController < Groups::ApplicationController
|
||||
before_action :verify_packages_enabled!
|
||||
|
||||
private
|
||||
|
||||
def verify_packages_enabled!
|
||||
render_404 unless group.packages_feature_enabled?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
module Packages
|
||||
class PackageFilesController < ApplicationController
|
||||
include PackagesAccess
|
||||
include SendFileUpload
|
||||
|
||||
def download
|
||||
package_file = project.package_files.find(params[:id])
|
||||
|
||||
send_upload(package_file.file, attachment: package_file.file_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
module Packages
|
||||
class PackagesController < Projects::ApplicationController
|
||||
include PackagesAccess
|
||||
|
||||
before_action :authorize_destroy_package!, only: [:destroy]
|
||||
|
||||
def show
|
||||
@package = project.packages.find(params[:id])
|
||||
@package_files = @package.package_files.recent
|
||||
@maven_metadatum = @package.maven_metadatum
|
||||
end
|
||||
|
||||
def destroy
|
||||
@package = project.packages.find(params[:id])
|
||||
@package.destroy
|
||||
|
||||
redirect_to project_packages_path(@project), status: :found, notice: _('Package was removed')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -392,6 +392,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
:initialize_with_readme,
|
||||
:autoclose_referenced_issues,
|
||||
:suggestion_commit_message,
|
||||
:packages_enabled,
|
||||
:service_desk_enabled,
|
||||
|
||||
project_feature_attributes: %i[
|
||||
|
|
|
@ -48,24 +48,40 @@ module BlobHelper
|
|||
return unless blob = readable_blob(options, path, project, ref)
|
||||
|
||||
common_classes = "btn btn-primary js-edit-blob ml-2 #{options[:extra_class]}"
|
||||
data = { track_event: 'click_edit', track_label: 'Edit' }
|
||||
|
||||
if Feature.enabled?(:web_ide_primary_edit, project.group)
|
||||
common_classes += " btn-inverted"
|
||||
data[:track_property] = 'secondary'
|
||||
end
|
||||
|
||||
edit_button_tag(blob,
|
||||
common_classes,
|
||||
_('Edit'),
|
||||
edit_blob_path(project, ref, path, options),
|
||||
project,
|
||||
ref)
|
||||
ref,
|
||||
data)
|
||||
end
|
||||
|
||||
def ide_edit_button(project = @project, ref = @ref, path = @path, blob:)
|
||||
return unless blob
|
||||
|
||||
common_classes = 'btn btn-primary ide-edit-button ml-2'
|
||||
data = { track_event: 'click_edit_ide', track_label: 'Web IDE' }
|
||||
|
||||
unless Feature.enabled?(:web_ide_primary_edit, project.group)
|
||||
common_classes += " btn-inverted"
|
||||
data[:track_property] = 'secondary'
|
||||
end
|
||||
|
||||
edit_button_tag(blob,
|
||||
'btn btn-inverted btn-primary ide-edit-button ml-2',
|
||||
common_classes,
|
||||
_('Web IDE'),
|
||||
ide_edit_path(project, ref, path),
|
||||
project,
|
||||
ref)
|
||||
ref,
|
||||
data)
|
||||
end
|
||||
|
||||
def modify_file_button(project = @project, ref = @ref, path = @path, blob:, label:, action:, btn_class:, modal_type:)
|
||||
|
@ -325,16 +341,16 @@ module BlobHelper
|
|||
button_tag(button_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' })
|
||||
end
|
||||
|
||||
def edit_link_tag(link_text, edit_path, common_classes)
|
||||
link_to link_text, edit_path, class: "#{common_classes} btn-sm"
|
||||
def edit_link_tag(link_text, edit_path, common_classes, data)
|
||||
link_to link_text, edit_path, class: "#{common_classes} btn-sm", data: data
|
||||
end
|
||||
|
||||
def edit_button_tag(blob, common_classes, text, edit_path, project, ref)
|
||||
def edit_button_tag(blob, common_classes, text, edit_path, project, ref, data)
|
||||
if !on_top_of_branch?(project, ref)
|
||||
edit_disabled_button_tag(text, common_classes)
|
||||
# This condition only applies to users who are logged in
|
||||
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
|
||||
edit_link_tag(text, edit_path, common_classes)
|
||||
edit_link_tag(text, edit_path, common_classes, data)
|
||||
elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
|
||||
edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
|
||||
end
|
||||
|
|
|
@ -28,6 +28,7 @@ module GroupsHelper
|
|||
|
||||
def group_packages_nav_link_paths
|
||||
%w[
|
||||
groups/packages#index
|
||||
groups/container_registries#index
|
||||
]
|
||||
end
|
||||
|
@ -157,6 +158,15 @@ module GroupsHelper
|
|||
groups.to_json
|
||||
end
|
||||
|
||||
def group_packages_nav?
|
||||
group_packages_list_nav? ||
|
||||
group_container_registry_nav?
|
||||
end
|
||||
|
||||
def group_packages_list_nav?
|
||||
@group.packages_feature_enabled?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_group_sidebar_links
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PackagesHelper
|
||||
def package_sort_path(options = {})
|
||||
"#{request.path}?#{options.to_param}"
|
||||
end
|
||||
|
||||
def nuget_package_registry_url(project_id)
|
||||
expose_url(api_v4_projects_packages_nuget_index_path(id: project_id, format: '.json'))
|
||||
end
|
||||
|
||||
def package_registry_instance_url(registry_type)
|
||||
expose_url("api/#{::API::API.version}/packages/#{registry_type}")
|
||||
end
|
||||
|
||||
def package_registry_project_url(project_id, registry_type = :maven)
|
||||
project_api_path = expose_path(api_v4_projects_path(id: project_id))
|
||||
package_registry_project_path = "#{project_api_path}/packages/#{registry_type}"
|
||||
expose_url(package_registry_project_path)
|
||||
end
|
||||
|
||||
def package_from_presenter(package)
|
||||
presenter = ::Packages::Detail::PackagePresenter.new(package)
|
||||
|
||||
presenter.detail_view.to_json
|
||||
end
|
||||
|
||||
def pypi_registry_url(project_id)
|
||||
full_url = expose_url(api_v4_projects_packages_pypi_simple_package_name_path({ id: project_id, package_name: '' }, true))
|
||||
full_url.sub!('://', '://__token__:<your_personal_token>@')
|
||||
end
|
||||
|
||||
def packages_coming_soon_enabled?(resource)
|
||||
::Feature.enabled?(:packages_coming_soon, resource) && ::Gitlab.dev_env_or_com?
|
||||
end
|
||||
|
||||
def packages_coming_soon_data(resource)
|
||||
return unless packages_coming_soon_enabled?(resource)
|
||||
|
||||
{
|
||||
project_path: ::Gitlab.com? ? 'gitlab-org/gitlab' : 'gitlab-org/gitlab-test',
|
||||
suggested_contributions: help_page_path('user/packages/index', anchor: 'suggested-contributions')
|
||||
}
|
||||
end
|
||||
|
||||
def packages_list_data(type, resource)
|
||||
{
|
||||
resource_id: resource.id,
|
||||
page_type: type,
|
||||
empty_list_help_url: help_page_path('administration/packages/index'),
|
||||
empty_list_illustration: image_path('illustrations/no-packages.svg'),
|
||||
coming_soon_json: packages_coming_soon_data(resource).to_json
|
||||
}
|
||||
end
|
||||
end
|
|
@ -429,9 +429,19 @@ module ProjectsHelper
|
|||
|
||||
apply_external_nav_tabs(nav_tabs, project)
|
||||
|
||||
nav_tabs += package_nav_tabs(project, current_user)
|
||||
|
||||
nav_tabs
|
||||
end
|
||||
|
||||
def package_nav_tabs(project, current_user)
|
||||
[].tap do |tabs|
|
||||
if ::Gitlab.config.packages.enabled && can?(current_user, :read_package, project)
|
||||
tabs << :packages
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def apply_external_nav_tabs(nav_tabs, project)
|
||||
nav_tabs << :external_issue_tracker if project.external_issue_tracker
|
||||
nav_tabs << :external_wiki if project.external_wiki
|
||||
|
@ -584,6 +594,7 @@ module ProjectsHelper
|
|||
def project_permissions_settings(project)
|
||||
feature = project.project_feature
|
||||
{
|
||||
packagesEnabled: !!project.packages_enabled,
|
||||
visibilityLevel: project.visibility_level,
|
||||
requestAccessEnabled: !!project.request_access_enabled,
|
||||
issuesAccessLevel: feature.issues_access_level,
|
||||
|
@ -604,6 +615,8 @@ module ProjectsHelper
|
|||
|
||||
def project_permissions_panel_data(project)
|
||||
{
|
||||
packagesAvailable: ::Gitlab.config.packages.enabled,
|
||||
packagesHelpPath: help_page_path('user/packages/index'),
|
||||
currentSettings: project_permissions_settings(project),
|
||||
canDisableEmails: can_disable_emails?(project, current_user),
|
||||
canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
|
||||
|
|
|
@ -581,6 +581,47 @@ module SortingHelper
|
|||
def sort_value_expire_date
|
||||
'expired_asc'
|
||||
end
|
||||
|
||||
def packages_sort_options_hash
|
||||
{
|
||||
sort_value_recently_created => sort_title_created_date,
|
||||
sort_value_oldest_created => sort_title_created_date,
|
||||
sort_value_name => sort_title_name,
|
||||
sort_value_name_desc => sort_title_name,
|
||||
sort_value_version_desc => sort_title_version,
|
||||
sort_value_version_asc => sort_title_version,
|
||||
sort_value_type_desc => sort_title_type,
|
||||
sort_value_type_asc => sort_title_type,
|
||||
sort_value_project_name_desc => sort_title_project_name,
|
||||
sort_value_project_name_asc => sort_title_project_name
|
||||
}
|
||||
end
|
||||
|
||||
def packages_reverse_sort_order_hash
|
||||
{
|
||||
sort_value_recently_created => sort_value_oldest_created,
|
||||
sort_value_oldest_created => sort_value_recently_created,
|
||||
sort_value_name => sort_value_name_desc,
|
||||
sort_value_name_desc => sort_value_name,
|
||||
sort_value_version_desc => sort_value_version_asc,
|
||||
sort_value_version_asc => sort_value_version_desc,
|
||||
sort_value_type_desc => sort_value_type_asc,
|
||||
sort_value_type_asc => sort_value_type_desc,
|
||||
sort_value_project_name_desc => sort_value_project_name_asc,
|
||||
sort_value_project_name_asc => sort_value_project_name_desc
|
||||
}
|
||||
end
|
||||
|
||||
def packages_sort_option_title(sort_value)
|
||||
packages_sort_options_hash[sort_value] || sort_title_created_date
|
||||
end
|
||||
|
||||
def packages_sort_direction_button(sort_value)
|
||||
reverse_sort = packages_reverse_sort_order_hash[sort_value]
|
||||
url = package_sort_path(sort: reverse_sort)
|
||||
|
||||
sort_direction_button(url, reverse_sort, sort_value)
|
||||
end
|
||||
end
|
||||
|
||||
SortingHelper.prepend_if_ee('::EE::SortingHelper')
|
||||
|
|
|
@ -45,7 +45,7 @@ class Packages::PackageFile < ApplicationRecord
|
|||
end
|
||||
|
||||
def download_path
|
||||
Gitlab::Routing.url_helpers.download_project_package_file_path(project, self) if ::Gitlab.ee?
|
||||
Gitlab::Routing.url_helpers.download_project_package_file_path(project, self)
|
||||
end
|
||||
|
||||
def local?
|
||||
|
|
|
@ -9,7 +9,7 @@ module MergeRequests
|
|||
def execute
|
||||
return [] if branch_names.blank?
|
||||
|
||||
source_branches = project.source_of_merge_requests.opened
|
||||
source_branches = project.source_of_merge_requests.open_and_closed
|
||||
.from_source_branches(branch_names).pluck(:source_branch)
|
||||
|
||||
target_branches = project.merge_requests.opened
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
- sort_value = @sort
|
||||
- sort_title = packages_sort_option_title(sort_value)
|
||||
|
||||
- if @packages.any?
|
||||
.d-flex.justify-content-end
|
||||
.dropdown.inline.gl-mt-3.gl-mb-3.package-sort-dropdown
|
||||
.btn-group{ role: 'group' }
|
||||
.btn-group{ role: 'group' }
|
||||
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static', 'qa-selector': 'sort-dropdown-button' }, class: 'btn btn-default' }
|
||||
= sort_title
|
||||
= icon('chevron-down')
|
||||
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
|
||||
%li
|
||||
= sortable_item(sort_title_created_date, package_sort_path(sort: sort_value_recently_created), sort_title)
|
||||
= sortable_item(sort_title_name, package_sort_path(sort: sort_value_name_desc), sort_title)
|
||||
= sortable_item(sort_title_project_name, package_sort_path(sort: sort_value_project_name_desc), sort_title)
|
||||
= sortable_item(sort_title_version, package_sort_path(sort: sort_value_version_desc), sort_title)
|
||||
= sortable_item(sort_title_type, package_sort_path(sort: sort_value_type_desc), sort_title)
|
||||
= packages_sort_direction_button(sort_value)
|
||||
|
||||
.table-holder
|
||||
.gl-responsive-table-row.table-row-header.bg-secondary-50.px-2.border-top{ role: 'row' }
|
||||
.table-section.section-30{ role: 'rowheader' }
|
||||
= _('Name')
|
||||
.table-section.section-20{ role: 'rowheader' }
|
||||
= _('Project')
|
||||
.table-section.section-20{ role: 'rowheader' }
|
||||
= _('Version')
|
||||
.table-section.section-10{ role: 'rowheader' }
|
||||
= _('Type')
|
||||
.table-section.section-20{ role: 'rowheader' }
|
||||
= _('Created')
|
||||
- @packages.each do |package|
|
||||
.gl-responsive-table-row{ data: { 'qa-selector': 'package-row' } }
|
||||
.table-section.section-30
|
||||
.table-mobile-header{ role: "rowheader" }= _("Name")
|
||||
.table-mobile-content.flex-truncate-parent
|
||||
= link_to package.name, project_package_path(package.project, package), class: 'flex-truncate-child'
|
||||
.table-section.section-20
|
||||
.table-mobile-header{ role: "rowheader" }= _("Project")
|
||||
.table-mobile-content
|
||||
= link_to_project(package.project)
|
||||
.table-section.section-20
|
||||
.table-mobile-header{ role: "rowheader" }= _("Version")
|
||||
.table-mobile-content
|
||||
= package.version
|
||||
.table-section.section-10
|
||||
.table-mobile-header{ role: "rowheader" }= _("Type")
|
||||
.table-mobile-content
|
||||
= package.package_type
|
||||
.table-section.section-20
|
||||
.table-mobile-header{ role: "rowheader" }= _("Created")
|
||||
.table-mobile-content
|
||||
= time_ago_with_tooltip(package.created_at)
|
||||
= paginate @packages, theme: "gitlab"
|
||||
- else
|
||||
.row.empty-state
|
||||
.col-12
|
||||
= render 'shared/packages/no_packages'
|
|
@ -0,0 +1,5 @@
|
|||
- page_title _("Packages")
|
||||
|
||||
.row
|
||||
.col-12
|
||||
#js-vue-packages-list{ data: packages_list_data('groups', @group) }
|
|
@ -1,16 +1,23 @@
|
|||
- if group_container_registry_nav?
|
||||
= nav_link(controller: 'groups/registry/repositories') do
|
||||
= link_to group_container_registries_path(@group), title: _('Container Registry') do
|
||||
- packages_link = group_packages_list_nav? ? group_packages_path(@group) : group_container_registries_path(@group)
|
||||
|
||||
- if group_packages_nav?
|
||||
= nav_link(controller: ['groups/packages', 'groups/registry/repositories']) do
|
||||
= link_to packages_link, title: _('Packages') do
|
||||
.nav-icon-container
|
||||
= sprite_icon('package')
|
||||
%span.nav-item-name
|
||||
= _('Packages & Registries')
|
||||
%ul.sidebar-sub-level-items
|
||||
= nav_link(controller: 'groups/registry/repositories', html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to group_container_registries_path(@group), title: _('Container Registry') do
|
||||
= nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to packages_link, title: _('Packages & Registries') do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Packages & Registries')
|
||||
%li.divider.fly-out-top-item
|
||||
= nav_link(controller: 'groups/registry/repositories') do
|
||||
= link_to group_container_registries_path(@group), title: _('Container Registry') do
|
||||
%span= _('Container Registry')
|
||||
- if group_packages_list_nav?
|
||||
= nav_link(controller: 'groups/packages') do
|
||||
= link_to group_packages_path(@group), title: _('Packages') do
|
||||
%span= _('Package Registry')
|
||||
- if group_container_registry_nav?
|
||||
= nav_link(controller: 'groups/registry/repositories') do
|
||||
= link_to group_container_registries_path(@group), title: _('Container Registry') do
|
||||
%span= _('Container Registry')
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
- if project_nav_tab? :container_registry
|
||||
= nav_link controller: :repositories do
|
||||
= link_to project_container_registry_index_path(@project) do
|
||||
- packages_link = project_nav_tab?(:packages) ? project_packages_path(@project) : project_container_registry_index_path(@project)
|
||||
|
||||
- if (project_nav_tab?(:packages) || project_nav_tab?(:container_registry))
|
||||
= nav_link controller: [:packages, :repositories] do
|
||||
= link_to packages_link, data: { qa_selector: 'packages_link' } do
|
||||
.nav-icon-container
|
||||
= sprite_icon('package')
|
||||
%span.nav-item-name
|
||||
= _('Packages & Registries')
|
||||
%ul.sidebar-sub-level-items
|
||||
= nav_link(controller: :repositories, html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to project_container_registry_index_path(@project) do
|
||||
= nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to packages_link do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Packages & Registries')
|
||||
%li.divider.fly-out-top-item
|
||||
= nav_link controller: :repositories do
|
||||
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry', title: _('Container Registry') do
|
||||
%span= _('Container Registry')
|
||||
- if project_nav_tab? :packages
|
||||
= nav_link controller: :packages do
|
||||
= link_to project_packages_path(@project), title: _('Package Registry') do
|
||||
%span= _('Package Registry')
|
||||
- if project_nav_tab? :container_registry
|
||||
= nav_link controller: :repositories do
|
||||
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry', title: _('Container Registry') do
|
||||
%span= _('Container Registry')
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
- requirements_link_url = help_page_path('user/project/issues/design_management', anchor: 'requirements')
|
||||
- requirements_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: requirements_link_url }
|
||||
- link_end = '</a>'.html_safe
|
||||
- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to %{requirements_link_start}enable LFS%{requirements_link_end}.").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end }
|
||||
|
||||
- if @project.design_management_enabled?
|
||||
- if Feature.enabled?(:design_management_moved, @project, default_enabled: true)
|
||||
.js-design-management-new{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } }
|
||||
|
@ -5,13 +10,8 @@
|
|||
.js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } }
|
||||
- else
|
||||
- if Feature.enabled?(:design_management_moved, @project, default_enabled: true)
|
||||
.row.empty-state.design-dropzone-border.gl-mt-5
|
||||
.text-content.center.gl-font-weight-bold
|
||||
- requirements_link_url = help_page_path('user/project/issues/design_management', anchor: 'requirements')
|
||||
- requirements_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: requirements_link_url }
|
||||
- support_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: support_url }
|
||||
- link_end = '</a>'.html_safe
|
||||
= s_("DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance.").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end, support_link_start: support_link_start, support_link_end: link_end }
|
||||
.gl-border-solid.gl-border-1.gl-border-gray-100.gl-rounded-base.gl-mt-5.gl-p-3.gl-text-center
|
||||
= enable_lfs_message
|
||||
- else
|
||||
.mt-4
|
||||
.row.empty-state
|
||||
|
@ -20,8 +20,4 @@
|
|||
%h4.center
|
||||
= _('The one place for your designs')
|
||||
%p.center
|
||||
- requirements_link_url = help_page_path('user/project/issues/design_management', anchor: 'requirements')
|
||||
- requirements_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: requirements_link_url }
|
||||
- support_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: support_url }
|
||||
- link_end = '</a>'.html_safe
|
||||
= s_("DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance.").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end, support_link_start: support_link_start, support_link_end: link_end }
|
||||
= enable_lfs_message
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
- sort_value = @sort
|
||||
- sort_title = packages_sort_option_title(sort_value)
|
||||
|
||||
- if @packages.any?
|
||||
.d-flex.justify-content-end
|
||||
.dropdown.inline.gl-mt-3.gl-mb-3.package-sort-dropdown
|
||||
.btn-group{ role: 'group' }
|
||||
.btn-group{ role: 'group' }
|
||||
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
|
||||
= sort_title
|
||||
= icon('chevron-down')
|
||||
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
|
||||
%li
|
||||
= sortable_item(sort_title_created_date, package_sort_path(sort: sort_value_recently_created), sort_title)
|
||||
= sortable_item(sort_title_name, package_sort_path(sort: sort_value_name_desc), sort_title)
|
||||
= sortable_item(sort_title_version, package_sort_path(sort: sort_value_version_desc), sort_title)
|
||||
= sortable_item(sort_title_type, package_sort_path(sort: sort_value_type_desc), sort_title)
|
||||
= packages_sort_direction_button(sort_value)
|
||||
|
||||
.table-holder
|
||||
.gl-responsive-table-row.table-row-header.bg-secondary-50.px-2.border-top{ role: 'row' }
|
||||
.table-section.section-30{ role: 'rowheader' }
|
||||
= _('Name')
|
||||
.table-section.section-20{ role: 'rowheader' }
|
||||
= _('Version')
|
||||
.table-section.section-20{ role: 'rowheader' }
|
||||
= _('Type')
|
||||
.table-section.section-20{ role: 'rowheader' }
|
||||
= _('Created')
|
||||
.table-section.section-10{ role: 'rowheader' }
|
||||
- @packages.each do |package|
|
||||
.gl-responsive-table-row.package-row.px-2{ data: { qa_selector: "package_row" } }
|
||||
.table-section.section-30
|
||||
.table-mobile-header{ role: "rowheader" }= _("Name")
|
||||
.table-mobile-content.flex-truncate-parent
|
||||
= link_to package.name, project_package_path(@project, package), class: 'flex-truncate-child', data: { qa_selector: "package_link" }
|
||||
.table-section.section-20
|
||||
.table-mobile-header{ role: "rowheader" }= _("Version")
|
||||
.table-mobile-content
|
||||
= package.version
|
||||
.table-section.section-20
|
||||
.table-mobile-header{ role: "rowheader" }= _("Type")
|
||||
.table-mobile-content
|
||||
= package.package_type
|
||||
.table-section.section-20
|
||||
.table-mobile-header{ role: "rowheader" }= _("Created")
|
||||
.table-mobile-content
|
||||
= time_ago_with_tooltip(package.created_at)
|
||||
.table-section.section-10
|
||||
.table-mobile-header{ role: "rowheader" }
|
||||
.table-mobile-content
|
||||
- if can_destroy_package
|
||||
.pull-right
|
||||
= link_to project_package_path(@project, package), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-remove", title: _('Delete Package') do
|
||||
= icon('trash')
|
||||
= paginate @packages, theme: "gitlab"
|
||||
- else
|
||||
.row.empty-state
|
||||
.col-12
|
||||
= render 'shared/packages/no_packages'
|
|
@ -0,0 +1,5 @@
|
|||
- page_title _("Packages")
|
||||
|
||||
.row
|
||||
.col-12
|
||||
#js-vue-packages-list{ data: packages_list_data('projects', @project) }
|
|
@ -0,0 +1,22 @@
|
|||
- add_to_breadcrumbs _("Packages"), project_packages_path(@project)
|
||||
- add_to_breadcrumbs @package.name, project_packages_path(@project)
|
||||
- breadcrumb_title @package.version
|
||||
- page_title _("Packages")
|
||||
|
||||
.row
|
||||
.col-12
|
||||
#js-vue-packages-detail{ data: { package: package_from_presenter(@package),
|
||||
can_delete: can?(current_user, :destroy_package, @project).to_s,
|
||||
destroy_path: project_package_path(@project, @package),
|
||||
svg_path: image_path('illustrations/no-packages.svg'),
|
||||
npm_path: package_registry_instance_url(:npm),
|
||||
npm_help_path: help_page_path('user/packages/npm_registry/index'),
|
||||
maven_path: package_registry_project_url(@project.id, :maven),
|
||||
maven_help_path: help_page_path('user/packages/maven_repository/index'),
|
||||
conan_path: package_registry_instance_url(:conan),
|
||||
conan_help_path: help_page_path('user/packages/conan_repository/index'),
|
||||
nuget_path: nuget_package_registry_url(@project.id),
|
||||
nuget_help_path: help_page_path('user/packages/nuget_repository/index'),
|
||||
pypi_path: pypi_registry_url(@project.id),
|
||||
pypi_setup_path: package_registry_project_url(@project.id, :pypi),
|
||||
pypi_help_path: help_page_path('user/packages/pypi_repository/index') } }
|
|
@ -1,5 +1,5 @@
|
|||
- test_reports_enabled = Feature.enabled?(:junit_pipeline_view, @project)
|
||||
- dag_pipeline_tab_enabled = Feature.enabled?(:dag_pipeline_tab, @project, default_enabled: false)
|
||||
- dag_pipeline_tab_enabled = Feature.enabled?(:dag_pipeline_tab, @project, default_enabled: true)
|
||||
|
||||
.tabs-holder
|
||||
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.svg-content= image_tag 'illustrations/no-packages.svg'
|
||||
.text-content
|
||||
%h4.text-center= _('There are no packages yet')
|
||||
%p
|
||||
- no_packages_url = help_page_path('administration/packages/index')
|
||||
- no_packages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: no_packages_url }
|
||||
= _('Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab.').html_safe % { no_packages_link_start: no_packages_link_start, no_packages_link_end: '</a>'.html_safe }
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Reduce 'cached' query calls for Banzai
|
||||
merge_request: 36735
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add target_details column to AuditEvent table
|
||||
merge_request: 37430
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Package feature moved to core
|
||||
merge_request: 36667
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update closed MRs on push
|
||||
merge_request: 37414
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Shorten 'enable LFS' manage for design management
|
||||
merge_request: 37385
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Refactor all factories to fix SaveBang Cop
|
||||
merge_request: 37268
|
||||
author: Rajendra Kadam
|
||||
type: fixed
|
|
@ -55,6 +55,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
post :toggle_subscription, on: :member
|
||||
end
|
||||
|
||||
resources :packages, only: [:index]
|
||||
|
||||
resources :milestones, constraints: { id: %r{[^/]+} } do
|
||||
member do
|
||||
get :merge_requests
|
||||
|
|
|
@ -30,6 +30,13 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
|
||||
resources :artifacts, only: [:index, :destroy]
|
||||
|
||||
resources :packages, only: [:index, :show, :destroy], module: :packages
|
||||
resources :package_files, only: [], module: :packages do
|
||||
member do
|
||||
get :download
|
||||
end
|
||||
end
|
||||
|
||||
resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do
|
||||
collection do
|
||||
resources :artifacts, only: [] do
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTargetDetailsToAuditEvent < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
# rubocop:disable Migration/AddLimitToTextColumns
|
||||
add_column(:audit_events, :target_details, :text)
|
||||
# rubocop:enable Migration/AddLimitToTextColumns
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column(:audit_events, :target_details)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTextLimitToAuditEventTargetDetails < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_text_limit :audit_events, :target_details, 5_500
|
||||
end
|
||||
|
||||
def down
|
||||
remove_text_limit :audit_events, :target_details
|
||||
end
|
||||
end
|
|
@ -9400,8 +9400,10 @@ CREATE TABLE public.audit_events (
|
|||
ip_address inet,
|
||||
author_name text,
|
||||
entity_path text,
|
||||
target_details text,
|
||||
CONSTRAINT check_492aaa021d CHECK ((char_length(entity_path) <= 5500)),
|
||||
CONSTRAINT check_83ff8406e2 CHECK ((char_length(author_name) <= 255))
|
||||
CONSTRAINT check_83ff8406e2 CHECK ((char_length(author_name) <= 255)),
|
||||
CONSTRAINT check_d493ec90b5 CHECK ((char_length(target_details) <= 5500))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.audit_events_id_seq
|
||||
|
@ -23987,10 +23989,12 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200713071042
|
||||
20200713141854
|
||||
20200713152443
|
||||
20200715124210
|
||||
20200715135130
|
||||
20200715202659
|
||||
20200716044023
|
||||
20200716120419
|
||||
20200716145156
|
||||
20200718040100
|
||||
20200718040200
|
||||
20200718040300
|
||||
|
|
|
@ -13,9 +13,7 @@ module API
|
|||
|
||||
expose :_links do
|
||||
expose :web_path do |package|
|
||||
if ::Gitlab.ee?
|
||||
::Gitlab::Routing.url_helpers.project_package_path(package.project, package)
|
||||
end
|
||||
::Gitlab::Routing.url_helpers.project_package_path(package.project, package)
|
||||
end
|
||||
|
||||
expose :delete_api_path, if: can_destroy(:package, &:project) do |package|
|
||||
|
|
|
@ -35,6 +35,7 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
expose :packages_enabled
|
||||
expose :empty_repo?, as: :empty_repo
|
||||
expose :archived?, as: :archived
|
||||
expose :visibility
|
||||
|
|
|
@ -61,6 +61,7 @@ module API
|
|||
optional :auto_devops_deploy_strategy, type: String, values: %w(continuous manual timed_incremental), desc: 'Auto Deploy strategy'
|
||||
optional :autoclose_referenced_issues, type: Boolean, desc: 'Flag indication if referenced issues auto-closing is enabled'
|
||||
optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins'
|
||||
optional :packages_enabled, type: Boolean, desc: 'Enable project packages feature'
|
||||
end
|
||||
|
||||
params :optional_project_params_ee do
|
||||
|
@ -137,6 +138,7 @@ module API
|
|||
:suggestion_commit_message,
|
||||
:repository_storage,
|
||||
:compliance_framework_setting,
|
||||
:packages_enabled,
|
||||
:service_desk_enabled,
|
||||
|
||||
# TODO: remove in API v5, replaced by *_access_level
|
||||
|
|
|
@ -19,7 +19,7 @@ module Banzai
|
|||
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
|
||||
namespace, project = $~[:namespace], $~[:project]
|
||||
project_path = full_project_path(namespace, project)
|
||||
label = find_label(project_path, $~[:label_id], $~[:label_name])
|
||||
label = find_label_cached(project_path, $~[:label_id], $~[:label_name])
|
||||
|
||||
if label
|
||||
labels[label.id] = yield match, label.id, project, namespace, $~
|
||||
|
@ -34,6 +34,12 @@ module Banzai
|
|||
escape_with_placeholders(unescaped_html, labels)
|
||||
end
|
||||
|
||||
def find_label_cached(parent_ref, label_id, label_name)
|
||||
cached_call(:banzai_find_label_cached, label_name&.tr('"', '') || label_id, path: [object_class, parent_ref]) do
|
||||
find_label(parent_ref, label_id, label_name)
|
||||
end
|
||||
end
|
||||
|
||||
def find_label(parent_ref, label_id, label_name)
|
||||
parent = parent_from_ref(parent_ref)
|
||||
return unless parent
|
||||
|
|
|
@ -169,7 +169,8 @@ module Banzai
|
|||
# been queried the object is returned from the cache.
|
||||
def collection_objects_for_ids(collection, ids)
|
||||
if Gitlab::SafeRequestStore.active?
|
||||
ids = ids.map(&:to_i)
|
||||
ids = ids.map(&:to_i).uniq
|
||||
|
||||
cache = collection_cache[collection_cache_key(collection)]
|
||||
to_query = ids - cache.keys
|
||||
|
||||
|
|
|
@ -9,10 +9,23 @@ module Banzai
|
|||
Snippet
|
||||
end
|
||||
|
||||
# Returns all the nodes that are visible to the given user.
|
||||
def nodes_visible_to_user(user, nodes)
|
||||
snippets = lazy { grouped_objects_for_nodes(nodes, references_relation, self.class.data_attribute) }
|
||||
|
||||
nodes.select do |node|
|
||||
if node.has_attribute?(self.class.data_attribute)
|
||||
can_read_reference?(user, snippets[node])
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_read_reference?(user, ref_project, node)
|
||||
can?(user, :read_snippet, referenced_by([node]).first)
|
||||
def can_read_reference?(user, snippet)
|
||||
can?(user, :read_snippet, snippet)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8195,7 +8195,7 @@ msgstr ""
|
|||
msgid "DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance."
|
||||
msgid "DesignManagement|To upload designs, you'll need to %{requirements_link_start}enable LFS%{requirements_link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "DesignManagement|Unresolve thread"
|
||||
|
|
6
qa/qa.rb
6
qa/qa.rb
|
@ -277,6 +277,11 @@ module QA
|
|||
autoload :Show, 'qa/page/project/job/show'
|
||||
end
|
||||
|
||||
module Packages
|
||||
autoload :Index, 'qa/page/project/packages/index'
|
||||
autoload :Show, 'qa/page/project/packages/show'
|
||||
end
|
||||
|
||||
module Settings
|
||||
autoload :Advanced, 'qa/page/project/settings/advanced'
|
||||
autoload :Main, 'qa/page/project/settings/main'
|
||||
|
@ -315,6 +320,7 @@ module QA
|
|||
autoload :Repository, 'qa/page/project/sub_menus/repository'
|
||||
autoload :Settings, 'qa/page/project/sub_menus/settings'
|
||||
autoload :Project, 'qa/page/project/sub_menus/project'
|
||||
autoload :Packages, 'qa/page/project/sub_menus/packages'
|
||||
end
|
||||
|
||||
module Issue
|
||||
|
|
|
@ -169,7 +169,7 @@ module QA
|
|||
end
|
||||
|
||||
def has_element?(name, **kwargs)
|
||||
wait_for_requests
|
||||
wait_for_requests(skip_finished_loading_check: !!kwargs.delete(:skip_finished_loading_check))
|
||||
|
||||
disabled = kwargs.delete(:disabled)
|
||||
|
||||
|
@ -209,15 +209,6 @@ module QA
|
|||
has_text?(text.gsub(/\s+/, " "), wait: wait)
|
||||
end
|
||||
|
||||
def finished_loading?
|
||||
wait_for_requests
|
||||
|
||||
# The number of selectors should be able to be reduced after
|
||||
# migration to the new spinner is complete.
|
||||
# https://gitlab.com/groups/gitlab-org/-/epics/956
|
||||
has_no_css?('.gl-spinner, .fa-spinner, .spinner', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
|
||||
end
|
||||
|
||||
def finished_loading_block?
|
||||
wait_for_requests
|
||||
|
||||
|
|
|
@ -55,12 +55,10 @@ module QA
|
|||
end
|
||||
|
||||
def fill_file_name(name)
|
||||
finished_loading?
|
||||
fill_element :file_name_field, name
|
||||
end
|
||||
|
||||
def fill_file_content(content)
|
||||
finished_loading?
|
||||
text_area.set content
|
||||
end
|
||||
|
||||
|
|
|
@ -100,19 +100,16 @@ module QA
|
|||
end
|
||||
|
||||
def has_file_content?(file_content)
|
||||
finished_loading?
|
||||
within_element(:file_content) do
|
||||
has_text?(file_content)
|
||||
end
|
||||
end
|
||||
|
||||
def click_edit_button
|
||||
finished_loading?
|
||||
click_element(:snippet_action_button, action: 'Edit')
|
||||
end
|
||||
|
||||
def click_delete_button
|
||||
finished_loading?
|
||||
click_element(:snippet_action_button, action: 'Delete')
|
||||
click_element(:delete_snippet_button)
|
||||
# wait for the page to reload after deletion
|
||||
|
@ -123,32 +120,27 @@ module QA
|
|||
end
|
||||
|
||||
def get_repository_uri_http
|
||||
finished_loading?
|
||||
click_element(:clone_button)
|
||||
Git::Location.new(find_element(:copy_http_url_button)['data-clipboard-text']).uri.to_s
|
||||
end
|
||||
|
||||
def get_repository_uri_ssh
|
||||
finished_loading?
|
||||
click_element(:clone_button)
|
||||
Git::Location.new(find_element(:copy_ssh_url_button)['data-clipboard-text']).uri.to_s
|
||||
end
|
||||
|
||||
def add_comment(comment)
|
||||
finished_loading?
|
||||
fill_element(:note_field, comment)
|
||||
click_element(:comment_button)
|
||||
end
|
||||
|
||||
def has_comment_author?(author_username)
|
||||
finished_loading?
|
||||
within_element(:note_author_content) do
|
||||
has_text?('@' + author_username)
|
||||
end
|
||||
end
|
||||
|
||||
def has_comment_content?(comment_content)
|
||||
finished_loading?
|
||||
within_element(:note_content) do
|
||||
has_text?(comment_content)
|
||||
end
|
||||
|
@ -161,14 +153,12 @@ module QA
|
|||
end
|
||||
|
||||
def edit_comment(comment)
|
||||
finished_loading?
|
||||
click_element(:edit_comment_button)
|
||||
fill_element(:edit_note_field, comment)
|
||||
click_element(:save_comment_button)
|
||||
end
|
||||
|
||||
def delete_comment(comment)
|
||||
finished_loading?
|
||||
click_element(:more_actions_dropdown)
|
||||
accept_alert do
|
||||
click_element(:delete_comment_button)
|
||||
|
|
|
@ -275,7 +275,7 @@ module QA
|
|||
end
|
||||
|
||||
def wait_for_loading
|
||||
finished_loading? && has_no_element?(:skeleton_note)
|
||||
has_no_element?(:skeleton_note)
|
||||
end
|
||||
|
||||
def click_open_in_web_ide
|
||||
|
|
|
@ -11,6 +11,7 @@ module QA
|
|||
include SubMenus::Operations
|
||||
include SubMenus::Repository
|
||||
include SubMenus::Settings
|
||||
include SubMenus::Packages
|
||||
|
||||
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
|
||||
element :activity_link
|
||||
|
|
|
@ -56,7 +56,7 @@ module QA
|
|||
|
||||
def await_installed(application_name)
|
||||
within_element(application_name) do
|
||||
has_element?(:uninstall_button, application: application_name, wait: 300)
|
||||
has_element?(:uninstall_button, application: application_name, wait: 300, skip_finished_loading_check: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Page
|
||||
module Project
|
||||
module Packages
|
||||
class Index < QA::Page::Base
|
||||
view 'app/views/projects/packages/packages/_legacy_package_list.html.haml' do
|
||||
element :package_row
|
||||
element :package_link
|
||||
end
|
||||
|
||||
def click_package(name)
|
||||
click_element(:package_link, text: name)
|
||||
end
|
||||
|
||||
def has_package?(name)
|
||||
has_element?(:package_link, text: name)
|
||||
end
|
||||
|
||||
def has_no_package?(name)
|
||||
has_no_element?(:package_link, text: name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Page
|
||||
module Project
|
||||
module Packages
|
||||
class Show < QA::Page::Base
|
||||
view 'app/assets/javascripts/packages/details/components/app.vue' do
|
||||
element :delete_button
|
||||
element :delete_modal_button
|
||||
element :package_information_content
|
||||
end
|
||||
|
||||
def has_package_info?(name, version)
|
||||
has_element?(:package_information_content, text: /#{name}.*#{version}/)
|
||||
end
|
||||
|
||||
def click_delete
|
||||
click_element(:delete_button)
|
||||
wait_for_animated_element(:delete_modal_button)
|
||||
click_element(:delete_modal_button)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,6 +14,7 @@ module QA
|
|||
|
||||
def click_create_first_snippet
|
||||
finished_loading?
|
||||
|
||||
# The svg takes a fraction of a second to load after which the
|
||||
# "New snippet" button shifts up a bit. This can cause
|
||||
# webdriver to miss the hit so we wait for the svg to load before
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Page
|
||||
module Project
|
||||
module SubMenus
|
||||
module Packages
|
||||
extend QA::Page::PageConcern
|
||||
|
||||
def self.included(base)
|
||||
super
|
||||
|
||||
base.class_eval do
|
||||
view 'app/views/layouts/nav/sidebar/_project_packages_link.html.haml' do
|
||||
element :packages_link
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def click_packages_link
|
||||
within_sidebar do
|
||||
click_element :packages_link
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -136,7 +136,6 @@ module QA
|
|||
end
|
||||
|
||||
def create_first_file(file_name)
|
||||
finished_loading?
|
||||
click_element(:first_file_button, Page::Component::WebIDE::Modal::CreateNewFile)
|
||||
fill_element(:file_name_field, file_name)
|
||||
click_button('Create file')
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue