diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 09:08:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 09:08:42 +0000 |
commit | b76ae638462ab0f673e5915986070518dd3f9ad3 (patch) | |
tree | bdab0533383b52873be0ec0eb4d3c66598ff8b91 /app/assets/javascripts/packages_and_registries/package_registry/components | |
parent | 434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff) | |
download | gitlab-ce-b76ae638462ab0f673e5915986070518dd3f9ad3.tar.gz |
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/packages_and_registries/package_registry/components')
16 files changed, 1664 insertions, 93 deletions
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue new file mode 100644 index 00000000000..4d6a1d5462b --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue @@ -0,0 +1,106 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { + PACKAGE_TYPE_NUGET, + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_MAVEN, +} from '~/packages_and_registries/package_registry/constants'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; + +export default { + i18n: { + sourceText: s__('PackageRegistry|Source project located at %{link}'), + licenseText: s__('PackageRegistry|License information located at %{link}'), + recipeText: s__('PackageRegistry|Recipe: %{recipe}'), + appGroup: s__('PackageRegistry|App group: %{group}'), + appName: s__('PackageRegistry|App name: %{name}'), + }, + components: { + DetailsRow, + GlLink, + GlSprintf, + }, + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + computed: { + showMetadata() { + return ( + [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes( + this.packageEntity.packageType, + ) && this.packageEntity.metadata + ); + }, + showNugetMetadata() { + return this.packageEntity.packageType === PACKAGE_TYPE_NUGET; + }, + showConanMetadata() { + return this.packageEntity.packageType === PACKAGE_TYPE_CONAN; + }, + showMavenMetadata() { + return this.packageEntity.packageType === PACKAGE_TYPE_MAVEN; + }, + }, +}; +</script> + +<template> + <div v-if="showMetadata"> + <h3 class="gl-font-lg" data-testid="title">{{ __('Additional Metadata') }}</h3> + + <div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main"> + <template v-if="showNugetMetadata"> + <details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source"> + <gl-sprintf :message="$options.i18n.sourceText"> + <template #link> + <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{ + packageEntity.metadata.projectUrl + }}</gl-link> + </template> + </gl-sprintf> + </details-row> + <details-row icon="license" padding="gl-p-4" data-testid="nuget-license"> + <gl-sprintf :message="$options.i18n.licenseText"> + <template #link> + <gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{ + packageEntity.metadata.licenseUrl + }}</gl-link> + </template> + </gl-sprintf> + </details-row> + </template> + + <details-row + v-else-if="showConanMetadata" + icon="information-o" + padding="gl-p-4" + data-testid="conan-recipe" + > + <gl-sprintf :message="$options.i18n.recipeText"> + <template #recipe>{{ packageEntity.metadata.recipe }}</template> + </gl-sprintf> + </details-row> + + <template v-else-if="showMavenMetadata"> + <details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app"> + <gl-sprintf :message="$options.i18n.appName"> + <template #name> + <strong>{{ packageEntity.metadata.appName }}</strong> + </template> + </gl-sprintf> + </details-row> + <details-row icon="information-o" padding="gl-p-4" data-testid="maven-group"> + <gl-sprintf :message="$options.i18n.appGroup"> + <template #group> + <strong>{{ packageEntity.metadata.appGroup }}</strong> + </template> + </gl-sprintf> + </details-row> + </template> + </div> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue index e2a2fb1430d..3d3fa62fd43 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue @@ -1,8 +1,4 @@ <script> -/* - * The commented part of this component needs to be re-enabled in the refactor process, - * See here for more info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64939 - */ import { GlBadge, GlButton, @@ -14,22 +10,39 @@ import { GlTabs, GlSprintf, } from '@gitlab/ui'; +import createFlash from '~/flash'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { objectToQuery } from '~/lib/utils/url_utility'; import { s__, __ } from '~/locale'; -// import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue'; -// import DependencyRow from '~/packages/details/components/dependency_row.vue'; -// import InstallationCommands from '~/packages/details/components/installation_commands.vue'; -// import PackageFiles from '~/packages/details/components/package_files.vue'; -// import PackageHistory from '~/packages/details/components/package_history.vue'; -// import PackageListRow from '~/packages/shared/components/package_list_row.vue'; -import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue'; +import { packageTypeToTrackCategory } from '~/packages/shared/utils'; +import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; +import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue'; +import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue'; +import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; +import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue'; +import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue'; +import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; import { - PackageType, - TrackingActions, + PACKAGE_TYPE_NUGET, + PACKAGE_TYPE_COMPOSER, + DELETE_PACKAGE_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_TRACKING_ACTION, + CANCEL_DELETE_PACKAGE_TRACKING_ACTION, + PULL_PACKAGE_TRACKING_ACTION, + DELETE_PACKAGE_FILE_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION, + CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION, SHOW_DELETE_SUCCESS_ALERT, -} from '~/packages/shared/constants'; -import { packageTypeToTrackCategory } from '~/packages/shared/utils'; + FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, + DELETE_PACKAGE_ERROR_MESSAGE, + DELETE_PACKAGE_FILE_ERROR_MESSAGE, + DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, +} from '~/packages_and_registries/package_registry/constants'; + +import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql'; +import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql'; +import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; import Tracking from '~/tracking'; export default { @@ -42,16 +55,13 @@ export default { GlTab, GlTabs, GlSprintf, - PackageTitle: () => import('~/packages/details/components/package_title.vue'), - TerraformTitle: () => - import('~/packages_and_registries/infrastructure_registry/components/details_title.vue'), - PackagesListLoader, - // PackageListRow, - // DependencyRow, - // PackageHistory, - // AdditionalMetadata, - // InstallationCommands, - // PackageFiles, + PackageTitle, + VersionRow, + DependencyRow, + PackageHistory, + AdditionalMetadata, + InstallationCommands, + PackageFiles, }, directives: { GlTooltip: GlTooltipDirective, @@ -59,7 +69,7 @@ export default { }, mixins: [Tracking.mixin()], inject: [ - 'titleComponent', + 'packageId', 'projectName', 'canDelete', 'svgPath', @@ -68,72 +78,150 @@ export default { 'projectListUrl', 'groupListUrl', ], - trackingActions: { ...TrackingActions }, + trackingActions: { + DELETE_PACKAGE_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_TRACKING_ACTION, + CANCEL_DELETE_PACKAGE_TRACKING_ACTION, + PULL_PACKAGE_TRACKING_ACTION, + DELETE_PACKAGE_FILE_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION, + CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION, + }, data() { return { fileToDelete: null, packageEntity: {}, }; }, + apollo: { + packageEntity: { + query: getPackageDetails, + variables() { + return this.queryVariables; + }, + update(data) { + return data.package; + }, + error(error) { + createFlash({ + message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, + captureError: true, + error, + }); + }, + }, + }, computed: { + queryVariables() { + return { + id: convertToGraphQLId('Packages::Package', this.packageId), + }; + }, packageFiles() { - return this.packageEntity.packageFiles; + return this.packageEntity?.packageFiles?.nodes; }, isLoading() { - return false; + return this.$apollo.queries.packageEntity.loading; }, isValidPackage() { - return Boolean(this.packageEntity.name); + return this.isLoading || Boolean(this.packageEntity?.name); }, tracking() { return { - category: packageTypeToTrackCategory(this.packageEntity.package_type), + category: packageTypeToTrackCategory(this.packageEntity.packageType), }; }, hasVersions() { - return this.packageEntity.versions?.length > 0; + return this.packageEntity.versions?.nodes?.length > 0; }, packageDependencies() { - return this.packageEntity.dependency_links || []; + return this.packageEntity.dependencyLinks?.nodes || []; }, showDependencies() { - return this.packageEntity.package_type === PackageType.NUGET; + return this.packageEntity.packageType === PACKAGE_TYPE_NUGET; }, showFiles() { - return this.packageEntity?.package_type !== PackageType.COMPOSER; + return this.packageEntity?.packageType !== PACKAGE_TYPE_COMPOSER; }, }, methods: { formatSize(size) { return numberToHumanSize(size); }, - getPackageVersions() { - if (!this.packageEntity.versions) { - // this.fetchPackageVersions(); + async deletePackage() { + const { data } = await this.$apollo.mutate({ + mutation: destroyPackageMutation, + variables: { + id: this.packageEntity.id, + }, + }); + + if (data?.destroyPackage?.errors[0]) { + throw data.destroyPackage.errors[0]; } }, async confirmPackageDeletion() { - this.track(TrackingActions.DELETE_PACKAGE); + this.track(DELETE_PACKAGE_TRACKING_ACTION); - await this.deletePackage(); + try { + await this.deletePackage(); - const returnTo = - !this.groupListUrl || document.referrer.includes(this.projectName) - ? this.projectListUrl - : this.groupListUrl; // to avoid security issue url are supplied from backend + const returnTo = + !this.groupListUrl || document.referrer.includes(this.projectName) + ? this.projectListUrl + : this.groupListUrl; // to avoid security issue url are supplied from backend - const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true }); + const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true }); - window.location.replace(`${returnTo}?${modalQuery}`); + window.location.replace(`${returnTo}?${modalQuery}`); + } catch (error) { + createFlash({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + type: 'warning', + captureError: true, + error, + }); + } + }, + async deletePackageFile(id) { + try { + const { data } = await this.$apollo.mutate({ + mutation: destroyPackageFileMutation, + variables: { + id, + }, + awaitRefetchQueries: true, + refetchQueries: [ + { + query: getPackageDetails, + variables: this.queryVariables, + }, + ], + }); + if (data?.destroyPackageFile?.errors[0]) { + throw data.destroyPackageFile.errors[0]; + } + createFlash({ + message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + type: 'success', + }); + } catch (error) { + createFlash({ + message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + type: 'warning', + captureError: true, + error, + }); + } }, handleFileDelete(file) { - this.track(TrackingActions.REQUEST_DELETE_PACKAGE_FILE); + this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION); this.fileToDelete = { ...file }; this.$refs.deleteFileModal.show(); }, confirmFileDelete() { - this.track(TrackingActions.DELETE_PACKAGE_FILE); - // this.deletePackageFile(this.fileToDelete.id); + this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION); + this.deletePackageFile(this.fileToDelete.id); this.fileToDelete = null; }, }, @@ -174,60 +262,48 @@ export default { :description="s__('PackageRegistry|There was a problem fetching the details for this package.')" :svg-path="svgPath" /> - - <div v-else class="packages-app"> - <component :is="titleComponent"> + <div v-else-if="!isLoading" class="packages-app"> + <package-title :package-entity="packageEntity"> <template #delete-button> <gl-button v-if="canDelete" v-gl-modal="'delete-modal'" - class="js-delete-button" variant="danger" category="primary" data-qa-selector="delete_button" + data-testid="delete-package" > {{ __('Delete') }} </gl-button> </template> - </component> + </package-title> <gl-tabs> <gl-tab :title="__('Detail')"> - <div data-qa-selector="package_information_content"> - <!-- <package-history :package-entity="packageEntity" :project-name="projectName" /> + <div v-if="!isLoading" data-qa-selector="package_information_content"> + <package-history :package-entity="packageEntity" :project-name="projectName" /> - <installation-commands - :package-entity="packageEntity" - :npm-path="npmPath" - :npm-help-path="npmHelpPath" - /> + <installation-commands :package-entity="packageEntity" /> - <additional-metadata :package-entity="packageEntity" /> --> + <additional-metadata :package-entity="packageEntity" /> </div> - <!-- <package-files + <package-files v-if="showFiles" :package-files="packageFiles" - :can-delete="canDelete" @download-file="track($options.trackingActions.PULL_PACKAGE)" @delete-file="handleFileDelete" - /> --> + /> </gl-tab> - <gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab"> + <gl-tab v-if="showDependencies"> <template #title> <span>{{ __('Dependencies') }}</span> - <gl-badge size="sm" data-testid="dependencies-badge">{{ - packageDependencies.length - }}</gl-badge> + <gl-badge size="sm">{{ packageDependencies.length }}</gl-badge> </template> <template v-if="packageDependencies.length > 0"> - <dependency-row - v-for="(dep, index) in packageDependencies" - :key="index" - :dependency="dep" - /> + <dependency-row v-for="dep in packageDependencies" :key="dep.id" :dependency-link="dep" /> </template> <p v-else class="gl-mt-3" data-testid="no-dependencies-message"> @@ -235,24 +311,9 @@ export default { </p> </gl-tab> - <gl-tab - :title="__('Other 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" - /> --> + <gl-tab :title="__('Other versions')" title-item-class="js-versions-tab"> + <template v-if="hasVersions"> + <version-row v-for="v in packageEntity.versions.nodes" :key="v.id" :package-entity="v" /> </template> <p v-else class="gl-mt-3" data-testid="no-versions-message"> @@ -263,8 +324,8 @@ export default { <gl-modal ref="deleteModal" - class="js-delete-modal" modal-id="delete-modal" + data-testid="delete-modal" :action-primary="$options.modal.packageDeletePrimaryAction" :action-cancel="$options.modal.cancelAction" @primary="confirmPackageDeletion" @@ -287,6 +348,7 @@ export default { modal-id="delete-file-modal" :action-primary="$options.modal.fileDeletePrimaryAction" :action-cancel="$options.modal.cancelAction" + data-testid="delete-file-modal" @primary="confirmFileDelete" @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)" > diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue new file mode 100644 index 00000000000..cc629ae394c --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue @@ -0,0 +1,87 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import { + TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND, + TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, +} from '~/packages_and_registries/package_registry/constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; + +export default { + name: 'ComposerInstallation', + components: { + InstallationTitle, + CodeInstruction, + GlLink, + GlSprintf, + }, + inject: ['composerHelpPath', 'composerConfigRepositoryName', 'composerPath', 'groupListUrl'], + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + computed: { + composerRegistryInclude() { + // eslint-disable-next-line @gitlab/require-i18n-strings + return `composer config repositories.${this.composerConfigRepositoryName} '{"type": "composer", "url": "${this.composerPath}"}'`; + }, + composerPackageInclude() { + // eslint-disable-next-line @gitlab/require-i18n-strings + return `composer req ${[this.packageEntity.name]}:${this.packageEntity.version}`; + }, + groupExists() { + return this.groupListUrl?.length > 0; + }, + }, + i18n: { + registryInclude: s__('PackageRegistry|Add composer registry'), + copyRegistryInclude: s__('PackageRegistry|Copy registry include'), + packageInclude: s__('PackageRegistry|Install package version'), + copyPackageInclude: s__('PackageRegistry|Copy require package include'), + infoLine: s__( + 'PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}', + ), + }, + tracking: { + TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND, + TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, + }, + installOptions: [{ value: 'composer', label: s__('PackageRegistry|Show Composer commands') }], +}; +</script> + +<template> + <div v-if="groupExists" data-testid="root-node"> + <installation-title package-type="composer" :options="$options.installOptions" /> + + <code-instruction + :label="$options.i18n.registryInclude" + :instruction="composerRegistryInclude" + :copy-text="$options.i18n.copyRegistryInclude" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + data-testid="registry-include" + /> + + <code-instruction + :label="$options.i18n.packageInclude" + :instruction="composerPackageInclude" + :copy-text="$options.i18n.copyPackageInclude" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + data-testid="package-include" + /> + <span data-testid="help-text"> + <gl-sprintf :message="$options.i18n.infoLine"> + <template #link="{ content }"> + <gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </span> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue new file mode 100644 index 00000000000..99e27c9d44a --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue @@ -0,0 +1,79 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import { + TRACKING_ACTION_COPY_CONAN_COMMAND, + TRACKING_ACTION_COPY_CONAN_SETUP_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, +} from '~/packages_and_registries/package_registry/constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; + +export default { + name: 'ConanInstallation', + components: { + InstallationTitle, + CodeInstruction, + GlLink, + GlSprintf, + }, + inject: ['conanHelpPath', 'conanPath'], + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + computed: { + conanInstallationCommand() { + // eslint-disable-next-line @gitlab/require-i18n-strings + return `conan install ${this.packageEntity.name} --remote=gitlab`; + }, + conanSetupCommand() { + // eslint-disable-next-line @gitlab/require-i18n-strings + return `conan remote add gitlab ${this.conanPath}`; + }, + }, + i18n: { + helpText: s__( + 'PackageRegistry|For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}.', + ), + }, + tracking: { + TRACKING_ACTION_COPY_CONAN_COMMAND, + TRACKING_ACTION_COPY_CONAN_SETUP_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, + }, + + installOptions: [{ value: 'conan', label: s__('PackageRegistry|Show Conan commands') }], +}; +</script> + +<template> + <div> + <installation-title package-type="conan" :options="$options.installOptions" /> + + <code-instruction + :label="s__('PackageRegistry|Conan Command')" + :instruction="conanInstallationCommand" + :copy-text="s__('PackageRegistry|Copy Conan Command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_CONAN_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + + <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> + + <code-instruction + :label="s__('PackageRegistry|Add Conan Remote')" + :instruction="conanSetupCommand" + :copy-text="s__('PackageRegistry|Copy Conan Setup Command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_CONAN_SETUP_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + <gl-sprintf :message="$options.i18n.helpText"> + <template #link="{ content }"> + <gl-link :href="conanHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue new file mode 100644 index 00000000000..95236eea0b5 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue @@ -0,0 +1,38 @@ +<script> +export default { + name: 'DependencyRow', + props: { + dependencyLink: { + type: Object, + required: true, + }, + }, + computed: { + showVersion() { + return Boolean(this.dependencyLink.dependency?.versionPattern); + }, + showTargetFramework() { + return Boolean(this.dependencyLink.metadata?.targetFramework); + }, + }, +}; +</script> + +<template> + <div class="gl-responsive-table-row"> + <div class="table-section section-50"> + <strong class="gl-text-body">{{ dependencyLink.dependency.name }}</strong> + <span v-if="showTargetFramework" data-testid="target-framework"> + ({{ dependencyLink.metadata.targetFramework }}) + </span> + </div> + + <div + v-if="showVersion" + class="table-section section-50 gl-display-flex gl-md-justify-content-end" + data-testid="version-pattern" + > + <span class="gl-text-body">{{ dependencyLink.dependency.versionPattern }}</span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/file_sha.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/file_sha.vue new file mode 100644 index 00000000000..a25839be7e1 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/file_sha.vue @@ -0,0 +1,41 @@ +<script> +import { s__ } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; + +export default { + name: 'FileSha', + components: { + DetailsRow, + ClipboardButton, + }, + props: { + sha: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + }, + i18n: { + copyButtonTitle: s__('PackageRegistry|Copy SHA'), + }, +}; +</script> + +<template> + <details-row dashed> + <div class="gl-px-4"> + {{ title }}: + {{ sha }} + <clipboard-button + :text="sha" + :title="$options.i18n.copyButtonTitle" + category="tertiary" + size="small" + /> + </div> + </details-row> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue new file mode 100644 index 00000000000..122d444e859 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue @@ -0,0 +1,45 @@ +<script> +import { + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_NPM, + PACKAGE_TYPE_NUGET, + PACKAGE_TYPE_PYPI, + PACKAGE_TYPE_COMPOSER, +} from '~/packages_and_registries/package_registry/constants'; +import ComposerInstallation from './composer_installation.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'; + +export default { + name: 'InstallationCommands', + components: { + [PACKAGE_TYPE_CONAN]: ConanInstallation, + [PACKAGE_TYPE_MAVEN]: MavenInstallation, + [PACKAGE_TYPE_NPM]: NpmInstallation, + [PACKAGE_TYPE_NUGET]: NugetInstallation, + [PACKAGE_TYPE_PYPI]: PypiInstallation, + [PACKAGE_TYPE_COMPOSER]: ComposerInstallation, + }, + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + computed: { + installationComponent() { + return this.$options.components[this.packageEntity.packageType]; + }, + }, +}; +</script> + +<template> + <div v-if="installationComponent"> + <component :is="installationComponent" :package-entity="packageEntity" /> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_title.vue new file mode 100644 index 00000000000..43133bf7825 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_title.vue @@ -0,0 +1,38 @@ +<script> +import PersistedDropdownSelection from '~/vue_shared/components/registry/persisted_dropdown_selection.vue'; + +export default { + name: 'InstallationTitle', + components: { + PersistedDropdownSelection, + }, + props: { + packageType: { + type: String, + required: true, + }, + options: { + type: Array, + required: true, + }, + }, + computed: { + storageKey() { + return `package_${this.packageType}_installation_instructions`; + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center"> + <h3 class="gl-font-lg">{{ __('Installation') }}</h3> + <div> + <persisted-dropdown-selection + :storage-key="storageKey" + :options="options" + @change="$emit('change', $event)" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue new file mode 100644 index 00000000000..2070f0bbca0 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue @@ -0,0 +1,229 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import { + TRACKING_ACTION_COPY_MAVEN_XML, + TRACKING_ACTION_COPY_MAVEN_COMMAND, + TRACKING_ACTION_COPY_MAVEN_SETUP, + TRACKING_ACTION_COPY_GRADLE_INSTALL_COMMAND, + TRACKING_ACTION_COPY_GRADLE_ADD_TO_SOURCE_COMMAND, + TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND, + TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, + TRACKING_LABEL_MAVEN_INSTALLATION, +} from '~/packages_and_registries/package_registry/constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; + +export default { + name: 'MavenInstallation', + components: { + InstallationTitle, + CodeInstruction, + GlLink, + GlSprintf, + }, + inject: ['mavenHelpPath', 'mavenPath'], + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + data() { + return { + instructionType: 'maven', + }; + }, + computed: { + appGroup() { + return this.packageEntity.metadata.appGroup; + }, + appName() { + return this.packageEntity.metadata.appName; + }, + appVersion() { + return this.packageEntity.metadata.appVersion; + }, + mavenInstallationXml() { + return `<dependency> + <groupId>${this.appGroup}</groupId> + <artifactId>${this.appName}</artifactId> + <version>${this.appVersion}</version> +</dependency>`; + }, + + mavenInstallationCommand() { + return `mvn dependency:get -Dartifact=${this.appGroup}:${this.appName}:${this.appVersion}`; + }, + + mavenSetupXml() { + return `<repositories> + <repository> + <id>gitlab-maven</id> + <url>${this.mavenPath}</url> + </repository> +</repositories> + +<distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>${this.mavenPath}</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>${this.mavenPath}</url> + </snapshotRepository> +</distributionManagement>`; + }, + + gradleGroovyInstalCommand() { + // eslint-disable-next-line @gitlab/require-i18n-strings + return `implementation '${this.appGroup}:${this.appName}:${this.appVersion}'`; + }, + + gradleGroovyAddSourceCommand() { + // eslint-disable-next-line @gitlab/require-i18n-strings + return `maven { + url '${this.mavenPath}' +}`; + }, + + gradleKotlinInstalCommand() { + return `implementation("${this.appGroup}:${this.appName}:${this.appVersion}")`; + }, + + gradleKotlinAddSourceCommand() { + return `maven("${this.mavenPath}")`; + }, + showMaven() { + return this.instructionType === 'maven'; + }, + showGroovy() { + return this.instructionType === 'groovy'; + }, + }, + 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}.', + ), + }, + tracking: { + TRACKING_ACTION_COPY_MAVEN_XML, + TRACKING_ACTION_COPY_MAVEN_COMMAND, + TRACKING_ACTION_COPY_MAVEN_SETUP, + TRACKING_ACTION_COPY_GRADLE_INSTALL_COMMAND, + TRACKING_ACTION_COPY_GRADLE_ADD_TO_SOURCE_COMMAND, + TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND, + TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, + TRACKING_LABEL_MAVEN_INSTALLATION, + }, + + installOptions: [ + { value: 'maven', label: s__('PackageRegistry|Maven XML') }, + { value: 'groovy', label: s__('PackageRegistry|Gradle Groovy DSL') }, + { value: 'kotlin', label: s__('PackageRegistry|Gradle Kotlin DSL') }, + ], +}; +</script> + +<template> + <div> + <installation-title + package-type="maven" + :options="$options.installOptions" + @change="instructionType = $event" + /> + + <template v-if="showMaven"> + <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')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_MAVEN_XML" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + multiline + /> + + <code-instruction + :label="s__('PackageRegistry|Maven Command')" + :instruction="mavenInstallationCommand" + :copy-text="s__('PackageRegistry|Copy Maven command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_MAVEN_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + + <h3 class="gl-font-lg">{{ s__('PackageRegistry|Registry setup') }}</h3> + <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')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_MAVEN_SETUP" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + multiline + /> + <gl-sprintf :message="$options.i18n.helpText"> + <template #link="{ content }"> + <gl-link :href="mavenHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </template> + <template v-else-if="showGroovy"> + <code-instruction + class="gl-mb-5" + :label="s__('PackageRegistry|Gradle Groovy DSL install command')" + :instruction="gradleGroovyInstalCommand" + :copy-text="s__('PackageRegistry|Copy Gradle Groovy DSL install command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_GRADLE_INSTALL_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + <code-instruction + :label="s__('PackageRegistry|Add Gradle Groovy DSL repository command')" + :instruction="gradleGroovyAddSourceCommand" + :copy-text="s__('PackageRegistry|Copy add Gradle Groovy DSL repository command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_GRADLE_ADD_TO_SOURCE_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + multiline + /> + </template> + <template v-else> + <code-instruction + class="gl-mb-5" + :label="s__('PackageRegistry|Gradle Kotlin DSL install command')" + :instruction="gradleKotlinInstalCommand" + :copy-text="s__('PackageRegistry|Copy Gradle Kotlin DSL install command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + <code-instruction + :label="s__('PackageRegistry|Add Gradle Kotlin DSL repository command')" + :instruction="gradleKotlinAddSourceCommand" + :copy-text="s__('PackageRegistry|Copy add Gradle Kotlin DSL repository command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + multiline + /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue new file mode 100644 index 00000000000..47081e23318 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue @@ -0,0 +1,141 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import { + TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND, + TRACKING_ACTION_COPY_NPM_SETUP_COMMAND, + TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND, + TRACKING_ACTION_COPY_YARN_SETUP_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, + NPM_PACKAGE_MANAGER, + YARN_PACKAGE_MANAGER, +} from '~/packages_and_registries/package_registry/constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; + +export default { + name: 'NpmInstallation', + components: { + InstallationTitle, + CodeInstruction, + GlLink, + GlSprintf, + }, + inject: ['npmHelpPath', 'npmPath'], + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + data() { + return { + instructionType: NPM_PACKAGE_MANAGER, + }; + }, + computed: { + npmCommand() { + return this.npmInstallationCommand(NPM_PACKAGE_MANAGER); + }, + npmSetup() { + return this.npmSetupCommand(NPM_PACKAGE_MANAGER); + }, + yarnCommand() { + return this.npmInstallationCommand(YARN_PACKAGE_MANAGER); + }, + yarnSetupCommand() { + return this.npmSetupCommand(YARN_PACKAGE_MANAGER); + }, + showNpm() { + return this.instructionType === NPM_PACKAGE_MANAGER; + }, + }, + methods: { + npmInstallationCommand(type) { + // eslint-disable-next-line @gitlab/require-i18n-strings + const instruction = type === NPM_PACKAGE_MANAGER ? 'npm i' : 'yarn add'; + + return `${instruction} ${this.packageEntity.name}`; + }, + npmSetupCommand(type) { + const scope = this.packageEntity.name.substring(0, this.packageEntity.name.indexOf('/')); + + if (type === NPM_PACKAGE_MANAGER) { + return `echo ${scope}:registry=${this.npmPath}/ >> .npmrc`; + } + + return `echo \\"${scope}:registry\\" \\"${this.npmPath}/\\" >> .yarnrc`; + }, + }, + packageManagers: { + NPM_PACKAGE_MANAGER, + }, + tracking: { + TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND, + TRACKING_ACTION_COPY_NPM_SETUP_COMMAND, + TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND, + TRACKING_ACTION_COPY_YARN_SETUP_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, + }, + i18n: { + helpText: s__( + 'PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more.', + ), + }, + installOptions: [ + { value: NPM_PACKAGE_MANAGER, label: s__('PackageRegistry|Show NPM commands') }, + { value: YARN_PACKAGE_MANAGER, label: s__('PackageRegistry|Show Yarn commands') }, + ], +}; +</script> + +<template> + <div> + <installation-title + :package-type="$options.packageManagers.NPM_PACKAGE_MANAGER" + :options="$options.installOptions" + @change="instructionType = $event" + /> + + <code-instruction + v-if="showNpm" + :instruction="npmCommand" + :copy-text="s__('PackageRegistry|Copy npm command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + + <code-instruction + v-else + :instruction="yarnCommand" + :copy-text="s__('PackageRegistry|Copy yarn command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + + <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> + + <code-instruction + v-if="showNpm" + :instruction="npmSetup" + :copy-text="s__('PackageRegistry|Copy npm setup command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NPM_SETUP_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + + <code-instruction + v-else + :instruction="yarnSetupCommand" + :copy-text="s__('PackageRegistry|Copy yarn setup command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_YARN_SETUP_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + + <gl-sprintf :message="$options.i18n.helpText"> + <template #link="{ content }"> + <gl-link :href="npmHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue new file mode 100644 index 00000000000..2e9991b7be5 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue @@ -0,0 +1,75 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import { + TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND, + TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, +} from '~/packages_and_registries/package_registry/constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; + +export default { + name: 'NugetInstallation', + components: { + InstallationTitle, + CodeInstruction, + GlLink, + GlSprintf, + }, + inject: ['nugetHelpPath', 'nugetPath'], + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + computed: { + nugetInstallationCommand() { + return `nuget install ${this.packageEntity.name} -Source "GitLab"`; + }, + nugetSetupCommand() { + return `nuget source Add -Name "GitLab" -Source "${this.nugetPath}" -UserName <your_username> -Password <your_token>`; + }, + }, + tracking: { + TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND, + TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, + }, + i18n: { + helpText: s__( + 'PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}.', + ), + }, + installOptions: [{ value: 'nuget', label: s__('PackageRegistry|Show Nuget commands') }], +}; +</script> + +<template> + <div> + <installation-title package-type="nuget" :options="$options.installOptions" /> + + <code-instruction + :label="s__('PackageRegistry|NuGet Command')" + :instruction="nugetInstallationCommand" + :copy-text="s__('PackageRegistry|Copy NuGet Command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> + + <code-instruction + :label="s__('PackageRegistry|Add NuGet Source')" + :instruction="nugetSetupCommand" + :copy-text="s__('PackageRegistry|Copy NuGet Setup Command')" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + <gl-sprintf :message="$options.i18n.helpText"> + <template #link="{ content }"> + <gl-link :href="nugetHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue new file mode 100644 index 00000000000..bf7fe6fb91b --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue @@ -0,0 +1,163 @@ +<script> +import { GlLink, GlTable, GlDropdownItem, GlDropdown, GlIcon, GlButton } from '@gitlab/ui'; +import { last } from 'lodash'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { __ } from '~/locale'; +import FileSha from '~/packages_and_registries/package_registry/components/details/file_sha.vue'; +import Tracking from '~/tracking'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + name: 'PackageFiles', + components: { + GlLink, + GlTable, + GlIcon, + GlDropdown, + GlDropdownItem, + GlButton, + FileIcon, + TimeAgoTooltip, + FileSha, + }, + mixins: [Tracking.mixin()], + inject: ['canDelete'], + props: { + packageFiles: { + type: Array, + required: false, + default: () => [], + }, + }, + computed: { + filesTableRows() { + return this.packageFiles.map((pf) => ({ + ...pf, + size: this.formatSize(pf.size), + pipeline: last(pf.pipelines), + })); + }, + showCommitColumn() { + // note that this is always false for now since we do not return + // pipelines associated to files for performance concerns + return this.filesTableRows.some((row) => Boolean(row.pipeline?.id)); + }, + filesTableHeaderFields() { + return [ + { + key: 'name', + label: __('Name'), + }, + { + key: 'commit', + label: __('Commit'), + hide: !this.showCommitColumn, + }, + { + key: 'size', + label: __('Size'), + }, + { + key: 'created', + label: __('Created'), + class: 'gl-text-right', + }, + { + key: 'actions', + label: '', + hide: !this.canDelete, + class: 'gl-text-right', + tdClass: 'gl-w-4', + }, + ].filter((c) => !c.hide); + }, + }, + methods: { + formatSize(size) { + return numberToHumanSize(size); + }, + hasDetails(item) { + return item.fileSha256 || item.fileMd5 || item.fileSha1; + }, + }, + i18n: { + deleteFile: __('Delete file'), + }, +}; +</script> + +<template> + <div> + <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3> + <gl-table + :fields="filesTableHeaderFields" + :items="filesTableRows" + :tbody-tr-attr="{ 'data-testid': 'file-row' }" + > + <template #cell(name)="{ item, toggleDetails, detailsShowing }"> + <gl-button + v-if="hasDetails(item)" + :icon="detailsShowing ? 'angle-up' : 'angle-down'" + :aria-label="detailsShowing ? __('Collapse') : __('Expand')" + category="tertiary" + size="small" + @click="toggleDetails" + /> + <gl-link + :href="item.downloadPath" + class="gl-text-gray-500" + data-testid="download-link" + @click="$emit('download-file')" + > + <file-icon + :file-name="item.fileName" + css-classes="gl-relative file-icon" + class="gl-mr-1 gl-relative" + /> + <span>{{ item.fileName }}</span> + </gl-link> + </template> + + <template #cell(commit)="{ item }"> + <gl-link + v-if="item.pipeline && item.pipeline" + :href="item.pipeline.commitPath" + class="gl-text-gray-500" + data-testid="commit-link" + >{{ item.pipeline.sha }}</gl-link + > + </template> + + <template #cell(created)="{ item }"> + <time-ago-tooltip :time="item.createdAt" /> + </template> + + <template #cell(actions)="{ item }"> + <gl-dropdown category="tertiary" right> + <template #button-content> + <gl-icon name="ellipsis_v" /> + </template> + <gl-dropdown-item data-testid="delete-file" @click="$emit('delete-file', item)"> + {{ $options.i18n.deleteFile }} + </gl-dropdown-item> + </gl-dropdown> + </template> + + <template #row-details="{ item }"> + <div + class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-bg-gray-10 gl-rounded-base gl-inset-border-1-gray-100" + > + <file-sha + v-if="item.fileSha256" + data-testid="sha-256" + title="SHA-256" + :sha="item.fileSha256" + /> + <file-sha v-if="item.fileMd5" data-testid="md5" title="MD5" :sha="item.fileMd5" /> + <file-sha v-if="item.fileSha1" data-testid="sha-1" title="SHA-1" :sha="item.fileSha1" /> + </div> + </template> + </gl-table> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue new file mode 100644 index 00000000000..af4a984add4 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue @@ -0,0 +1,169 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { first } from 'lodash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { truncateSha } from '~/lib/utils/text_utility'; +import { s__, n__ } from '~/locale'; +import { HISTORY_PIPELINES_LIMIT } from '~/packages/details/constants'; +import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + name: 'PackageHistory', + i18n: { + createdOn: s__('PackageRegistry|%{name} version %{version} was first created %{datetime}'), + createdByCommitText: s__('PackageRegistry|Created by commit %{link} on branch %{branch}'), + createdByPipelineText: s__( + 'PackageRegistry|Built by pipeline %{link} triggered %{datetime} by %{author}', + ), + publishText: s__('PackageRegistry|Published to the %{project} Package Registry %{datetime}'), + combinedUpdateText: s__( + 'PackageRegistry|Package updated by commit %{link} on branch %{branch}, built by pipeline %{pipeline}, and published to the registry %{datetime}', + ), + archivedPipelineMessageSingular: s__('PackageRegistry|Package has %{number} archived update'), + archivedPipelineMessagePlural: s__('PackageRegistry|Package has %{number} archived updates'), + }, + components: { + GlLink, + GlSprintf, + HistoryItem, + TimeAgoTooltip, + }, + props: { + packageEntity: { + type: Object, + required: true, + }, + projectName: { + type: String, + required: true, + }, + }, + data() { + return { + showDescription: false, + }; + }, + computed: { + pipelines() { + return this.packageEntity?.pipelines?.nodes || []; + }, + firstPipeline() { + return first(this.pipelines); + }, + lastPipelines() { + return this.pipelines.slice(1).slice(-HISTORY_PIPELINES_LIMIT); + }, + showPipelinesInfo() { + return Boolean(this.firstPipeline?.id); + }, + archiviedLines() { + return Math.max(this.pipelines.length - HISTORY_PIPELINES_LIMIT - 1, 0); + }, + archivedPipelineMessage() { + return n__( + this.$options.i18n.archivedPipelineMessageSingular, + this.$options.i18n.archivedPipelineMessagePlural, + this.archiviedLines, + ); + }, + }, + methods: { + truncate(value) { + return truncateSha(value); + }, + convertToBaseId(value) { + return getIdFromGraphQLId(value); + }, + }, +}; +</script> + +<template> + <div class="issuable-discussion"> + <h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3> + <ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline"> + <history-item icon="clock" data-testid="created-on"> + <gl-sprintf :message="$options.i18n.createdOn"> + <template #name> + <strong>{{ packageEntity.name }}</strong> + </template> + <template #version> + <strong>{{ packageEntity.version }}</strong> + </template> + <template #datetime> + <time-ago-tooltip :time="packageEntity.createdAt" /> + </template> + </gl-sprintf> + </history-item> + + <template v-if="showPipelinesInfo"> + <!-- FIRST PIPELINE BLOCK --> + <history-item icon="commit" data-testid="first-pipeline-commit"> + <gl-sprintf :message="$options.i18n.createdByCommitText"> + <template #link> + <gl-link :href="firstPipeline.commitPath">#{{ truncate(firstPipeline.sha) }}</gl-link> + </template> + <template #branch> + <strong>{{ firstPipeline.ref }}</strong> + </template> + </gl-sprintf> + </history-item> + <history-item icon="pipeline" data-testid="first-pipeline-pipeline"> + <gl-sprintf :message="$options.i18n.createdByPipelineText"> + <template #link> + <gl-link :href="firstPipeline.path">#{{ convertToBaseId(firstPipeline.id) }}</gl-link> + </template> + <template #datetime> + <time-ago-tooltip :time="firstPipeline.createdAt" /> + </template> + <template #author>{{ firstPipeline.user.name }}</template> + </gl-sprintf> + </history-item> + </template> + + <!-- PUBLISHED LINE --> + <history-item icon="package" data-testid="published"> + <gl-sprintf :message="$options.i18n.publishText"> + <template #project> + <strong>{{ projectName }}</strong> + </template> + <template #datetime> + <time-ago-tooltip :time="packageEntity.createdAt" /> + </template> + </gl-sprintf> + </history-item> + + <history-item v-if="archiviedLines" icon="history" data-testid="archived"> + <gl-sprintf :message="archivedPipelineMessage"> + <template #number> + <strong>{{ archiviedLines }}</strong> + </template> + </gl-sprintf> + </history-item> + + <!-- PIPELINES LIST ENTRIES --> + <history-item + v-for="pipeline in lastPipelines" + :key="pipeline.id" + icon="pencil" + data-testid="pipeline-entry" + > + <gl-sprintf :message="$options.i18n.combinedUpdateText"> + <template #link> + <gl-link :href="pipeline.commitPath">#{{ truncate(pipeline.sha) }}</gl-link> + </template> + <template #branch> + <strong>{{ pipeline.ref }}</strong> + </template> + <template #pipeline> + <gl-link :href="pipeline.path">#{{ convertToBaseId(pipeline.id) }}</gl-link> + </template> + <template #datetime> + <time-ago-tooltip :time="pipeline.createdAt" /> + </template> + </gl-sprintf> + </history-item> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue new file mode 100644 index 00000000000..65547af3913 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue @@ -0,0 +1,134 @@ +<script> +import { GlIcon, GlSprintf, GlBadge } from '@gitlab/ui'; +import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { __ } from '~/locale'; +import PackageTags from '~/packages/shared/components/package_tags.vue'; +import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants'; +import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils'; +import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + name: 'PackageTitle', + components: { + TitleArea, + GlIcon, + GlSprintf, + PackageTags, + MetadataItem, + GlBadge, + TimeAgoTooltip, + }, + i18n: { + packageInfo: __('v%{version} published %{timeAgo}'), + }, + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + data() { + return { + isDesktop: true, + }; + }, + computed: { + packageTypeDisplay() { + return getPackageTypeLabel(this.packageEntity.packageType); + }, + packagePipeline() { + return this.packageEntity.pipelines?.nodes[0]; + }, + packageIcon() { + if (this.packageEntity.packageType === PACKAGE_TYPE_NUGET) { + return this.packageEntity.metadata?.iconUrl || null; + } + return null; + }, + hasTagsToDisplay() { + return Boolean(this.packageEntity.tags?.nodes && this.packageEntity.tags?.nodes.length); + }, + totalSize() { + return this.packageEntity.packageFiles + ? numberToHumanSize( + this.packageEntity.packageFiles.nodes.reduce((acc, p) => acc + Number(p.size), 0), + ) + : '0'; + }, + }, + mounted() { + this.isDesktop = GlBreakpointInstance.isDesktop(); + }, + methods: { + dynamicSlotName(index) { + return `metadata-tag${index}`; + }, + }, +}; +</script> + +<template> + <title-area :title="packageEntity.name" :avatar="packageIcon" data-qa-selector="package_title"> + <template #sub-header> + <gl-icon name="eye" class="gl-mr-3" /> + <span data-testid="sub-header"> + <gl-sprintf :message="$options.i18n.packageInfo"> + <template #version> + {{ packageEntity.version }} + </template> + + <template #timeAgo> + <time-ago-tooltip + v-if="packageEntity.createdAt" + class="gl-ml-2" + :time="packageEntity.createdAt" + /> + </template> + </gl-sprintf> + </span> + </template> + + <template v-if="packageTypeDisplay" #metadata-type> + <metadata-item data-testid="package-type" icon="package" :text="packageTypeDisplay" /> + </template> + + <template #metadata-size> + <metadata-item data-testid="package-size" icon="disk" :text="totalSize" /> + </template> + + <template v-if="packagePipeline" #metadata-pipeline> + <metadata-item + data-testid="pipeline-project" + icon="review-list" + :text="packagePipeline.project.name" + :link="packagePipeline.project.webUrl" + /> + </template> + + <template v-if="packagePipeline && packagePipeline.ref" #metadata-ref> + <metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" /> + </template> + + <template v-if="isDesktop && hasTagsToDisplay" #metadata-tags> + <package-tags :tag-display-limit="2" :tags="packageEntity.tags.nodes" hide-label /> + </template> + + <!-- we need to duplicate the package tags on mobile to ensure proper styling inside the flex wrap --> + <template + v-for="(tag, index) in packageEntity.tags.nodes" + v-else-if="hasTagsToDisplay" + #[dynamicSlotName(index)] + > + <gl-badge :key="index" class="gl-my-1" data-testid="tag-badge" variant="info" size="sm"> + {{ tag.name }} + </gl-badge> + </template> + + <template #right-actions> + <slot name="delete-button"></slot> + </template> + </title-area> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue new file mode 100644 index 00000000000..669adab9df6 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue @@ -0,0 +1,93 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; +import { + TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND, + TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, +} from '~/packages_and_registries/package_registry/constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; + +export default { + name: 'PyPiInstallation', + components: { + InstallationTitle, + CodeInstruction, + GlLink, + GlSprintf, + }, + inject: ['pypiHelpPath', 'pypiPath', 'pypiSetupPath'], + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + computed: { + pypiPipCommand() { + // eslint-disable-next-line @gitlab/require-i18n-strings + return `pip install ${this.packageEntity.name} --extra-index-url ${this.pypiPath}`; + }, + pypiSetupCommand() { + return `[gitlab] +repository = ${this.pypiSetupPath} +username = __token__ +password = <your personal access token>`; + }, + }, + tracking: { + TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND, + TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND, + TRACKING_LABEL_CODE_INSTRUCTION, + }, + 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}.', + ), + }, + installOptions: [{ value: 'pypi', label: s__('PackageRegistry|Show PyPi commands') }], +}; +</script> + +<template> + <div> + <installation-title package-type="pypi" :options="$options.installOptions" /> + + <code-instruction + :label="s__('PackageRegistry|Pip Command')" + :instruction="pypiPipCommand" + :copy-text="s__('PackageRegistry|Copy Pip command')" + data-testid="pip-command" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + + <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> + <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.tracking.TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + <gl-sprintf :message="$options.i18n.helpText"> + <template #link="{ content }"> + <gl-link :href="pypiHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue new file mode 100644 index 00000000000..d218a405af6 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue @@ -0,0 +1,71 @@ +<script> +import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import PackageTags from '~/packages/shared/components/package_tags.vue'; +import PublishMethod from '~/packages/shared/components/publish_method.vue'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { PACKAGE_DEFAULT_STATUS } from '../../constants'; + +export default { + name: 'PackageListRow', + components: { + GlLink, + GlSprintf, + GlTruncate, + PackageTags, + PublishMethod, + ListItem, + TimeAgoTooltip, + }, + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + computed: { + packageLink() { + return `${getIdFromGraphQLId(this.packageEntity.id)}`; + }, + disabledRow() { + return this.packageEntity.status && this.packageEntity.status !== PACKAGE_DEFAULT_STATUS; + }, + }, +}; +</script> + +<template> + <list-item :disabled="disabledRow"> + <template #left-primary> + <div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"> + <gl-link :href="packageLink" class="gl-text-body gl-min-w-0" :disabled="disabledRow"> + <gl-truncate :text="packageEntity.name" /> + </gl-link> + + <package-tags + v-if="packageEntity.tags.nodes && packageEntity.tags.nodes.length" + class="gl-ml-3" + :tags="packageEntity.tags.nodes" + hide-label + :tag-display-limit="1" + /> + </div> + </template> + <template #left-secondary> + {{ packageEntity.version }} + </template> + + <template #right-primary> + <publish-method :package-entity="packageEntity" /> + </template> + + <template #right-secondary> + <gl-sprintf :message="__('Created %{timestamp}')"> + <template #timestamp> + <time-ago-tooltip :time="packageEntity.createdAt" /> + </template> + </gl-sprintf> + </template> + </list-item> +</template> |