diff options
15 files changed, 677 insertions, 71 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index a67374a2bb6..915fa91d873 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -155,9 +155,7 @@ Dangerfile @gl-quality/eng-prod /app/assets/javascripts/ci_variable_list/ @gitlab-org/ci-cd/verify/frontend /app/assets/javascripts/ci/pipeline_schedules/ @gitlab-org/ci-cd/verify/frontend /app/assets/javascripts/pipeline_editor/ @gitlab-org/ci-cd/verify/frontend -/ee/app/assets/javascripts/ci/ci_minutes_usage/ @gitlab-org/ci-cd/verify/frontend -/ee/app/assets/javascripts/ci/usage_quotas/ci_minutes_usage/ @gitlab-org/ci-cd/verify/frontend -/ee/app/assets/javascripts/usage_quotas/pipelines/ @gitlab-org/ci-cd/verify/frontend +/ee/app/assets/javascripts/ci/ @gitlab-org/ci-cd/verify/frontend /ee/app/assets/javascripts/reports/ @gitlab-org/ci-cd/verify/frontend ^[Templates] diff --git a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue index 1bbc38138e6..34e443f4e58 100644 --- a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue +++ b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue @@ -67,11 +67,19 @@ export default { return this.queryVariables; }, update({ project: { jobs: { nodes = [], pageInfo = {}, count = 0 } = {} } }) { - return { - nodes: nodes.map(mapArchivesToJobNodes).map(mapBooleansToJobNodes), - count, - pageInfo, - }; + this.pageInfo = pageInfo; + this.count = count; + return nodes + .map(mapArchivesToJobNodes) + .map(mapBooleansToJobNodes) + .map((jobNode) => { + return { + ...jobNode, + // GlTable uses an item's _showDetails attribute to determine whether + // it should show the <template #row-details /> for its table row + _showDetails: this.expandedJobs.includes(jobNode.id), + }; + }); }, error() { createAlert({ @@ -82,11 +90,10 @@ export default { }, data() { return { - jobArtifacts: { - nodes: [], - count: 0, - pageInfo: {}, - }, + jobArtifacts: [], + count: 0, + pageInfo: {}, + expandedJobs: [], pagination: INITIAL_PAGINATION_STATE, }; }, @@ -101,13 +108,13 @@ export default { }; }, showPagination() { - return this.jobArtifacts.count > JOBS_PER_PAGE; + return this.count > JOBS_PER_PAGE; }, prevPage() { - return Number(this.jobArtifacts.pageInfo.hasPreviousPage); + return Number(this.pageInfo.hasPreviousPage); }, nextPage() { - return Number(this.jobArtifacts.pageInfo.hasNextPage); + return Number(this.pageInfo.hasNextPage); }, }, methods: { @@ -122,7 +129,7 @@ export default { return `#${id}`; }, handlePageChange(page) { - const { startCursor, endCursor } = this.jobArtifacts.pageInfo; + const { startCursor, endCursor } = this.pageInfo; if (page > this.pagination.currentPage) { this.pagination = { @@ -139,9 +146,15 @@ export default { }; } }, - handleRowToggle(toggleDetails, hasArtifacts) { + handleRowToggle(toggleDetails, hasArtifacts, id, detailsShowing) { if (!hasArtifacts) return; toggleDetails(); + + if (!detailsShowing) { + this.expandedJobs.push(id); + } else { + this.expandedJobs.splice(this.expandedJobs.indexOf(id), 1); + } }, downloadPath(job) { return job.archive?.downloadPath; @@ -202,7 +215,7 @@ export default { <template> <div> <gl-table - :items="jobArtifacts.nodes" + :items="jobArtifacts" :fields="$options.fields" :busy="$apollo.queries.jobArtifacts.loading" stacked="sm" @@ -212,12 +225,12 @@ export default { <gl-loading-icon size="lg" /> </template> <template - #cell(artifacts)="{ item: { artifacts, hasArtifacts }, toggleDetails, detailsShowing }" + #cell(artifacts)="{ item: { id, artifacts, hasArtifacts }, toggleDetails, detailsShowing }" > <span :class="{ 'gl-cursor-pointer': hasArtifacts }" data-testid="job-artifacts-count" - @click="handleRowToggle(toggleDetails, hasArtifacts)" + @click="handleRowToggle(toggleDetails, hasArtifacts, id, detailsShowing)" > <gl-icon v-if="hasArtifacts" diff --git a/app/assets/javascripts/artifacts/graphql/cache_update.js b/app/assets/javascripts/artifacts/graphql/cache_update.js index c620e03c80d..9fa6114c7d4 100644 --- a/app/assets/javascripts/artifacts/graphql/cache_update.js +++ b/app/assets/javascripts/artifacts/graphql/cache_update.js @@ -3,28 +3,28 @@ import produce from 'immer'; export const hasErrors = ({ errors = [] }) => errors?.length; export function removeArtifactFromStore(store, deletedArtifactId, query, variables) { - if (!hasErrors(deletedArtifactId)) { - const sourceData = store.readQuery({ - query, - variables, - }); + if (hasErrors(deletedArtifactId)) return; - const data = produce(sourceData, (draftData) => { - draftData.project.jobs.nodes = draftData.project.jobs.nodes.map((jobNode) => { - return { - ...jobNode, - artifacts: { - ...jobNode.artifacts, - nodes: jobNode.artifacts.nodes.filter(({ id }) => id !== deletedArtifactId), - }, - }; - }); - }); + const sourceData = store.readQuery({ + query, + variables, + }); - store.writeQuery({ - query, - variables, - data, + const data = produce(sourceData, (draftData) => { + draftData.project.jobs.nodes = draftData.project.jobs.nodes.map((jobNode) => { + return { + ...jobNode, + artifacts: { + ...jobNode.artifacts, + nodes: jobNode.artifacts.nodes.filter(({ id }) => id !== deletedArtifactId), + }, + }; }); - } + }); + + store.writeQuery({ + query, + variables, + data, + }); } diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js index 9def0bdc3e5..6da058ebc9c 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js @@ -21,11 +21,11 @@ export const I18N = { approvalsTitle: s__('BranchRules|Approvals'), manageApprovalsLinkTitle: s__('BranchRules|Manage in Merge Request Approvals'), approvalsDescription: s__( - 'BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Lean more.%{linkEnd}', + 'BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Learn more.%{linkEnd}', ), statusChecksTitle: s__('BranchRules|Status checks'), statusChecksDescription: s__( - 'BranchRules|Check for a status response in merge requests. Failures do not block merges. %{linkStart}Lean more.%{linkEnd}', + 'BranchRules|Check for a status response in merge requests. Failures do not block merges. %{linkStart}Learn more.%{linkEnd}', ), statusChecksLinkTitle: s__('BranchRules|Manage in Status checks'), statusChecksHeader: s__('BranchRules|Status checks (%{total})'), diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue index 38bffb886e0..eb11e17dd1b 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue @@ -54,7 +54,7 @@ export default { const branchRule = branchRules.nodes.find((rule) => rule.name === this.branch); this.branchProtection = branchRule?.branchProtection; this.approvalRules = branchRule?.approvalRules; - this.statusChecks = branchRule?.externalStatusChecks || []; + this.statusChecks = branchRule?.externalStatusChecks?.nodes || []; }, }, }, diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql index 4ca474a5ceb..aa1e4923aa8 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql +++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql @@ -61,6 +61,13 @@ query getBranchRulesDetails($projectPath: ID!) { } } } + externalStatusChecks { + nodes { + id + name + externalUrl + } + } } } } diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 106e4871736..d9cacb6395d 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -78,7 +78,7 @@ To add a commit diff comment: You can select multiple lines by dragging the **Comment** (**{comment}**) icon. 1. Enter your comment and select **Start a review** or **Add comment now**. -The comment is displayed on the merge request's **Discussions** tab. +The comment is displayed on the merge request's **Overview** tab. The comment is not displayed on your project's **Repository > Commits** page. @@ -200,7 +200,7 @@ You can also mark an [issue as confidential](../project/issues/confidential_issu For issues and merge requests with many comments, you can filter the page to show comments only. -1. Open a merge request's **Discussion** tab, or epic or issue's **Overview** tab. +1. Open the **Overview** tab in a merge request, issue, or epic. 1. On the right side of the page, select from the filter: - **Show all activity**: Display all user comments and system notes. (issue updates, mentions from other issues, changes to the description, and so on). diff --git a/doc/user/packages/workflows/build_packages.md b/doc/user/packages/workflows/build_packages.md new file mode 100644 index 00000000000..063217f0ad7 --- /dev/null +++ b/doc/user/packages/workflows/build_packages.md @@ -0,0 +1,504 @@ +--- +stage: Package +group: Package Registry +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Build packages + +Learn how to install and build packages different package formats. + +- [Composer](#composer) +- [Conan](#conan) +- [Maven](#maven) +- [Gradle](#gradle) +- [npm](#npm) +- [Yarn](#yarn) +- [NuGet](#nuget) +- [PyPI](#pypi) + +## Composer + +1. Create a directory called `my-composer-package` and change to that directory: + + ```shell + mkdir my-composer-package && cd my-composer-package + ``` + +1. Run [`composer init`](https://getcomposer.org/doc/03-cli.md#init) and answer the prompts. + + For namespace, enter your unique [namespace](../../../user/namespace/index.md), like your GitLab username or group name. + + A file called `composer.json` is created: + + ```json + { + "name": "<namespace>/composer-test", + "description": "Library XY", + "type": "library", + "license": "GPL-3.0-only", + "authors": [ + { + "name": "John Doe", + "email": "john@example.com" + } + ], + "require": {} + } + ``` + +## Conan + +### Install Conan + +Download the Conan package manager to your local development environment by +following the instructions at [conan.io](https://conan.io/downloads.html). + +When installation is complete, verify you can use Conan in your terminal by +running: + +```shell +conan --version +``` + +The Conan version is printed in the output: + +```plaintext +Conan version 1.20.5 +``` + +### Install CMake + +When you develop with C++ and Conan, you can select from many available +compilers. This example uses the CMake build system generator. + +To install CMake: + +- For Mac, use [Homebrew](https://brew.sh/) and run `brew install cmake`. +- For other operating systems, follow the instructions at [cmake.org](https://cmake.org/install/). + +When installation is complete, verify you can use CMake in your terminal by +running: + +```shell +cmake --version +``` + +The CMake version is printed in the output. + +### Create a project + +To test the Package Registry, you need a C++ project. If you don't already have +one, you can clone the Conan [hello world starter project](https://github.com/conan-io/hello). + +### Build a Conan package + +To build a package: + +1. Open a terminal and navigate to your project's root folder. +1. Generate a new recipe by running `conan new` with a package name and version: + + ```shell + conan new Hello/0.1 -t + ``` + +1. Create a package for the recipe by running `conan create` with the Conan user + and channel: + + ```shell + conan create . mycompany/beta + ``` + + NOTE: + If you use an [instance remote](../conan_repository/index.md#add-a-remote-for-your-instance), you must + follow a specific [naming convention](../conan_repository/index.md#package-recipe-naming-convention-for-instance-remotes). + +A package with the recipe `Hello/0.1@mycompany/beta` is created. + +For more details about creating and managing Conan packages, see the +[Conan documentation](https://docs.conan.io/en/latest/creating_packages.html). + +## Maven + +### Install Maven + +The required minimum versions are: + +- Java 11.0.5+ +- Maven 3.6+ + +Follow the instructions at [maven.apache.org](https://maven.apache.org/install.html) +to download and install Maven for your local development environment. After +installation is complete, verify you can use Maven in your terminal by running: + +```shell +mvn --version +``` + +The output should be similar to: + +```shell +Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-04T20:00:29+01:00) +Maven home: /Users/<your_user>/apache-maven-3.6.1 +Java version: 12.0.2, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk-12.0.2.jdk/Contents/Home +Default locale: en_GB, platform encoding: UTF-8 +OS name: "mac os x", version: "10.15.2", arch: "x86_64", family: "mac" +``` + +### Build a Maven package + +1. Open your terminal and create a directory to store the project. +1. From the new directory, run this Maven command to initialize a new package: + + ```shell + mvn archetype:generate -DgroupId=com.mycompany.mydepartment -DartifactId=my-project -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false + ``` + + The arguments are: + + - `DgroupId`: A unique string that identifies your package. Follow + the [Maven naming conventions](https://maven.apache.org/guides/mini/guide-naming-conventions.html). + - `DartifactId`: The name of the `JAR`, appended to the end of the `DgroupId`. + - `DarchetypeArtifactId`: The archetype used to create the initial structure of + the project. + - `DinteractiveMode`: Create the project using batch mode (optional). + +This message indicates that the project was set up successfully: + +```shell +... +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 3.429 s +[INFO] Finished at: 2020-01-28T11:47:04Z +[INFO] ------------------------------------------------------------------------ +``` + +In the folder where you ran the command, a new directory should be displayed. +The directory name should match the `DartifactId` parameter, which in this case, +is `my-project`. + +## Gradle + +### Install Gradle + +If you want to create a new Gradle project, you must install Gradle. Follow +instructions at [gradle.org](https://gradle.org/install/) to download and install +Gradle for your local development environment. + +In your terminal, verify you can use Gradle by running: + +```shell +gradle -version +``` + +To use an existing Gradle project, in the project directory, +on Linux execute `gradlew`, or on Windows execute `gradlew.bat`. + +The output should be similar to: + +```plaintext +------------------------------------------------------------ +Gradle 6.0.1 +------------------------------------------------------------ + +Build time: 2019-11-18 20:25:01 UTC +Revision: fad121066a68c4701acd362daf4287a7c309a0f5 + +Kotlin: 1.3.50 +Groovy: 2.5.8 +Ant: Apache Ant(TM) version 1.10.7 compiled on September 1 2019 +JVM: 11.0.5 (Oracle Corporation 11.0.5+10) +OS: Windows 10 10.0 amd64 +``` + +### Create a package + +1. Open your terminal and create a directory to store the project. +1. From this new directory, run this command to initialize a new package: + + ```shell + gradle init + ``` + + The output should be: + + ```plaintext + Select type of project to generate: + 1: basic + 2: application + 3: library + 4: Gradle plugin + Enter selection (default: basic) [1..4] + ``` + +1. Enter `3` to create a new Library project. The output should be: + + ```plaintext + Select implementation language: + 1: C++ + 2: Groovy + 3: Java + 4: Kotlin + 5: Scala + 6: Swift + ``` + +1. Enter `3` to create a new Java Library project. The output should be: + + ```plaintext + Select build script DSL: + 1: Groovy + 2: Kotlin + Enter selection (default: Groovy) [1..2] + ``` + +1. Enter `1` to create a new Java Library project that is described in Groovy DSL, or `2` to create one that is described in Kotlin DSL. The output should be: + + ```plaintext + Select test framework: + 1: JUnit 4 + 2: TestNG + 3: Spock + 4: JUnit Jupiter + ``` + +1. Enter `1` to initialize the project with JUnit 4 testing libraries. The output should be: + + ```plaintext + Project name (default: test): + ``` + +1. Enter a project name or press <kbd>Enter</kbd> to use the directory name as project name. + +## npm + +### Install npm + +Install Node.js and npm in your local development environment by following +the instructions at [npmjs.com](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm/). + +When installation is complete, verify you can use npm in your terminal by +running: + +```shell +npm --version +``` + +The npm version is shown in the output: + +```plaintext +6.10.3 +``` + +### Create an npm package + +1. Create an empty directory. +1. Go to the directory and initialize an empty package by running: + + ```shell + npm init + ``` + +1. Enter responses to the questions. Ensure the **package name** follows + the [naming convention](../npm_registry/index.md#package-naming-convention) and is scoped to the project or group where the registry exists. + +## Yarn + +### Install Yarn + +As an alternative to npm, you can install Yarn in your local environment by following the +instructions at [classic.yarnpkg.com](https://classic.yarnpkg.com/en/docs/install). + +When installation is complete, verify you can use Yarn in your terminal by +running: + +```shell +yarn --version +``` + +The Yarn version is shown in the output: + +```plaintext +1.19.1 +``` + +### Create a package + +1. Create an empty directory. +1. Go to the directory and initialize an empty package by running: + + ```shell + yarn init + ``` + +1. Enter responses to the questions. Ensure the **package name** follows + the [naming convention](../npm_registry/index.md#package-naming-convention) and is scoped to the + project or group where the registry exists. + +A `package.json` file is created. + +## NuGet + +### Install NuGet + +Follow the instructions from [https://learn.microsoft.com/en-us/nuget/install-nuget-client-tools] Microsoft to install NuGet. If you have +[Visual Studio](https://visualstudio.microsoft.com/vs/), NuGet is +probably already installed. + +Verify that the [NuGet CLI](https://www.nuget.org/) is installed by running: + +```shell +nuget help +``` + +The output should be similar to: + +```plaintext +NuGet Version: 5.1.0.6013 +usage: NuGet <command> [args] [options] +Type 'NuGet help <command>' for help on a specific command. + +Available commands: + +[output truncated] +``` + +## PyPI + +### Install pip and twine + +Install a recent version of [pip](https://pypi.org/project/pip/) and +[twine](https://pypi.org/project/twine/). + +### Create a project + +Create a test project. + +1. Open your terminal. +1. Create a directory called `MyPyPiPackage`, and then go to that directory: + + ```shell + mkdir MyPyPiPackage && cd MyPyPiPackage + ``` + +1. Create another directory and go to it: + + ```shell + mkdir mypypipackage && cd mypypipackage + ``` + +1. Create the required files in this directory: + + ```shell + touch __init__.py + touch greet.py + ``` + +1. Open the `greet.py` file, and then add: + + ```python + def SayHello(): + print("Hello from MyPyPiPackage") + return + ``` + +1. Open the `__init__.py` file, and then add: + + ```python + from .greet import SayHello + ``` + +1. To test the code, in your `MyPyPiPackage` directory, start the Python prompt. + + ```shell + python + ``` + +1. Run this command: + + ```python + >>> from mypypipackage import SayHello + >>> SayHello() + ``` + +A message indicates that the project was set up successfully: + +```plaintext +Python 3.8.2 (v3.8.2:7b3ab5921f, Feb 24 2020, 17:52:18) +[Clang 6.0 (clang-600.0.57)] on darwin +Type "help", "copyright", "credits" or "license" for more information. +>>> from mypypipackage import SayHello +>>> SayHello() +Hello from MyPyPiPackage +``` + +### Create a PyPI package + +After you create a project, you can create a package. + +1. In your terminal, go to the `MyPyPiPackage` directory. +1. Create a `pyproject.toml` file: + + ```shell + touch pyproject.toml + ``` + + This file contains all the information about the package. For more information + about this file, see [creating `pyproject.toml`](https://packaging.python.org/en/latest/tutorials/packaging-projects/#creating-pyproject-toml). + Because GitLab identifies packages based on + [Python normalized names (PEP-503)](https://www.python.org/dev/peps/pep-0503/#normalized-names), + ensure your package name meets these requirements. See the [installation section](../pypi_repository/index.md#authenticate-with-a-ci-job-token) + for details. + +1. Open the `pyproject.toml` file, and then add basic information: + + ```toml + [build-system] + requires = ["setuptools>=61.0"] + build-backend = "setuptools.build_meta" + + [project] + name = "mypypipackage" + version = "0.0.1" + authors = [ + { name="Example Author", email="author@example.com" }, + ] + description = "A small example package" + requires-python = ">=3.7" + classifiers = [ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + ] + + [tool.setuptools.packages] + find = {} + ``` + +1. Save the file. +1. Install the package build library: + + ```shell + pip install build + ``` + +1. Build the package: + + ```shell + python -m build + ``` + +The output should be visible in a newly-created `dist` folder: + +```shell +ls dist +``` + +The output should appear similar to the following: + +```plaintext +mypypipackage-0.0.1-py3-none-any.whl mypypipackage-0.0.1.tar.gz +``` + +The package is now ready to be published to the Package Registry. diff --git a/doc/user/project/merge_requests/approvals/index.md b/doc/user/project/merge_requests/approvals/index.md index ea03427161e..eb460225858 100644 --- a/doc/user/project/merge_requests/approvals/index.md +++ b/doc/user/project/merge_requests/approvals/index.md @@ -68,7 +68,7 @@ if a user approves a merge request and is shown in the reviewer list, a green ch After a merge request receives the [number and type of approvals](rules.md) you configure, it can merge unless it's blocked for another reason. Merge requests can be blocked by other problems, -such as merge conflicts, [pending discussions](../../../discussions/index.md#prevent-merge-unless-all-threads-are-resolved), +such as merge conflicts, [unresolved threads](../../../discussions/index.md#prevent-merge-unless-all-threads-are-resolved), or a [failed CI/CD pipeline](../merge_when_pipeline_succeeds.md). To prevent merge request authors from approving their own merge requests, diff --git a/lib/api/api.rb b/lib/api/api.rb index 04114d38b33..cdd0f90355a 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -175,6 +175,7 @@ module API mount ::API::Admin::InstanceClusters mount ::API::Appearance mount ::API::Applications + mount ::API::Badges mount ::API::BroadcastMessages mount ::API::BulkImports mount ::API::Ci::Jobs @@ -242,7 +243,6 @@ module API mount ::API::AlertManagementAlerts mount ::API::Avatar mount ::API::AwardEmoji - mount ::API::Badges mount ::API::Boards mount ::API::Branches mount ::API::Ci::JobArtifacts diff --git a/lib/api/badges.rb b/lib/api/badges.rb index 0a3f247ffd6..020ba53b9ee 100644 --- a/lib/api/badges.rb +++ b/lib/api/badges.rb @@ -28,6 +28,8 @@ module API desc "Gets a list of #{source_type} badges viewable by the authenticated user." do detail 'This feature was introduced in GitLab 10.6.' success Entities::Badge + is_array true + tags %w[badges] end params do use :pagination @@ -46,6 +48,7 @@ module API desc "Preview a badge from a #{source_type}." do detail 'This feature was introduced in GitLab 10.6.' success Entities::BasicBadgeDetails + tags %w[badges] end params do requires :link_url, type: String, desc: 'URL of the badge link' @@ -69,6 +72,7 @@ module API desc "Gets a badge of a #{source_type}." do detail 'This feature was introduced in GitLab 10.6.' success Entities::Badge + tags %w[badges] end params do requires :badge_id, type: Integer, desc: 'The badge ID' @@ -86,6 +90,7 @@ module API desc "Adds a badge to a #{source_type}." do detail 'This feature was introduced in GitLab 10.6.' success Entities::Badge + tags %w[badges] end params do requires :link_url, type: String, desc: 'URL of the badge link' @@ -107,6 +112,7 @@ module API desc "Updates a badge of a #{source_type}." do detail 'This feature was introduced in GitLab 10.6.' success Entities::Badge + tags %w[badges] end params do optional :link_url, type: String, desc: 'URL of the badge link' @@ -127,8 +133,9 @@ module API end end - desc 'Removes a badge from a project or group.' do + desc "Removes a badge from the #{source_type}." do detail 'This feature was introduced in GitLab 10.6.' + tags %w[badges] end params do requires :badge_id, type: Integer, desc: 'The badge ID' diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ddbd3220077..7326c9f81df 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6932,7 +6932,7 @@ msgstr "" msgid "BranchRules|Approvals" msgstr "" -msgid "BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Lean more.%{linkEnd}" +msgid "BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Learn more.%{linkEnd}" msgstr "" msgid "BranchRules|Branch" @@ -6944,7 +6944,7 @@ msgstr "" msgid "BranchRules|Branch rules details" msgstr "" -msgid "BranchRules|Check for a status response in merge requests. Failures do not block merges. %{linkStart}Lean more.%{linkEnd}" +msgid "BranchRules|Check for a status response in merge requests. Failures do not block merges. %{linkStart}Learn more.%{linkEnd}" msgstr "" msgid "BranchRules|Create wildcard: %{searchTerm}" diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js index 8f3d1f9bf92..131b4b99bb2 100644 --- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js +++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js @@ -1,10 +1,12 @@ -import { GlLoadingIcon, GlTable, GlLink, GlBadge, GlPagination } from '@gitlab/ui'; +import { GlLoadingIcon, GlTable, GlLink, GlBadge, GlPagination, GlModal } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import waitForPromises from 'helpers/wait_for_promises'; import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue'; +import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue'; +import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql'; @@ -23,7 +25,14 @@ describe('JobArtifactsTable component', () => { const findLoadingState = () => wrapper.findComponent(GlLoadingIcon); const findTable = () => wrapper.findComponent(GlTable); + const findDetailsRows = () => wrapper.findAllComponents(ArtifactsTableRowDetails); + const findDetailsInRow = (i) => + findTable().findAll('tbody tr').at(i).findComponent(ArtifactsTableRowDetails); + const findCount = () => wrapper.findByTestId('job-artifacts-count'); + const findCountAt = (i) => wrapper.findAllByTestId('job-artifacts-count').at(i); + + const findModal = () => wrapper.findComponent(GlModal); const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status'); const findSuccessfulJobStatus = () => findStatuses().at(0); @@ -41,6 +50,7 @@ describe('JobArtifactsTable component', () => { const findDownloadButton = () => wrapper.findByTestId('job-artifacts-download-button'); const findBrowseButton = () => wrapper.findByTestId('job-artifacts-browse-button'); const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button'); + const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button'); const findPagination = () => wrapper.findComponent(GlPagination); const setPage = async (page) => { @@ -121,14 +131,6 @@ describe('JobArtifactsTable component', () => { expect(findCount().text()).toBe(`${job.artifacts.nodes.length} files`); }); - it('expands to show the list of artifacts', async () => { - jest.spyOn(wrapper.vm, 'handleRowToggle'); - - findCount().trigger('click'); - - expect(wrapper.vm.handleRowToggle).toHaveBeenCalled(); - }); - it('shows the job status as an icon for a successful job', () => { expect(findSuccessfulJobStatus().findComponent(CiIcon).exists()).toBe(true); expect(findSuccessfulJobStatus().findComponent(GlBadge).exists()).toBe(false); @@ -160,6 +162,72 @@ describe('JobArtifactsTable component', () => { it('shows the created time', () => { expect(findCreated().text()).toBe('5 years ago'); }); + + describe('row expansion', () => { + it('toggles the visibility of the row details', async () => { + expect(findDetailsRows().length).toBe(0); + + findCount().trigger('click'); + await waitForPromises(); + + expect(findDetailsRows().length).toBe(1); + + findCount().trigger('click'); + await waitForPromises(); + + expect(findDetailsRows().length).toBe(0); + }); + + it('expands and collapses jobs', async () => { + // both jobs start collapsed + expect(findDetailsInRow(0).exists()).toBe(false); + expect(findDetailsInRow(1).exists()).toBe(false); + + findCountAt(0).trigger('click'); + await waitForPromises(); + + // first job is expanded, second row has its details + expect(findDetailsInRow(0).exists()).toBe(false); + expect(findDetailsInRow(1).exists()).toBe(true); + expect(findDetailsInRow(2).exists()).toBe(false); + + findCountAt(1).trigger('click'); + await waitForPromises(); + + // both jobs are expanded, each has details below it + expect(findDetailsInRow(0).exists()).toBe(false); + expect(findDetailsInRow(1).exists()).toBe(true); + expect(findDetailsInRow(2).exists()).toBe(false); + expect(findDetailsInRow(3).exists()).toBe(true); + + findCountAt(0).trigger('click'); + await waitForPromises(); + + // first job collapsed, second job expanded + expect(findDetailsInRow(0).exists()).toBe(false); + expect(findDetailsInRow(1).exists()).toBe(false); + expect(findDetailsInRow(2).exists()).toBe(true); + }); + + it('keeps the job expanded when an artifact is deleted', async () => { + findCount().trigger('click'); + await waitForPromises(); + + expect(findDetailsInRow(0).exists()).toBe(false); + expect(findDetailsInRow(1).exists()).toBe(true); + + findArtifactDeleteButton().trigger('click'); + await waitForPromises(); + + expect(findModal().props('visible')).toBe(true); + + wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary'); + await waitForPromises(); + + expect(findDetailsInRow(0).exists()).toBe(false); + expect(findDetailsInRow(1).exists()).toBe(true); + }); + }); }); describe('download button', () => { @@ -179,7 +247,7 @@ describe('JobArtifactsTable component', () => { createComponent( { getJobArtifactsQuery: jest.fn() }, - { jobArtifacts: { nodes: [jobWithoutDownloadPath] } }, + { jobArtifacts: [jobWithoutDownloadPath] }, ); await waitForPromises(); @@ -205,7 +273,7 @@ describe('JobArtifactsTable component', () => { createComponent( { getJobArtifactsQuery: jest.fn() }, - { jobArtifacts: { nodes: [jobWithoutBrowsePath] } }, + { jobArtifacts: [jobWithoutBrowsePath] }, ); await waitForPromises(); @@ -233,10 +301,8 @@ describe('JobArtifactsTable component', () => { getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates), }, { - jobArtifacts: { - count: enoughJobsToPaginate.length, - pageInfo, - }, + count: enoughJobsToPaginate.length, + pageInfo, }, ); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js index 875d3dafcc6..27065a704e2 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js @@ -12,7 +12,11 @@ import { import Protection from '~/projects/settings/branch_rules/components/view/protection.vue'; import branchRulesQuery from '~/projects/settings/branch_rules/queries/branch_rules_details.query.graphql'; import { sprintf } from '~/locale'; -import { branchProtectionsMockResponse, approvalRulesMock } from './mock_data'; +import { + branchProtectionsMockResponse, + approvalRulesMock, + statusChecksRulesMock, +} from './mock_data'; jest.mock('~/lib/utils/url_utility', () => ({ getParameterByName: jest.fn().mockReturnValue('main'), @@ -118,11 +122,10 @@ describe('View branch rules', () => { expect(findStatusChecksTitle().exists()).toBe(true); expect(findBranchProtections().at(3).props()).toMatchObject({ - // status checks BE/FE integration will happen on a follow-up, so we expect data to be empty - header: sprintf(I18N.statusChecksHeader, { total: 0 }), + header: sprintf(I18N.statusChecksHeader, { total: 2 }), headerLinkHref: statusChecksPath, headerLinkTitle: I18N.statusChecksLinkTitle, - statusChecks: [], + statusChecks: statusChecksRulesMock, }); }); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js index 326684a2bcc..c07d4673344 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js @@ -137,6 +137,10 @@ export const branchProtectionsMockResponse = { __typename: 'ApprovalProjectRuleConnection', nodes: approvalRulesMock, }, + externalStatusChecks: { + __typename: 'ExternalStatusCheckConnection', + nodes: statusChecksRulesMock, + }, }, { __typename: 'BranchRule', @@ -158,6 +162,10 @@ export const branchProtectionsMockResponse = { __typename: 'ApprovalProjectRuleConnection', nodes: [], }, + externalStatusChecks: { + __typename: 'ExternalStatusCheckConnection', + nodes: [], + }, }, ], }, |