diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-14 21:13:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-14 21:13:24 +0000 |
commit | 6df3cf6b4acbfe83f7a19e40bbd1471af5d6ce95 (patch) | |
tree | ad732f3108fb02f1e8f95f13cc87edaedbb728b1 | |
parent | 5b62f8e3ee531f63ce3c49cae03e2a618ba51615 (diff) | |
download | gitlab-ce-6df3cf6b4acbfe83f7a19e40bbd1471af5d6ce95.tar.gz |
Add latest changes from gitlab-org/gitlab@master
76 files changed, 1645 insertions, 601 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 3c9742f59e6..8586d0af0fc 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -11,7 +11,7 @@ include: - local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml - local: .gitlab/ci/package-and-test/variables.gitlab-ci.yml - project: gitlab-org/quality/pipeline-common - ref: 3.0.0 + ref: 3.1.0 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index ab6737b8278..4e794d972dc 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -1,6 +1,6 @@ include: - project: gitlab-org/quality/pipeline-common - ref: 3.0.0 + ref: 3.1.0 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml diff --git a/.gitlab/issue_templates/AI Project Proposal.md b/.gitlab/issue_templates/AI Project Proposal.md index 48c5c0ac7ba..1b3202b4028 100644 --- a/.gitlab/issue_templates/AI Project Proposal.md +++ b/.gitlab/issue_templates/AI Project Proposal.md @@ -4,31 +4,35 @@ The title should be something that is easily understood that quickly communicate A proposal title should combine the beneficiary of the feature/UI, the job it will allow them to accomplish, and their expected outcome when the work is delivered. Well-defined statements are concise without sacrificing the substance of the proposal so that anyone can understand it at a glance. (e.g.🤖 {Reduce the effort} + {for security teams} + {when prioritizing business-critical risks in their assets}) --> -# Experiment/Prototype +# [Experiment](https://docs.gitlab.com/ee/policy/alpha-beta-support.html#experiment) + ## Problem to be solved + ### User problem _What user problem will this solve?_ -#### Solution hypothesis +### Solution hypothesis _Why do you believe this AI solution is a good way to solve this problem?_ ### Assumption _What assumptions are you making about this problem and the solution?_ -### [Personas](https://about.gitlab.com/handbook/product/personas/#list-of-user-personas) -_What personas have this problem, who is the intended user?_ +### Personas +_What [personas](https://about.gitlab.com/handbook/product/personas/#list-of-user-personas) have this problem, who is the intended user?_ ## Proposal <!-- Use this section to explain the proposed changes, including details around usage and business drivers. --> + ### Success _How will you measure whether this experiment is a success?_ -# Feature release -### Main Job story +# [General Availability](https://docs.gitlab.com/ee/policy/alpha-beta-support.html#generally-available-ga) + +## Main Job story _What job to be done will this solve?_ <!-- What is the [Main Job story](https://about.gitlab.com/handbook/product/ux/jobs-to-be-done/#how-to-write-a-jtbd) that this proposal was derived from? (e.g. When I am on triage rotation, I want to address all the business-critical risks in my assets, So I can minimize the likelihood of my organization being compromised by a security breach.) --> -## Proposal updates/additions +### Proposal updates/additions <!-- Use this section to explain any changes or updates to the original proposal, including details around usage, business drivers, and reasonings that drove the updates/additions. --> ### Problem validation @@ -42,15 +46,15 @@ _What business objective will be achieved with this proposal?_ _Has this proposal been derived from research?_ <!-- How well do we understand the user's problem and their need? Refer to https://about.gitlab.com/handbook/product/ux/product-design/ux-roadmaps/#confidence to assess confidence --> -| Confidence | Research | -| --- | --- | +| Confidence | Research | +| ----------------- | ------------------------------ | | [High/Medium/Low] | [research/insight issue](Link) | ### Requirements _What tasks or actions should the user be capable of performing with this feature?_ <!-- Requirements can be taken from existing features or design issues used to build this proposal. Any related issues should be linked with this issue in the Feature/solution issues section below. They are more granular validated needs, goals, and additional details that the proposal encompasses. --> ->⚠️ Related feature and research issues should be linked in the related issues section (Delete this line when this is done) +> ⚠️ Related feature and research issues should be linked in the related issues section (Delete this line when this is done) #### The user needs to be able to: - ... @@ -58,50 +62,64 @@ _What tasks or actions should the user be capable of performing with this featur - ... ## Checklist + ### Experiment -<details> <summary> Issue information </summary> -- [ ] Add information to the issue body about: - - [ ] The user problem being solved - - [ ] Your assumptions - - [ ] Who it's for, list of personas impacted - - [ ] Your proposal -- [ ] Add relevant designs to the Design Management area of the issue if available -- [ ] Ensure this issue has the ~wg-ai-integration label to ensure visibility to various teams working on this + +<details> +<summary> Issue information </summary> + +- [ ] Add information to the issue body about: + - [ ] The user problem being solved + - [ ] Your assumptions + - [ ] Who it's for, list of personas impacted + - [ ] Your proposal +- [ ] Add relevant designs to the Design Management area of the issue if available +- [ ] Ensure this issue has the ~wg-ai-integration label to ensure visibility to various teams working on this + </details> -### Feature release -<details> <summary> Issue information </summary> -- [ ] Add information to the issue body about: - - [ ] Your proposal - - [ ] The Job Statement it's expected to satisfy - - [ ] Details about the user problem and provide any research or problem validation - - [ ] List the personas impacted by the proposal. -- [ ] Add all relevant solution validation issues to the Linked items section that shows this proposal will solve the customer problem, or details explaining why it's not possible to provide that validation. -- [ ] Add relevant designs to the Design Management area of the issue. -- [ ] You have adhered to our [Definition of Done](https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html#definition-of-done) standards -- [ ] Ensure this issue has the ~wg-ai-integration label to ensure visibility to various teams working on this +### General Availability + +<details> +<summary>Issue information</summary> + +- [ ] Add information to the issue body about: + - [ ] Your proposal + - [ ] The Job Statement it's expected to satisfy + - [ ] Details about the user problem and provide any research or problem validation + - [ ] List the personas impacted by the proposal. +- [ ] Add all relevant solution validation issues to the Linked items section that shows this proposal will solve the customer problem, or details explaining why it's not possible to provide that validation. +- [ ] Add relevant designs to the Design Management area of the issue. +- [ ] You have adhered to our [Definition of Done](https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html#definition-of-done) standards +- [ ] Ensure this issue has the ~wg-ai-integration label to ensure visibility to various teams working on this + </details> -<details> <summary> Technical needs </summary> -- [ ] [https://gitlab.com/gitlab-org/gitlab/-/issues/403859#note_1337519985](https://gitlab.com/gitlab-org/gitlab/-/issues/403859#note_1337519985)+s -1. Work estimate and skills needs to build an ML viable feature. -- To build any ML feature depending on the work, there are many personas that contribute including, Data Scientist, NLP engineer, ML Engineer, MLOps Engineer, ML Infra engineers, and Fullstack engineer to integrate the ML Services with Gitlab. Post-prototype we would assess the skills needed to build a production-grade ML feature for the prototype -2. Data Limitation -- We would like to upfront validate if we have viable data for the feature including whether we can use the DataOps pipeline of ModelOps or create a custom one. We would want to understand the training data, test data, and feedback data to dial up the accuracy and the limitations of the data. -3. Model Limitation --We would want to understand if we can use an open-source pre-trained model, tune and customize it or start a model from scratch as well. Further, we would asses based on the ModelOps model evaluation framework which would be the right model to use based on the use case. -4. Cost, Scalability, Reliability --We would want to estimate the cost of hosting, serving, inference of the model, and the full end-to-end infrastructure including monitoring and observability. -5. Legal and Ethical Framework --We would want to align with legal and ethical framework like any other ModelOps features to cover across the nine principles of responsible ML and any legal support needed. +<details> +<summary>Technical needs</summary> + +- [ ] [Operational Requirements Review - Checklist - #note_1337519985](https://gitlab.com/gitlab-org/gitlab/-/issues/403859#note_1337519985) + +1. **Work estimate and skills needs to build an ML viable feature:** To build any ML feature depending on the work, there are many personas that contribute including, Data Scientist, NLP engineer, ML Engineer, MLOps Engineer, ML Infra engineers, and Fullstack engineer to integrate the ML Services with Gitlab. Post-prototype we would assess the skills needed to build a production-grade ML feature for the prototype +2. **Data Limitation:** We would like to upfront validate if we have viable data for the feature including whether we can use the DataOps pipeline of ModelOps or create a custom one. We would want to understand the training data, test data, and feedback data to dial up the accuracy and the limitations of the data. +3. **Model Limitation:** We would want to understand if we can use an open-source pre-trained model, tune and customize it or start a model from scratch as well. Further, we would asses based on the ModelOps model evaluation framework which would be the right model to use based on the use case. +4. **Cost, Scalability, Reliability:** We would want to estimate the cost of hosting, serving, inference of the model, and the full end-to-end infrastructure including monitoring and observability. +5. **Legal and Ethical Framework:** We would want to align with legal and ethical framework like any other ModelOps features to cover across the nine principles of responsible ML and any legal support needed. + </details> -<details> <summary> Dependency needs </summary> -- [ ] [https://gitlab.com/gitlab-org/gitlab/-/issues/403859#note_1337519985](https://gitlab.com/gitlab-org/gitlab/-/issues/403859#note_1337519985)+s +<details> +<summary>Dependency needs</summary> + +- [ ] [Operational Requirements Review - Checklist - #note_1337519985](https://gitlab.com/gitlab-org/gitlab/-/issues/403859#note_1337519985) + </details> -<details> <summary> Legal needs </summary> -- [ ] TBD +<details> +<summary>Legal needs</summary> + +- [ ] TBD + </details> ## Additional resources @@ -117,4 +135,4 @@ _What tasks or actions should the user be capable of performing with this featur /cc @tmccaslin @hbenson @wayne @pedroms @jmandell /confidential -Make change to this template here: https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/AI%20Project%20Proposal.md
\ No newline at end of file +[Make change to this template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/AI%20Project%20Proposal.md) diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue index cfb18cc4f82..736eaa7062d 100644 --- a/app/assets/javascripts/environments/components/kubernetes_overview.vue +++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue @@ -1,14 +1,20 @@ <script> -import { GlCollapse, GlButton } from '@gitlab/ui'; +import { GlCollapse, GlButton, GlAlert } from '@gitlab/ui'; import { __, s__ } from '~/locale'; +import csrf from '~/lib/utils/csrf'; +import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils'; import KubernetesAgentInfo from './kubernetes_agent_info.vue'; +import KubernetesPods from './kubernetes_pods.vue'; export default { components: { GlCollapse, GlButton, + GlAlert, KubernetesAgentInfo, + KubernetesPods, }, + inject: ['kasTunnelUrl'], props: { agentName: { required: true, @@ -22,10 +28,16 @@ export default { required: true, type: String, }, + namespace: { + required: false, + type: String, + default: '', + }, }, data() { return { isVisible: false, + error: '', }; }, computed: { @@ -35,11 +47,26 @@ export default { label() { return this.isVisible ? this.$options.i18n.collapse : this.$options.i18n.expand; }, + gitlabAgentId() { + const id = isGid(this.agentId) ? getIdFromGraphQLId(this.agentId) : this.agentId; + return id.toString(); + }, + k8sAccessConfiguration() { + return { + basePath: this.kasTunnelUrl, + baseOptions: { + headers: { 'GitLab-Agent-Id': this.gitlabAgentId, ...csrf.headers }, + }, + }; + }, }, methods: { toggleCollapse() { this.isVisible = !this.isVisible; }, + onClusterError(message) { + this.error = message; + }, }, i18n: { collapse: __('Collapse'), @@ -66,7 +93,17 @@ export default { :agent-name="agentName" :agent-id="agentId" :agent-project-path="agentProjectPath" + class="gl-mb-5" /> + + <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-5"> + {{ error }} + </gl-alert> + + <kubernetes-pods + :configuration="k8sAccessConfiguration" + :namespace="namespace" class="gl-mb-5" + @cluster-error="onClusterError" /></template> </gl-collapse> </div> diff --git a/app/assets/javascripts/environments/components/kubernetes_pods.vue b/app/assets/javascripts/environments/components/kubernetes_pods.vue new file mode 100644 index 00000000000..e43bc838708 --- /dev/null +++ b/app/assets/javascripts/environments/components/kubernetes_pods.vue @@ -0,0 +1,111 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import { s__ } from '~/locale'; +import k8sPodsQuery from '../graphql/queries/k8s_pods.query.graphql'; + +export default { + components: { + GlLoadingIcon, + GlSingleStat, + }, + apollo: { + k8sPods: { + query: k8sPodsQuery, + variables() { + return { + configuration: this.configuration, + namespace: this.namespace, + }; + }, + update(data) { + return data?.k8sPods || []; + }, + error(error) { + this.error = error; + this.$emit('cluster-error', this.error); + }, + }, + }, + props: { + configuration: { + required: true, + type: Object, + }, + namespace: { + required: true, + type: String, + }, + }, + data() { + return { + error: '', + }; + }, + + computed: { + podStats() { + if (!this.k8sPods) return null; + + return [ + { + // eslint-disable-next-line @gitlab/require-i18n-strings + value: this.getPodsByPhase('Running'), + title: this.$options.i18n.runningPods, + }, + { + // eslint-disable-next-line @gitlab/require-i18n-strings + value: this.getPodsByPhase('Pending'), + title: this.$options.i18n.pendingPods, + }, + { + // eslint-disable-next-line @gitlab/require-i18n-strings + value: this.getPodsByPhase('Succeeded'), + title: this.$options.i18n.succeededPods, + }, + { + // eslint-disable-next-line @gitlab/require-i18n-strings + value: this.getPodsByPhase('Failed'), + title: this.$options.i18n.failedPods, + }, + ]; + }, + loading() { + return this.$apollo.queries.k8sPods.loading; + }, + }, + methods: { + getPodsByPhase(phase) { + const filteredPods = this.k8sPods.filter((item) => item.status.phase === phase); + return filteredPods.length; + }, + }, + i18n: { + podsTitle: s__('Environment|Pods'), + runningPods: s__('Environment|Running'), + pendingPods: s__('Environment|Pending'), + succeededPods: s__('Environment|Succeeded'), + failedPods: s__('Environment|Failed'), + }, +}; +</script> +<template> + <div> + <p class="gl-text-gray-500">{{ $options.i18n.podsTitle }}</p> + + <gl-loading-icon v-if="loading" /> + + <div + v-else-if="podStats && !error" + class="gl-display-flex gl-flex-wrap-wrap gl-sm-flex-wrap-nowrap gl-mx-n3 gl-mt-n3" + > + <gl-single-stat + v-for="(stat, index) in podStats" + :key="index" + class="gl-w-full gl-flex-direction-column gl-align-items-center gl-justify-content-center gl-bg-white gl-border gl-border-gray-a-08 gl-mx-3 gl-p-3 gl-mt-3" + :value="stat.value" + :title="stat.title" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue index f46bd0e3780..b5ef7d00cc3 100644 --- a/app/assets/javascripts/environments/components/new_environment_item.vue +++ b/app/assets/javascripts/environments/components/new_environment_item.vue @@ -173,7 +173,8 @@ export default { return this.glFeatures?.kasUserAccessProject; }, hasRequiredAgentData() { - return this.agent.project && this.agent.id && this.agent.name; + const { project, id, name } = this.agent || {}; + return project && id && name; }, showKubernetesOverview() { return this.isKubernetesOverviewAvailable && this.hasRequiredAgentData; @@ -367,6 +368,7 @@ export default { :agent-project-path="agent.project" :agent-name="agent.name" :agent-id="agent.id" + :namespace="agent.kubernetesNamespace" /> </div> <div v-if="rolloutStatus" :class="$options.deployBoardClasses"> diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js index 26514b59995..0482741979b 100644 --- a/app/assets/javascripts/environments/graphql/client.js +++ b/app/assets/javascripts/environments/graphql/client.js @@ -5,6 +5,7 @@ import pageInfoQuery from './queries/page_info.query.graphql'; import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql'; import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql'; import environmentToStopQuery from './queries/environment_to_stop.query.graphql'; +import k8sPodsQuery from './queries/k8s_pods.query.graphql'; import { resolvers } from './resolvers'; import typeDefs from './typedefs.graphql'; @@ -82,6 +83,14 @@ export const apolloProvider = (endpoint) => { }, }, }); + cache.writeQuery({ + query: k8sPodsQuery, + data: { + status: { + phase: '', + }, + }, + }); return new VueApollo({ defaultClient, }); diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql new file mode 100644 index 00000000000..818bca24d51 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/k8s_pods.query.graphql @@ -0,0 +1,7 @@ +query getK8sPods($configuration: Object, $namespace: String) { + k8sPods(configuration: $configuration, namespace: $namespace) @client { + status { + phase + } + } +} diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js index e21670870b8..39e05825cf0 100644 --- a/app/assets/javascripts/environments/graphql/resolvers.js +++ b/app/assets/javascripts/environments/graphql/resolvers.js @@ -1,3 +1,4 @@ +import { CoreV1Api, Configuration } from '@gitlab/cluster-client'; import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; import { @@ -71,6 +72,19 @@ export const resolvers = (endpoint) => ({ isLastDeployment(_, { environment }) { return environment?.lastDeployment?.isLast; }, + k8sPods(_, { configuration, namespace }) { + const coreV1Api = new CoreV1Api(new Configuration(configuration)); + const podsApi = namespace + ? coreV1Api.listCoreV1NamespacedPod(namespace) + : coreV1Api.listCoreV1PodForAllNamespaces(); + + return podsApi + .then((res) => res?.data?.items || []) + .catch((err) => { + const error = err?.response?.data?.message ? new Error(err.response.data.message) : err; + throw error; + }); + }, }, Mutation: { stopEnvironment(_, { environment }, { client }) { diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql index b4d1f7326f6..7c102fd04d8 100644 --- a/app/assets/javascripts/environments/graphql/typedefs.graphql +++ b/app/assets/javascripts/environments/graphql/typedefs.graphql @@ -62,6 +62,19 @@ type LocalPageInfo { previousPage: Int! } +type k8sPodStatus { + phase: String +} + +type LocalK8sPods { + status: k8sPodStatus +} + +input LocalConfiguration { + basePath: String + baseOptions: JSON +} + extend type Query { environmentApp(page: Int, scope: String): LocalEnvironmentApp folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder @@ -71,6 +84,7 @@ extend type Query { environmentToStop: LocalEnvironment isEnvironmentStopping(environment: LocalEnvironmentInput): Boolean isLastDeployment(environment: LocalEnvironmentInput): Boolean + k8sPods(configuration: LocalConfiguration, namespace: String): [LocalK8sPods] } extend type Mutation { diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js index d9a523fd806..3f746bc5383 100644 --- a/app/assets/javascripts/environments/index.js +++ b/app/assets/javascripts/environments/index.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { removeLastSlashInUrlPath } from '~/lib/utils/url_utility'; import { parseBoolean } from '../lib/utils/common_utils'; import { apolloProvider } from './graphql/client'; import EnvironmentsApp from './components/environments_app.vue'; @@ -16,6 +17,7 @@ export default (el) => { projectPath, defaultBranchName, projectId, + kasTunnelUrl, } = el.dataset; return new Vue({ @@ -28,6 +30,7 @@ export default (el) => { newEnvironmentPath, helpPagePath, projectId, + kasTunnelUrl: removeLastSlashInUrlPath(kasTunnelUrl), canCreateEnvironment: parseBoolean(canCreateEnvironment), }, render(h) { diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue index 3c765de92a2..23b58543f11 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue +++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue @@ -2,6 +2,7 @@ import { GlLink } from '@gitlab/ui'; import { FEATURE_NAME, FEATURE_FEEDBACK_ISSUE } from '~/ml/experiment_tracking/constants'; import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue'; +import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue'; import { TITLE_LABEL, INFO_LABEL, @@ -12,12 +13,16 @@ import { PARAMETERS_LABEL, METRICS_LABEL, METADATA_LABEL, + DELETE_CANDIDATE_CONFIRMATION_MESSAGE, + DELETE_CANDIDATE_PRIMARY_ACTION_LABEL, + DELETE_CANDIDATE_MODAL_TITLE, } from './translations'; export default { name: 'MlCandidatesShow', components: { IncubationAlert, + DeleteButton, GlLink, }, props: { @@ -36,6 +41,9 @@ export default { PARAMETERS_LABEL, METRICS_LABEL, METADATA_LABEL, + DELETE_CANDIDATE_CONFIRMATION_MESSAGE, + DELETE_CANDIDATE_PRIMARY_ACTION_LABEL, + DELETE_CANDIDATE_MODAL_TITLE, }, computed: { sections() { @@ -67,11 +75,22 @@ export default { :link-to-feedback-issue="$options.FEATURE_FEEDBACK_ISSUE" /> - <h3> - {{ $options.i18n.TITLE_LABEL }} - </h3> + <div class="detail-page-header gl-flex-wrap"> + <div class="detail-page-header-body"> + <h1 class="page-title gl-font-size-h-display flex-fill"> + {{ $options.i18n.TITLE_LABEL }} + </h1> - <table class="candidate-details"> + <delete-button + :delete-path="candidate.info.path" + :delete-confirmation-text="$options.i18n.DELETE_CANDIDATE_CONFIRMATION_MESSAGE" + :action-primary-text="$options.i18n.DELETE_CANDIDATE_PRIMARY_ACTION_LABEL" + :modal-title="$options.i18n.DELETE_CANDIDATE_MODAL_TITLE" + /> + </div> + </div> + + <table class="candidate-details gl-w-full"> <tbody> <tr class="divider"></tr> diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js index caad145873e..5f7714aa0c0 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js +++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js @@ -9,3 +9,8 @@ export const ARTIFACTS_LABEL = s__('MlExperimentTracking|Artifacts'); export const PARAMETERS_LABEL = s__('MlExperimentTracking|Parameters'); export const METRICS_LABEL = s__('MlExperimentTracking|Metrics'); export const METADATA_LABEL = s__('MlExperimentTracking|Metadata'); +export const DELETE_CANDIDATE_CONFIRMATION_MESSAGE = s__( + 'MlExperimentTracking|Deleting this candidate will delete the associated parameters, metrics, and metadata.', +); +export const DELETE_CANDIDATE_PRIMARY_ACTION_LABEL = s__('MlExperimentTracking|Delete candidate'); +export const DELETE_CANDIDATE_MODAL_TITLE = s__('MLExperimentTracking|Delete candidate?'); diff --git a/app/assets/javascripts/pages/admin/jobs/components/constants.js b/app/assets/javascripts/pages/admin/jobs/components/constants.js index cfde1fc0a2b..84be895e194 100644 --- a/app/assets/javascripts/pages/admin/jobs/components/constants.js +++ b/app/assets/javascripts/pages/admin/jobs/components/constants.js @@ -1,4 +1,5 @@ import { s__, __ } from '~/locale'; +import { DEFAULT_FIELDS } from '~/jobs/components/table/constants'; export const CANCEL_JOBS_MODAL_ID = 'cancel-jobs-modal'; export const CANCEL_JOBS_MODAL_TITLE = s__('AdminArea|Are you sure?'); @@ -10,3 +11,11 @@ export const PRIMARY_ACTION_TEXT = s__('AdminArea|Yes, proceed'); export const CANCEL_JOBS_WARNING = s__( "AdminArea|You're about to cancel all running and pending jobs across this instance. Do you want to proceed?", ); + +/* Admin Table constants */ +export const DEFAULT_FIELDS_ADMIN = [ + ...DEFAULT_FIELDS.slice(0, 2), + { key: 'project', label: __('Project'), columnClass: 'gl-w-20p' }, + { key: 'runner', label: __('Runner'), columnClass: 'gl-w-15p' }, + ...DEFAULT_FIELDS.slice(2), +]; diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue index c5a0509b625..6d5b4f24119 100644 --- a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue +++ b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue @@ -1,5 +1,16 @@ <script> +import { queryToObject } from '~/lib/utils/url_utility'; +import { validateQueryString } from '~/jobs/components/filtered_search/utils'; +import JobsTable from '~/jobs/components/table/jobs_table.vue'; +import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue'; +import { DEFAULT_FIELDS_ADMIN } from '../constants'; +import GetAllJobs from './graphql/queries/get_all_jobs.query.graphql'; + export default { + components: { + JobsTable, + JobsTableTabs, + }, inject: { jobStatuses: { default: null, @@ -11,9 +22,69 @@ export default { default: '', }, }, + apollo: { + jobs: { + query: GetAllJobs, + variables() { + return this.variables; + }, + update(data) { + const { jobs: { nodes: list = [], pageInfo = {}, count } = {} } = data || {}; + return { + list, + pageInfo, + count, + }; + }, + error() { + this.hasError = true; + }, + }, + }, + data() { + return { + jobs: { + list: [], + }, + hasError: false, + count: 0, + scope: null, + infiniteScrollingTriggered: false, + DEFAULT_FIELDS_ADMIN, + }; + }, + computed: { + loading() { + return this.$apollo.queries.jobs.loading; + }, + variables() { + return { ...this.validatedQueryString }; + }, + validatedQueryString() { + const queryStringObject = queryToObject(window.location.search); + + return validateQueryString(queryStringObject); + }, + jobsCount() { + return this.jobs.count; + }, + }, + watch: { + // this watcher ensures that the count on the all tab + // is not updated when switching to the finished tab + jobsCount(newCount) { + if (this.scope) return; + + this.count = newCount; + }, + }, }; </script> <template> - <div>{{ __('Jobs') }}</div> + <div> + <jobs-table-tabs :all-jobs-count="count" :loading="loading" /> + + <jobs-table :jobs="jobs.list" :table-fields="DEFAULT_FIELDS_ADMIN" /> + </div> </template> diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/cache_config.js b/app/assets/javascripts/pages/admin/jobs/components/table/graphql/cache_config.js new file mode 100644 index 00000000000..fd7ee2a6f8c --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/components/table/graphql/cache_config.js @@ -0,0 +1,62 @@ +import { isEqual } from 'lodash'; + +export default { + typePolicies: { + Query: { + fields: { + jobs: { + keyArgs: ['statuses'], + }, + }, + }, + CiJobConnection: { + merge(existing = {}, incoming, { args = {} }) { + if (incoming.nodes) { + let nodes; + + const areNodesEqual = isEqual(existing.nodes, incoming.nodes); + const statuses = Array.isArray(args.statuses) ? [...args.statuses] : args.statuses; + const { pageInfo } = incoming; + + if (Object.keys(existing).length !== 0 && isEqual(existing?.statuses, args?.statuses)) { + if (areNodesEqual) { + if (incoming.pageInfo.hasNextPage) { + nodes = [...existing.nodes, ...incoming.nodes]; + } else { + nodes = [...incoming.nodes]; + } + } else { + if (!existing.pageInfo?.hasNextPage) { + nodes = [...incoming.nodes]; + + return { + nodes, + statuses, + pageInfo, + count: incoming.count, + }; + } + + nodes = [...existing.nodes, ...incoming.nodes]; + } + } else { + nodes = [...incoming.nodes]; + } + + return { + nodes, + statuses, + pageInfo, + count: incoming.count, + }; + } + + return { + nodes: existing.nodes, + pageInfo: existing.pageInfo, + statuses: args.statuses, + }; + }, + }, + }, +}; diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql b/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql new file mode 100644 index 00000000000..374009efa15 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql @@ -0,0 +1,81 @@ +query getAllJobs($after: String, $first: Int = 50, $statuses: [CiJobStatus!]) { + jobs(after: $after, first: $first, statuses: $statuses) { + count + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + nodes { + artifacts { + nodes { + id + downloadPath + fileType + } + } + allowFailure + status + scheduledAt + manualJob + triggered + createdByTag + detailedStatus { + id + detailsPath + group + icon + label + text + tooltip + action { + id + buttonTitle + icon + method + path + title + } + } + id + refName + refPath + tags + shortSha + commitPath + pipeline { + id + project { + id + fullPath + webUrl + } + path + user { + id + webPath + avatarUrl + } + } + stage { + id + name + } + name + duration + finishedAt + coverage + retryable + playable + cancelable + active + stuck + userPermissions { + readBuild + readJobArtifacts + updateBuild + } + } + } +} diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js index 2297efd805e..9c2a255a1a3 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/index.js +++ b/app/assets/javascripts/pages/admin/jobs/index/index.js @@ -1,11 +1,21 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import Translate from '~/vue_shared/translate'; +import createDefaultClient from '~/lib/graphql'; import { CANCEL_JOBS_MODAL_ID } from '../components/constants'; import CancelJobsModal from '../components/cancel_jobs_modal.vue'; import AdminJobsTableApp from '../components/table/admin_jobs_table_app.vue'; +import cacheConfig from '../components/table/graphql/cache_config'; Vue.use(Translate); +Vue.use(VueApollo); + +const client = createDefaultClient({}, { cacheConfig }); + +const apolloProvider = new VueApollo({ + defaultClient: client, +}); function initJobs() { const buttonId = 'js-stop-jobs-button'; @@ -44,6 +54,7 @@ export function initAdminJobsApp() { return new Vue({ el: containerEl, + apolloProvider, provide: { url, emptyStateSvgPath, diff --git a/app/controllers/projects/ml/candidates_controller.rb b/app/controllers/projects/ml/candidates_controller.rb index ed465860004..e534000f494 100644 --- a/app/controllers/projects/ml/candidates_controller.rb +++ b/app/controllers/projects/ml/candidates_controller.rb @@ -10,9 +10,10 @@ module Projects def show; end def destroy + @experiment = @candidate.experiment @candidate.destroy! - redirect_to project_ml_experiments_path(@project), + redirect_to project_ml_experiment_path(@project, @experiment.iid), status: :found, notice: s_("MlExperimentTracking|Candidate removed") end diff --git a/app/helpers/projects/ml/experiments_helper.rb b/app/helpers/projects/ml/experiments_helper.rb index 927337da6bb..1f044ebed3b 100644 --- a/app/helpers/projects/ml/experiments_helper.rb +++ b/app/helpers/projects/ml/experiments_helper.rb @@ -15,6 +15,7 @@ module Projects path_to_artifact: link_to_artifact(candidate), experiment_name: candidate.experiment.name, path_to_experiment: link_to_experiment(candidate.project, candidate.experiment), + path: link_to_details(candidate), status: candidate.status }, metadata: candidate.metadata diff --git a/app/models/issue.rb b/app/models/issue.rb index d7b72fa9dad..77dcaf9336c 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -39,6 +39,8 @@ class Issue < ApplicationRecord DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze + IssueTypeOutOfSyncError = Class.new(StandardError) + SORTING_PREFERENCE_FIELD = :issues_sort MAX_BRANCH_TEMPLATE = 255 @@ -233,6 +235,7 @@ class Issue < ApplicationRecord scope :with_projects_matching_search_data, -> { where('issue_search_data.project_id = issues.project_id') } before_validation :ensure_namespace_id, :ensure_work_item_type + before_save :check_issue_type_in_sync! after_save :ensure_metrics!, unless: :importing? after_commit :expire_etag_cache, unless: :importing? @@ -724,6 +727,33 @@ class Issue < ApplicationRecord private + def check_issue_type_in_sync! + # We might have existing records out of sync, so we need to skip this check unless the value is changed + # so those records can still be updated until we fix them and remove the issue_type column + # https://gitlab.com/gitlab-org/gitlab/-/work_items/403158?iid_path=true + return unless (changes.keys & %w[issue_type work_item_type_id]).any? + + if issue_type != work_item_type.base_type + error = IssueTypeOutOfSyncError.new( + <<~ERROR + Issue `issue_type` out of sync with `work_item_type_id` column. + `issue_type` must be equal to `work_item.base_type`. + You can assign the correct work_item_type like this for example: + + Issue.new(issue_type: :incident, work_item_type: WorkItems::Type.default_by_type(:incident)) + + More details in https://gitlab.com/gitlab-org/gitlab/-/issues/338005 + ERROR + ) + + Gitlab::ErrorTracking.track_and_raise_for_dev_exception( + error, + issue_type: issue_type, + work_item_type_id: work_item_type_id + ) + end + end + def due_date_after_start_date return unless start_date.present? && due_date.present? diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 42807b4d7cf..05090efe260 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -124,11 +124,6 @@ module Issues def update_project_counter_caches?(issue) super || issue.confidential_changed? end - - override :allowed_create_params - def allowed_create_params(params) - super(params).except(:work_item_type_id, :work_item_type) - end end end diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb index 1b87c42be69..cb90aca5800 100644 --- a/app/services/issues/build_service.rb +++ b/app/services/issues/build_service.rb @@ -70,18 +70,6 @@ module Issues def issue_params @issue_params ||= build_issue_params - - if @issue_params[:work_item_type].present? - @issue_params[:issue_type] = @issue_params[:work_item_type].base_type - else - # If :issue_type is nil then params[:issue_type] was either nil - # or not permitted. Either way, the :issue_type will default - # to the column default of `issue`. And that means we need to - # ensure the work_item_type_id is set - @issue_params[:work_item_type_id] = get_work_item_type_id(@issue_params[:issue_type]) - end - - @issue_params end private @@ -98,11 +86,7 @@ module Issues :confidential ] - params[:work_item_type] = WorkItems::Type.find_by(id: params[:work_item_type_id]) if params[:work_item_type_id].present? # rubocop: disable CodeReuse/ActiveRecord - public_issue_params << :issue_type if create_issue_type_allowed?(container, params[:issue_type]) - base_type = params[:work_item_type]&.base_type - public_issue_params << :work_item_type if create_issue_type_allowed?(container, base_type) params.slice(*public_issue_params) end @@ -113,10 +97,6 @@ module Issues .merge(public_params) .with_indifferent_access end - - def get_work_item_type_id(issue_type = :issue) - find_work_item_type_id(issue_type) - end end end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index e652fec02e5..06977dec04b 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -27,6 +27,7 @@ module Issues # We should not initialize the callback classes during the build service execution because these will be # initialized when we call #create below @issue = @build_service.execute(initialize_callbacks: false) + set_work_item_type(@issue) # issue_type is set in BuildService, so we can delete it from params, in later phase # it can be set also from quick actions - in that case work_item_id is synced later again @@ -76,7 +77,6 @@ module Issues handle_escalation_status_change(issue) create_timeline_event(issue) try_to_associate_contacts(issue) - change_additional_attributes(issue) super end @@ -105,12 +105,24 @@ module Issues private - def handle_quick_actions(issue) - # Do not handle quick actions unless the work item is the default Issue. - # The available quick actions for a work item depend on its type and widgets. - return if @params[:work_item_type].present? && @params[:work_item_type] != WorkItems::Type.default_by_type(:issue) + def set_work_item_type(issue) + work_item_type = if params[:work_item_type_id].present? + params.delete(:work_item_type) + WorkItems::Type.find_by(id: params.delete(:work_item_type_id)) # rubocop: disable CodeReuse/ActiveRecord + else + params.delete(:work_item_type) + end + + base_type = work_item_type&.base_type + if create_issue_type_allowed?(container, base_type) + issue.work_item_type = work_item_type + # Up to this point issue_type might be set to the default, so we need to sync if a work item type is provided + issue.issue_type = work_item_type.base_type + end - super + # If no work item type was provided, we need to set it to whatever issue_type was up to this point, + # and that includes the column default + issue.work_item_type = WorkItems::Type.default_by_type(issue.issue_type) end def authorization_action @@ -144,15 +156,6 @@ module Issues set_crm_contacts(issue, contacts) end - - override :change_additional_attributes - def change_additional_attributes(issue) - super - - # issue_type can be still set through quick actions, in that case - # we have to make sure to re-sync work_item_type with it - issue.work_item_type_id = find_work_item_type_id(params[:issue_type]) if params[:issue_type] - end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index b64826d5357..877f0a73c82 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -115,14 +115,6 @@ module Issues attr_reader :spam_params - def handle_quick_actions(issue) - # Do not handle quick actions unless the work item is the default Issue. - # The available quick actions for a work item depend on its type and widgets. - return unless issue.work_item_type.default_issue? - - super - end - def handle_date_changes(issue) return unless issue.previous_changes.slice('due_date', 'start_date').any? diff --git a/app/services/work_items/create_service.rb b/app/services/work_items/create_service.rb index eff2132039f..ae355dc6d96 100644 --- a/app/services/work_items/create_service.rb +++ b/app/services/work_items/create_service.rb @@ -2,6 +2,7 @@ module WorkItems class CreateService < Issues::CreateService + extend ::Gitlab::Utils::Override include WidgetableService def initialize(container:, spam_params:, current_user: nil, params: {}, widget_params: {}) @@ -48,6 +49,15 @@ module WorkItems private + override :handle_quick_actions + def handle_quick_actions(work_item) + # Do not handle quick actions unless the work item is the default Issue. + # The available quick actions for a work item depend on its type and widgets. + return if work_item.work_item_type != WorkItems::Type.default_by_type(:issue) + + super + end + def authorization_action :create_work_item end diff --git a/app/services/work_items/update_service.rb b/app/services/work_items/update_service.rb index d4acadbc851..defdeebfed8 100644 --- a/app/services/work_items/update_service.rb +++ b/app/services/work_items/update_service.rb @@ -2,6 +2,7 @@ module WorkItems class UpdateService < ::Issues::UpdateService + extend Gitlab::Utils::Override include WidgetableService def initialize(container:, current_user: nil, params: {}, spam_params: nil, widget_params: {}) @@ -26,6 +27,15 @@ module WorkItems private + override :handle_quick_actions + def handle_quick_actions(work_item) + # Do not handle quick actions unless the work item is the default Issue. + # The available quick actions for a work item depend on its type and widgets. + return unless work_item.work_item_type.default_issue? + + super + end + def prepare_update_params(work_item) execute_widgets( work_item: work_item, diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index e4b8750b96c..7ddaf868a35 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -8,4 +8,5 @@ "help-page-path" => help_page_path("ci/environments/index.md"), "project-path" => @project.full_path, "project-id" => @project.id, - "default-branch-name" => @project.default_branch_or_main } } + "default-branch-name" => @project.default_branch_or_main, + "kas-tunnel-url" => ::Gitlab::Kas.tunnel_url } } diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml index 92b0a5a0b90..b8ee62055f0 100644 --- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml +++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml @@ -1,7 +1,7 @@ - display_issuable_type = issuable_display_type(@merge_request) .btn-group.gl-md-ml-3.gl-display-flex.dropdown.gl-dropdown.gl-md-w-auto.gl-w-full - = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret has-tooltip gl-display-none! gl-md-display-inline-flex!", data: { toggle: 'dropdown', title: _('Merge request actions'), testid: 'merge-request-actions', 'aria-label': _('Merge request actions') } do + = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret gl-display-none! gl-md-display-inline-flex!", title: _('Merge request actions'), 'aria-label': _('Merge request actions'), data: { toggle: 'dropdown', testid: 'merge-request-actions' } do = sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon" = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do %span.gl-dropdown-button-text= _('Merge request actions') diff --git a/config/feature_flags/development/summarize_diff_quick_action.yml b/config/feature_flags/development/summarize_diff_quick_action.yml new file mode 100644 index 00000000000..ebbfaba2dba --- /dev/null +++ b/config/feature_flags/development/summarize_diff_quick_action.yml @@ -0,0 +1,8 @@ +--- +name: summarize_diff_quick_action +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117458 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407256 +milestone: '15.11' +type: development +group: group::code review +default_enabled: false diff --git a/doc/api/dependencies.md b/doc/api/dependencies.md index 47db8645be1..5ae36926868 100644 --- a/doc/api/dependencies.md +++ b/doc/api/dependencies.md @@ -34,7 +34,7 @@ GET /projects/:id/dependencies?package_manager=yarn,bundler | Attribute | Type | Required | Description | | ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). | -| `package_manager` | string array | no | Returns dependencies belonging to specified package manager. Valid values: `bundler`, `composer`, `conan`, `go`, `gradle`, `maven`, `npm`, `nuget`, `pip`, `pipenv`, `yarn`, `sbt`, or `setuptools`. | +| `package_manager` | string array | no | Returns dependencies belonging to specified package manager. Valid values: `bundler`, `composer`, `conan`, `go`, `gradle`, `maven`, `npm`, `nuget`, `pip`, `pipenv`, `pnpm`, `yarn`, `sbt`, or `setuptools`. | ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/dependencies" diff --git a/jest.config.base.js b/jest.config.base.js index e8ba15f0a09..3cbf2fdd61b 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -199,6 +199,7 @@ module.exports = (path, options = {}) => { 'vue-test-utils-compat', '@gitlab/ui', '@gitlab/favicon-overlay', + '@gitlab/cluster-client', 'bootstrap-vue', 'three', 'monaco-editor', diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb index ae8bc102f57..10e8c702826 100644 --- a/lib/gitlab/quick_actions/issue_actions.rb +++ b/lib/gitlab/quick_actions/issue_actions.rb @@ -259,7 +259,8 @@ module Gitlab current_user.can?(:"set_#{quick_action_target.issue_type}_metadata", quick_action_target) end command :promote_to_incident do - @updates[:issue_type] = "incident" + @updates[:issue_type] = :incident + @updates[:work_item_type] = ::WorkItems::Type.default_by_type(:incident) end desc { _('Add customer relation contacts') } diff --git a/lib/gitlab/usage_data_counters/known_events/quickactions.yml b/lib/gitlab/usage_data_counters/known_events/quickactions.yml index 7006173cc59..ee5fa29c0c3 100644 --- a/lib/gitlab/usage_data_counters/known_events/quickactions.yml +++ b/lib/gitlab/usage_data_counters/known_events/quickactions.yml @@ -87,6 +87,8 @@ aggregation: weekly - name: i_quickactions_subscribe aggregation: weekly +- name: i_quickactions_summarize_diff + aggregation: weekly - name: i_quickactions_tableflip aggregation: weekly - name: i_quickactions_tag diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b5802faba75..1e10165d02e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12185,6 +12185,9 @@ msgstr "" msgid "Create %{workspace} label" msgstr "" +msgid "Create LLM-generated summary from diff(s)" +msgstr "" + msgid "Create New Directory" msgstr "" @@ -12629,6 +12632,9 @@ msgstr "" msgid "Created on:" msgstr "" +msgid "Creates a LLM-generated summary from diff(s)." +msgstr "" + msgid "Creates a branch and a merge request to resolve this issue." msgstr "" @@ -16684,9 +16690,24 @@ msgstr "" msgid "Environment|Deployment tier" msgstr "" +msgid "Environment|Failed" +msgstr "" + msgid "Environment|Kubernetes overview" msgstr "" +msgid "Environment|Pending" +msgstr "" + +msgid "Environment|Pods" +msgstr "" + +msgid "Environment|Running" +msgstr "" + +msgid "Environment|Succeeded" +msgstr "" + msgid "Epic" msgstr "" @@ -26433,6 +26454,9 @@ msgstr "" msgid "MD5" msgstr "" +msgid "MLExperimentTracking|Delete candidate?" +msgstr "" + msgid "MLExperimentTracking|Delete experiment?" msgstr "" @@ -28307,9 +28331,15 @@ msgstr "" msgid "MlExperimentTracking|Created at" msgstr "" +msgid "MlExperimentTracking|Delete candidate" +msgstr "" + msgid "MlExperimentTracking|Delete experiment" msgstr "" +msgid "MlExperimentTracking|Deleting this candidate will delete the associated parameters, metrics, and metadata." +msgstr "" + msgid "MlExperimentTracking|Deleting this experiment will also delete its candidates and their associated metadata." msgstr "" @@ -37441,6 +37471,9 @@ msgstr "" msgid "Request details" msgstr "" +msgid "Request for summary queued." +msgstr "" + msgid "Request parameter %{param} is missing." msgstr "" @@ -45059,6 +45092,9 @@ msgstr "" msgid "This comment changed after you started editing it. Review the %{startTag}updated comment%{endTag} to ensure information is not lost." msgstr "" +msgid "This comment was generated using OpenAI" +msgstr "" + msgid "This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request." msgstr "" diff --git a/package.json b/package.json index 5fd478da2c9..6418a12910e 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@cubejs-client/core": "^0.32.17", "@cubejs-client/vue": "^0.32.17", "@gitlab/at.js": "1.5.7", + "@gitlab/cluster-client": "^1.2.0", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.2.0", "@gitlab/svgs": "3.38.0", @@ -64,41 +65,41 @@ "@rails/actioncable": "6.1.4-7", "@rails/ujs": "6.1.4-7", "@sourcegraph/code-host-integration": "0.0.84", - "@tiptap/core": "^2.0.0-beta.220", - "@tiptap/extension-blockquote": "^2.0.0-beta.220", - "@tiptap/extension-bold": "^2.0.0-beta.220", - "@tiptap/extension-bubble-menu": "2.0.0-beta.220", - "@tiptap/extension-bullet-list": "^2.0.0-beta.220", - "@tiptap/extension-code": "^2.0.0-beta.220", - "@tiptap/extension-code-block": "^2.0.0-beta.220", - "@tiptap/extension-code-block-lowlight": "2.0.0-beta.220", - "@tiptap/extension-document": "^2.0.0-beta.220", - "@tiptap/extension-dropcursor": "^2.0.0-beta.220", - "@tiptap/extension-gapcursor": "^2.0.0-beta.220", - "@tiptap/extension-hard-break": "^2.0.0-beta.220", - "@tiptap/extension-heading": "^2.0.0-beta.220", - "@tiptap/extension-highlight": "^2.0.0-beta.220", - "@tiptap/extension-history": "^2.0.0-beta.220", - "@tiptap/extension-horizontal-rule": "^2.0.0-beta.220", - "@tiptap/extension-image": "^2.0.0-beta.220", - "@tiptap/extension-italic": "^2.0.0-beta.220", - "@tiptap/extension-link": "^2.0.0-beta.220", - "@tiptap/extension-list-item": "^2.0.0-beta.220", - "@tiptap/extension-ordered-list": "^2.0.0-beta.220", - "@tiptap/extension-paragraph": "^2.0.0-beta.220", - "@tiptap/extension-strike": "^2.0.0-beta.220", - "@tiptap/extension-subscript": "^2.0.0-beta.220", - "@tiptap/extension-superscript": "^2.0.0-beta.220", - "@tiptap/extension-table": "^2.0.0-beta.220", - "@tiptap/extension-table-cell": "^2.0.0-beta.220", - "@tiptap/extension-table-header": "^2.0.0-beta.220", - "@tiptap/extension-table-row": "^2.0.0-beta.220", - "@tiptap/extension-task-item": "^2.0.0-beta.220", - "@tiptap/extension-task-list": "^2.0.0-beta.220", - "@tiptap/extension-text": "^2.0.0-beta.220", - "@tiptap/pm": "^2.0.0-beta.220", - "@tiptap/suggestion": "^2.0.0-beta.220", - "@tiptap/vue-2": "2.0.0-beta.220", + "@tiptap/core": "^2.0.3", + "@tiptap/extension-blockquote": "^2.0.3", + "@tiptap/extension-bold": "^2.0.3", + "@tiptap/extension-bubble-menu": "2.0.3", + "@tiptap/extension-bullet-list": "^2.0.3", + "@tiptap/extension-code": "^2.0.3", + "@tiptap/extension-code-block": "^2.0.3", + "@tiptap/extension-code-block-lowlight": "2.0.3", + "@tiptap/extension-document": "^2.0.3", + "@tiptap/extension-dropcursor": "^2.0.3", + "@tiptap/extension-gapcursor": "^2.0.3", + "@tiptap/extension-hard-break": "^2.0.3", + "@tiptap/extension-heading": "^2.0.3", + "@tiptap/extension-highlight": "^2.0.3", + "@tiptap/extension-history": "^2.0.3", + "@tiptap/extension-horizontal-rule": "^2.0.3", + "@tiptap/extension-image": "^2.0.3", + "@tiptap/extension-italic": "^2.0.3", + "@tiptap/extension-link": "^2.0.3", + "@tiptap/extension-list-item": "^2.0.3", + "@tiptap/extension-ordered-list": "^2.0.3", + "@tiptap/extension-paragraph": "^2.0.3", + "@tiptap/extension-strike": "^2.0.3", + "@tiptap/extension-subscript": "^2.0.3", + "@tiptap/extension-superscript": "^2.0.3", + "@tiptap/extension-table": "^2.0.3", + "@tiptap/extension-table-cell": "^2.0.3", + "@tiptap/extension-table-header": "^2.0.3", + "@tiptap/extension-table-row": "^2.0.3", + "@tiptap/extension-task-item": "^2.0.3", + "@tiptap/extension-task-list": "^2.0.3", + "@tiptap/extension-text": "^2.0.3", + "@tiptap/pm": "^2.0.3", + "@tiptap/suggestion": "^2.0.3", + "@tiptap/vue-2": "2.0.3", "@vue/apollo-components": "^4.0.0-beta.4", "@vue/apollo-option": "^4.0.0-beta.4", "apollo-upload-client": "15.0.0", diff --git a/qa/Gemfile b/qa/Gemfile index ef66b1327eb..16d9cf9e74a 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' -gem 'gitlab-qa', '~> 10', '>= 10.2.0', require: 'gitlab/qa' +gem 'gitlab-qa', '~> 10', '>= 10.2.1', require: 'gitlab/qa' gem 'activesupport', '~> 6.1.7.2' # This should stay in sync with the root's Gemfile gem 'allure-rspec', '~> 2.20.0' gem 'capybara', '~> 3.39.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 1cd898903b8..10e3de2b999 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -102,7 +102,7 @@ GEM gitlab (4.18.0) httparty (~> 0.18) terminal-table (>= 1.5.1) - gitlab-qa (10.2.0) + gitlab-qa (10.2.1) activesupport (~> 6.1) gitlab (~> 4.18.0) http (~> 5.0) @@ -318,7 +318,7 @@ DEPENDENCIES faraday-retry (~> 2.1) fog-core (= 2.1.0) fog-google (~> 1.19) - gitlab-qa (~> 10, >= 10.2.0) + gitlab-qa (~> 10, >= 10.2.1) influxdb-client (~> 2.9) knapsack (~> 4.0) nokogiri (~> 1.14, >= 1.14.3) @@ -343,4 +343,4 @@ DEPENDENCIES zeitwerk (~> 2.6, >= 2.6.7) BUNDLED WITH - 2.4.11 + 2.4.12 diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb index 54a3fc57a5d..67824a10288 100644 --- a/spec/factories/issues.rb +++ b/spec/factories/issues.rb @@ -86,6 +86,16 @@ FactoryBot.define do association :work_item_type, :default, :key_result end + trait :incident do + issue_type { :incident } + association :work_item_type, :default, :incident + end + + trait :test_case do + issue_type { :test_case } + association :work_item_type, :default, :test_case + end + factory :incident do issue_type { :incident } association :work_item_type, :default, :incident diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb index 8ff0c294b24..e3989a8a192 100644 --- a/spec/features/merge_request/user_accepts_merge_request_spec.rb +++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb @@ -16,7 +16,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli it 'when merge method is set to merge commit' do visit(merge_request_path(merge_request)) - click_button('Merge') + click_merge_button puts merge_request.short_merged_commit_sha @@ -31,7 +31,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli visit(merge_request_path(merge_request)) - click_button('Merge') + click_merge_button expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}") end @@ -41,7 +41,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli visit(merge_request_path(merge_request)) - click_button('Merge') + click_merge_button expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}") end @@ -55,7 +55,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli it 'accepts a merge request' do check('Delete source branch') - click_button('Merge') + click_merge_button expect(page).to have_content('Changes merged into') expect(page).not_to have_selector('.js-remove-branch-button') @@ -72,7 +72,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli end it 'accepts a merge request' do - click_button('Merge') + click_merge_button expect(page).to have_content('Changes merged into') expect(page).to have_selector('.js-remove-branch-button') @@ -90,7 +90,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli it 'accepts a merge request' do check('Delete source branch') - click_button('Merge') + click_merge_button expect(page).to have_content('Changes merged into') expect(page).not_to have_selector('.js-remove-branch-button') @@ -112,9 +112,15 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli find('[data-testid="widget_edit_commit_message"]').click fill_in('merge-message-edit', with: 'wow such merge') - click_button('Merge') + click_merge_button expect(page).to have_selector('.gl-badge', text: 'Merged') end end + + def click_merge_button + page.within('.mr-state-widget') do + click_button 'Merge' + end + end end diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb index cdc00017ab3..2a71dd5eac7 100644 --- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb @@ -17,7 +17,9 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea wait_for_requests - expect(page).to have_button 'Merge' + page.within('.mr-state-widget') do + expect(page).to have_button 'Merge' + end end end @@ -56,7 +58,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea wait_for_requests - expect(page).not_to have_button('Merge') + expect(page).not_to have_button('Merge', exact: true) expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.') end end @@ -69,7 +71,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea wait_for_requests - expect(page).not_to have_button 'Merge' + expect(page).not_to have_button('Merge', exact: true) expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.') end end @@ -82,7 +84,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea wait_for_requests - expect(page).to have_button 'Merge' + expect(page).to have_button('Merge', exact: true) end end @@ -94,7 +96,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea wait_for_requests - expect(page).not_to have_button 'Merge' + expect(page).not_to have_button('Merge', exact: true) end end end @@ -126,8 +128,9 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea visit project_merge_request_path(project, merge_request) wait_for_requests - - expect(page).to have_button 'Merge' + page.within('.mr-state-widget') do + expect(page).to have_button 'Merge' + end end end @@ -139,7 +142,9 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea wait_for_requests - expect(page).to have_button 'Merge' + page.within('.mr-state-widget') do + expect(page).to have_button 'Merge' + end end end end diff --git a/spec/features/merge_request/user_reverts_merge_request_spec.rb b/spec/features/merge_request/user_reverts_merge_request_spec.rb index e09a4569caf..da48a31abbd 100644 --- a/spec/features/merge_request/user_reverts_merge_request_spec.rb +++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb @@ -13,7 +13,9 @@ RSpec.describe 'User reverts a merge request', :js, feature_category: :code_revi visit(merge_request_path(merge_request)) - click_button('Merge') + page.within('.mr-state-widget') do + click_button 'Merge' + end wait_for_requests diff --git a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb index b83580565e4..476be5ab599 100644 --- a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb +++ b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb @@ -21,7 +21,7 @@ feature_category: :code_review_workflow do context 'with unresolved threads' do it 'does not allow to merge' do - expect(page).not_to have_button 'Merge' + expect(page).not_to have_button('Merge', exact: true) expect(page).to have_content('all threads must be resolved') end end @@ -33,7 +33,7 @@ feature_category: :code_review_workflow do end it 'allows MR to be merged' do - expect(page).to have_button 'Merge' + expect(page).to have_button('Merge', exact: true) end end end @@ -46,7 +46,7 @@ feature_category: :code_review_workflow do context 'with unresolved threads' do it 'does not allow to merge' do - expect(page).to have_button 'Merge' + expect(page).to have_button('Merge', exact: true) end end @@ -57,7 +57,7 @@ feature_category: :code_review_workflow do end it 'allows MR to be merged' do - expect(page).to have_button 'Merge' + expect(page).to have_button('Merge', exact: true) end end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index acf2893b513..eb293fbbd20 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -396,7 +396,9 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category: end it 'updates the MR widget', :sidekiq_might_not_need_inline do - click_button 'Merge' + page.within('.mr-state-widget') do + click_button 'Merge' + end expect(page).to have_content('An error occurred while merging') end @@ -452,7 +454,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category: wait_for_requests - expect(page).not_to have_button('Merge') + expect(page).not_to have_button('Merge', exact: true) expect(page).to have_content('Merging!') end end diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb index ae3158e4270..1c754943acb 100644 --- a/spec/features/user_sees_revert_modal_spec.rb +++ b/spec/features/user_sees_revert_modal_spec.rb @@ -21,7 +21,9 @@ feature_category: :code_review_workflow do before do sign_in(user) visit(project_merge_request_path(project, merge_request)) - click_button('Merge') + page.within('.mr-state-widget') do + click_button 'Merge' + end wait_for_requests end diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index b5435990042..8d91ffe5ffc 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -801,6 +801,14 @@ export const resolvedDeploymentDetails = { export const agent = { project: 'agent-project', - id: '1', + id: 'gid://gitlab/ClusterAgent/1', name: 'agent-name', + kubernetesNamespace: 'agent-namespace', }; + +const runningPod = { status: { phase: 'Running' } }; +const pendingPod = { status: { phase: 'Pending' } }; +const succeededPod = { status: { phase: 'Succeeded' } }; +const failedPod = { status: { phase: 'Failed' } }; + +export const k8sPodsMock = [runningPod, runningPod, pendingPod, succeededPod, failedPod, failedPod]; diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js index 2c223d3a1a7..c66844f5f24 100644 --- a/spec/frontend/environments/graphql/resolvers_spec.js +++ b/spec/frontend/environments/graphql/resolvers_spec.js @@ -1,4 +1,5 @@ import MockAdapter from 'axios-mock-adapter'; +import { CoreV1Api } from '@gitlab/cluster-client'; import { s__ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; @@ -17,6 +18,7 @@ import { resolvedEnvironment, folder, resolvedFolder, + k8sPodsMock, } from './mock_data'; const ENDPOINT = `${TEST_HOST}/environments`; @@ -143,6 +145,61 @@ describe('~/frontend/environments/graphql/resolvers', () => { expect(environmentFolder).toEqual(resolvedFolder); }); }); + describe('k8sPods', () => { + const namespace = 'default'; + const configuration = { + basePath: 'kas-proxy/', + baseOptions: { + headers: { 'GitLab-Agent-Id': '1' }, + }, + }; + + const mockPodsListFn = jest.fn().mockImplementation(() => { + return Promise.resolve({ + data: { + items: k8sPodsMock, + }, + }); + }); + + const mockNamespacedPodsListFn = jest.fn().mockImplementation(mockPodsListFn); + const mockAllPodsListFn = jest.fn().mockImplementation(mockPodsListFn); + + beforeEach(() => { + jest + .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod') + .mockImplementation(mockNamespacedPodsListFn); + jest + .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces') + .mockImplementation(mockAllPodsListFn); + }); + + it('should request namespaced pods from the cluster_client library if namespace is specified', async () => { + const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace }); + + expect(mockNamespacedPodsListFn).toHaveBeenCalledWith(namespace); + expect(mockAllPodsListFn).not.toHaveBeenCalled(); + + expect(pods).toEqual(k8sPodsMock); + }); + it('should request all pods from the cluster_client library if namespace is not specified', async () => { + const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' }); + + expect(mockAllPodsListFn).toHaveBeenCalled(); + expect(mockNamespacedPodsListFn).not.toHaveBeenCalled(); + + expect(pods).toEqual(k8sPodsMock); + }); + it('should throw an error if the API call fails', async () => { + jest + .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces') + .mockRejectedValue(new Error('API error')); + + await expect(mockResolvers.Query.k8sPods(null, { configuration })).rejects.toThrow( + 'API error', + ); + }); + }); describe('stopEnvironment', () => { it('should post to the stop environment path', async () => { mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK); diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js index 8673c657760..1912fd4a82b 100644 --- a/spec/frontend/environments/kubernetes_overview_spec.js +++ b/spec/frontend/environments/kubernetes_overview_spec.js @@ -1,19 +1,28 @@ import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; -import { GlCollapse, GlButton } from '@gitlab/ui'; +import { GlCollapse, GlButton, GlAlert } from '@gitlab/ui'; import KubernetesOverview from '~/environments/components/kubernetes_overview.vue'; import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue'; - -const agent = { - project: 'agent-project', - id: '1', - name: 'agent-name', -}; +import KubernetesPods from '~/environments/components/kubernetes_pods.vue'; +import { agent } from './graphql/mock_data'; +import { mockKasTunnelUrl } from './mock_data'; const propsData = { agentId: agent.id, agentName: agent.name, agentProjectPath: agent.project, + namespace: agent.kubernetesNamespace, +}; + +const provide = { + kasTunnelUrl: mockKasTunnelUrl, +}; + +const configuration = { + basePath: provide.kasTunnelUrl.replace(/\/$/, ''), + baseOptions: { + headers: { 'GitLab-Agent-Id': '1' }, + }, }; describe('~/environments/components/kubernetes_overview.vue', () => { @@ -22,10 +31,13 @@ describe('~/environments/components/kubernetes_overview.vue', () => { const findCollapse = () => wrapper.findComponent(GlCollapse); const findCollapseButton = () => wrapper.findComponent(GlButton); const findAgentInfo = () => wrapper.findComponent(KubernetesAgentInfo); + const findKubernetesPods = () => wrapper.findComponent(KubernetesPods); + const findAlert = () => wrapper.findComponent(GlAlert); const createWrapper = () => { wrapper = shallowMount(KubernetesOverview, { propsData, + provide, }); }; @@ -57,6 +69,7 @@ describe('~/environments/components/kubernetes_overview.vue', () => { it("doesn't render components when the collapse is not visible", () => { expect(findAgentInfo().exists()).toBe(false); + expect(findKubernetesPods().exists()).toBe(false); }); it('opens on click', async () => { @@ -70,15 +83,40 @@ describe('~/environments/components/kubernetes_overview.vue', () => { }); describe('when section is expanded', () => { - it('renders kubernetes agent info', async () => { + beforeEach(() => { createWrapper(); - await toggleCollapse(); + toggleCollapse(); + }); + it('renders kubernetes agent info', () => { expect(findAgentInfo().props()).toEqual({ agentName: agent.name, agentId: agent.id, agentProjectPath: agent.project, }); }); + + it('renders kubernetes pods', () => { + expect(findKubernetesPods().props()).toEqual({ + namespace: agent.kubernetesNamespace, + configuration, + }); + }); + }); + + describe('on cluster error', () => { + beforeEach(() => { + createWrapper(); + toggleCollapse(); + }); + + it('shows alert with the error message', async () => { + const error = 'Error message from pods'; + + findKubernetesPods().vm.$emit('cluster-error', error); + await nextTick(); + + expect(findAlert().text()).toBe(error); + }); }); }); diff --git a/spec/frontend/environments/kubernetes_pods_spec.js b/spec/frontend/environments/kubernetes_pods_spec.js new file mode 100644 index 00000000000..137309d7853 --- /dev/null +++ b/spec/frontend/environments/kubernetes_pods_spec.js @@ -0,0 +1,114 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import KubernetesPods from '~/environments/components/kubernetes_pods.vue'; +import { mockKasTunnelUrl } from './mock_data'; +import { k8sPodsMock } from './graphql/mock_data'; + +Vue.use(VueApollo); + +describe('~/environments/components/kubernetes_pods.vue', () => { + let wrapper; + + const namespace = 'my-kubernetes-namespace'; + const configuration = { + basePath: mockKasTunnelUrl, + baseOptions: { + headers: { 'GitLab-Agent-Id': '1' }, + }, + }; + + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findAllStats = () => wrapper.findAllComponents(GlSingleStat); + const findSingleStat = (at) => findAllStats().at(at); + + const createApolloProvider = () => { + const mockResolvers = { + Query: { + k8sPods: jest.fn().mockReturnValue(k8sPodsMock), + }, + }; + + return createMockApollo([], mockResolvers); + }; + + const createWrapper = (apolloProvider = createApolloProvider()) => { + wrapper = shallowMount(KubernetesPods, { + propsData: { namespace, configuration }, + apolloProvider, + }); + }; + + describe('mounted', () => { + it('shows the loading icon', () => { + createWrapper(); + + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('hides the loading icon when the list of pods loaded', async () => { + createWrapper(); + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + }); + + describe('when gets pods data', () => { + it('renders stats', async () => { + createWrapper(); + await waitForPromises(); + + expect(findAllStats()).toHaveLength(4); + }); + + it.each` + count | title | index + ${2} | ${KubernetesPods.i18n.runningPods} | ${0} + ${1} | ${KubernetesPods.i18n.pendingPods} | ${1} + ${1} | ${KubernetesPods.i18n.succeededPods} | ${2} + ${2} | ${KubernetesPods.i18n.failedPods} | ${3} + `( + 'renders stat with title "$title" and count "$count" at index $index', + async ({ count, title, index }) => { + createWrapper(); + await waitForPromises(); + + expect(findSingleStat(index).props()).toMatchObject({ + value: count, + title, + }); + }, + ); + }); + + describe('when gets an error from the cluster_client API', () => { + const error = new Error('Error from the cluster_client API'); + const createErroredApolloProvider = () => { + const mockResolvers = { + Query: { + k8sPods: jest.fn().mockRejectedValueOnce(error), + }, + }; + + return createMockApollo([], mockResolvers); + }; + + beforeEach(async () => { + createWrapper(createErroredApolloProvider()); + await waitForPromises(); + }); + + it("doesn't show pods stats", () => { + expect(findAllStats()).toHaveLength(0); + }); + + it('emits an error message', () => { + expect(wrapper.emitted('cluster-error')).toMatchObject([[error]]); + }); + }); +}); diff --git a/spec/frontend/environments/mock_data.js b/spec/frontend/environments/mock_data.js index a6d67c26304..bd2c6b7c892 100644 --- a/spec/frontend/environments/mock_data.js +++ b/spec/frontend/environments/mock_data.js @@ -313,6 +313,8 @@ const createEnvironment = (data = {}) => ({ ...data, }); +const mockKasTunnelUrl = 'https://kas.gitlab.com/k8s-proxy'; + export { environment, environmentsList, @@ -321,4 +323,5 @@ export { tableData, deployBoardMockData, createEnvironment, + mockKasTunnelUrl, }; diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js index 89a9ca725ba..b4f5263a151 100644 --- a/spec/frontend/environments/new_environment_item_spec.js +++ b/spec/frontend/environments/new_environment_item_spec.js @@ -12,6 +12,7 @@ import Deployment from '~/environments/components/deployment.vue'; import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue'; import KubernetesOverview from '~/environments/components/kubernetes_overview.vue'; import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data'; +import { mockKasTunnelUrl } from './mock_data'; Vue.use(VueApollo); @@ -26,7 +27,13 @@ describe('~/environments/components/new_environment_item.vue', () => { mountExtended(EnvironmentItem, { apolloProvider, propsData: { environment: resolvedEnvironment, ...propsData }, - provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1', ...provideData }, + provide: { + helpPagePath: '/help', + projectId: '1', + projectPath: '/1', + kasTunnelUrl: mockKasTunnelUrl, + ...provideData, + }, stubs: { transition: stubTransition() }, }); @@ -536,6 +543,7 @@ describe('~/environments/components/new_environment_item.vue', () => { agentProjectPath: agent.project, agentName: agent.name, agentId: agent.id, + namespace: agent.kubernetesNamespace, }); }); diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb index 3583beb83c2..a825e1af268 100644 --- a/spec/frontend/fixtures/jobs.rb +++ b/spec/frontend/fixtures/jobs.rb @@ -48,50 +48,48 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do let!(:with_artifact) { create(:ci_build, :success, name: 'with_artifact', job_artifacts: [artifact], pipeline: pipeline) } let!(:with_coverage) { create(:ci_build, :success, name: 'with_coverage', coverage: 40.0, pipeline: pipeline) } - fixtures_path = 'graphql/jobs/' - get_jobs_query = 'get_jobs.query.graphql' - full_path = 'frontend-fixtures/builds-project' + shared_examples 'graphql queries' do |path, jobs_query| + let_it_be(:variables) { {} } - let_it_be(:query) do - get_graphql_query_as_string("jobs/components/table/graphql/queries/#{get_jobs_query}") - end + let_it_be(:query) do + get_graphql_query_as_string("#{path}/#{jobs_query}") + end - it "#{fixtures_path}#{get_jobs_query}.json" do - post_graphql(query, current_user: user, variables: { - fullPath: full_path - }) + fixtures_path = 'graphql/jobs/' - expect_graphql_errors_to_be_empty - end + it "#{fixtures_path}#{jobs_query}.json" do + post_graphql(query, current_user: user, variables: variables) - it "#{fixtures_path}#{get_jobs_query}.as_guest.json" do - guest = create(:user) - project.add_guest(guest) + expect_graphql_errors_to_be_empty + end - post_graphql(query, current_user: guest, variables: { - fullPath: full_path - }) + it "#{fixtures_path}#{jobs_query}.as_guest.json" do + guest = create(:user) + project.add_guest(guest) - expect_graphql_errors_to_be_empty - end + post_graphql(query, current_user: guest, variables: variables) - it "#{fixtures_path}#{get_jobs_query}.paginated.json" do - post_graphql(query, current_user: user, variables: { - fullPath: full_path, - first: 2 - }) + expect_graphql_errors_to_be_empty + end - expect_graphql_errors_to_be_empty - end + it "#{fixtures_path}#{jobs_query}.paginated.json" do + post_graphql(query, current_user: user, variables: variables.merge({ first: 2 })) - it "#{fixtures_path}#{get_jobs_query}.empty.json" do - post_graphql(query, current_user: user, variables: { - fullPath: full_path, - first: 0 - }) + expect_graphql_errors_to_be_empty + end - expect_graphql_errors_to_be_empty + it "#{fixtures_path}#{jobs_query}.empty.json" do + post_graphql(query, current_user: user, variables: variables.merge({ first: 0 })) + + expect_graphql_errors_to_be_empty + end end + + it_behaves_like 'graphql queries', 'jobs/components/table/graphql/queries', 'get_jobs.query.graphql' do + let(:variables) { { fullPath: 'frontend-fixtures/builds-project' } } + end + + it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_all_jobs.query.graphql' end describe 'get_jobs_count.query.graphql', type: :request do diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js index 483b4ca711f..fb1ded7b4ef 100644 --- a/spec/frontend/jobs/mock_data.js +++ b/spec/frontend/jobs/mock_data.js @@ -1,7 +1,9 @@ import mockJobsCount from 'test_fixtures/graphql/jobs/get_jobs_count.query.graphql.json'; import mockJobsEmpty from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.empty.json'; import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.paginated.json'; +import mockAllJobsPaginated from 'test_fixtures/graphql/jobs/get_all_jobs.query.graphql.paginated.json'; import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json'; +import mockAllJobs from 'test_fixtures/graphql/jobs/get_all_jobs.query.graphql.json'; import mockJobsAsGuest from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.as_guest.json'; import { TEST_HOST } from 'spec/test_constants'; import { TOKEN_TYPE_STATUS } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -11,8 +13,10 @@ threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); // Fixtures generated at spec/frontend/fixtures/jobs.rb export const mockJobsResponsePaginated = mockJobsPaginated; +export const mockAllJobsResponsePaginated = mockAllJobsPaginated; export const mockJobsResponseEmpty = mockJobsEmpty; export const mockJobsNodes = mockJobs.data.project.jobs.nodes; +export const mockAllJobsNodes = mockAllJobs.data.jobs.nodes; export const mockJobsNodesAsGuest = mockJobsAsGuest.data.project.jobs.nodes; export const mockJobsCountResponse = mockJobsCount; @@ -922,6 +926,14 @@ export const stages = [ }, ]; +export const statuses = { + success: 'SUCCESS', + failed: 'FAILED', + canceled: 'CANCELED', + pending: 'PENDING', + running: 'RUNNING', +}; + export default { id: 4757, artifact: { diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap b/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap index dc21db39259..6fd3dce1941 100644 --- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap +++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/__snapshots__/ml_candidates_show_spec.js.snap @@ -2,99 +2,36 @@ exports[`MlCandidatesShow renders correctly 1`] = ` <div> + <incubation-alert-stub + featurename="Machine learning experiment tracking" + linktofeedbackissue="https://gitlab.com/gitlab-org/gitlab/-/issues/381660" + /> + <div - class="gl-alert gl-alert-warning" + class="detail-page-header gl-flex-wrap" > - <svg - aria-hidden="true" - class="gl-icon s16 gl-alert-icon" - data-testid="warning-icon" - role="img" - > - <use - href="#warning" - /> - </svg> - <div - aria-live="assertive" - class="gl-alert-content" - role="alert" + class="detail-page-header-body" > - <h2 - class="gl-alert-title" - > - Machine learning experiment tracking is in incubating phase - </h2> - - <div - class="gl-alert-body" + <h1 + class="page-title gl-font-size-h-display flex-fill" > - GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited. - - <a - class="gl-link" - href="https://about.gitlab.com/handbook/engineering/incubation/" - rel="noopener noreferrer" - target="_blank" - > - Learn more about incubating features - </a> - </div> + Model candidate details + + </h1> - <div - class="gl-alert-actions" - > - <a - class="btn gl-alert-action btn-confirm btn-md gl-button" - href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660" - > - <!----> - - <!----> - - <span - class="gl-button-text" - > - - Give feedback on this feature - - </span> - </a> - </div> + <delete-button-stub + actionprimarytext="Delete candidate" + deleteconfirmationtext="Deleting this candidate will delete the associated parameters, metrics, and metadata." + deletepath="path_to_candidate" + modaltitle="Delete candidate?" + /> </div> - - <button - aria-label="Dismiss" - class="btn gl-dismiss-btn btn-default btn-sm gl-button btn-default-tertiary btn-icon" - type="button" - > - <!----> - - <svg - aria-hidden="true" - class="gl-button-icon gl-icon s16" - data-testid="close-icon" - role="img" - > - <use - href="#close" - /> - </svg> - - <!----> - </button> </div> - <h3> - - Model candidate details - - </h3> - <table - class="candidate-details" + class="candidate-details gl-w-full" > <tbody> <tr @@ -143,12 +80,9 @@ exports[`MlCandidatesShow renders correctly 1`] = ` </td> <td> - <a - class="gl-link" - href="#" - > + <gl-link-stub> The Experiment - </a> + </gl-link-stub> </td> </tr> @@ -162,12 +96,11 @@ exports[`MlCandidatesShow renders correctly 1`] = ` </td> <td> - <a - class="gl-link" + <gl-link-stub href="path_to_artifact" > Artifacts - </a> + </gl-link-stub> </td> </tr> diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js index 36455339041..7d03ab3b509 100644 --- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js +++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js @@ -1,6 +1,7 @@ -import { GlAlert } from '@gitlab/ui'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import MlCandidatesShow from '~/ml/experiment_tracking/routes/candidates/show'; +import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue'; +import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue'; describe('MlCandidatesShow', () => { let wrapper; @@ -25,23 +26,31 @@ describe('MlCandidatesShow', () => { experiment_name: 'The Experiment', experiment_path: 'path/to/experiment', status: 'SUCCESS', + path: 'path_to_candidate', }, }; - return mountExtended(MlCandidatesShow, { propsData: { candidate } }); + wrapper = shallowMountExtended(MlCandidatesShow, { propsData: { candidate } }); }; - const findAlert = () => wrapper.findComponent(GlAlert); + beforeEach(createWrapper); - it('shows incubation warning', () => { - wrapper = createWrapper(); + const findAlert = () => wrapper.findComponent(IncubationAlert); + const findDeleteButton = () => wrapper.findComponent(DeleteButton); + it('shows incubation warning', () => { expect(findAlert().exists()).toBe(true); }); - it('renders correctly', () => { - wrapper = createWrapper(); + it('shows delete button', () => { + expect(findDeleteButton().exists()).toBe(true); + }); + it('passes the delete path to delete button', () => { + expect(findDeleteButton().props('deletePath')).toBe('path_to_candidate'); + }); + + it('renders correctly', () => { expect(wrapper.element).toMatchSnapshot(); }); }); diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js new file mode 100644 index 00000000000..e7ac2576b6f --- /dev/null +++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js @@ -0,0 +1,61 @@ +import { GlSkeletonLoader, GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import JobsTable from '~/jobs/components/table/jobs_table.vue'; +import getJobsQuery from '~/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql'; +import AdminJobsTableApp from '~/pages/admin/jobs/components/table/admin_jobs_table_app.vue'; + +import { mockAllJobsResponsePaginated, statuses } from '../../../../../jobs/mock_data'; + +Vue.use(VueApollo); + +describe('Job table app', () => { + let wrapper; + + const successHandler = jest.fn().mockResolvedValue(mockAllJobsResponsePaginated); + + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon); + const findTable = () => wrapper.findComponent(JobsTable); + + const createMockApolloProvider = (handler) => { + const requestHandlers = [[getJobsQuery, handler]]; + + return createMockApollo(requestHandlers); + }; + + const createComponent = ({ + handler = successHandler, + mountFn = shallowMount, + data = {}, + } = {}) => { + wrapper = mountFn(AdminJobsTableApp, { + data() { + return { + ...data, + }; + }, + provide: { + jobStatuses: statuses, + }, + apolloProvider: createMockApolloProvider(handler), + }); + }; + + describe('loaded state', () => { + beforeEach(async () => { + createComponent(); + + await waitForPromises(); + }); + + it('should display the jobs table with data', () => { + expect(findTable().exists()).toBe(true); + expect(findSkeletonLoader().exists()).toBe(false); + expect(findLoadingSpinner().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js new file mode 100644 index 00000000000..59e9eda6343 --- /dev/null +++ b/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js @@ -0,0 +1,106 @@ +import cacheConfig from '~/pages/admin/jobs/components/table/graphql/cache_config'; +import { + CIJobConnectionExistingCache, + CIJobConnectionIncomingCache, + CIJobConnectionIncomingCacheRunningStatus, +} from '../../../../../../jobs/mock_data'; + +const firstLoadArgs = { first: 3, statuses: 'PENDING' }; +const runningArgs = { first: 3, statuses: 'RUNNING' }; + +describe('jobs/components/table/graphql/cache_config', () => { + describe('when fetching data with the same statuses', () => { + it('should contain cache nodes and a status when merging caches on first load', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, { + args: firstLoadArgs, + }); + + expect(res.nodes).toHaveLength(CIJobConnectionIncomingCache.nodes.length); + expect(res.statuses).toBe('PENDING'); + }); + + it('should add to existing caches when merging caches after first load', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge( + CIJobConnectionExistingCache, + CIJobConnectionIncomingCache, + { + args: firstLoadArgs, + }, + ); + + expect(res.nodes).toHaveLength( + CIJobConnectionIncomingCache.nodes.length + CIJobConnectionExistingCache.nodes.length, + ); + }); + + it('should not add to existing cache if the incoming elements are the same', () => { + // simulate that this is the last page + const finalExistingCache = { + ...CIJobConnectionExistingCache, + pageInfo: { + hasNextPage: false, + }, + }; + + const res = cacheConfig.typePolicies.CiJobConnection.merge( + CIJobConnectionExistingCache, + finalExistingCache, + { + args: firstLoadArgs, + }, + ); + + expect(res.nodes).toHaveLength(CIJobConnectionExistingCache.nodes.length); + }); + + it('should contain the pageInfo key as part of the result', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, { + args: firstLoadArgs, + }); + + expect(res.pageInfo).toEqual( + expect.objectContaining({ + __typename: 'PageInfo', + endCursor: 'eyJpZCI6IjIwNTEifQ', + hasNextPage: true, + hasPreviousPage: false, + startCursor: 'eyJpZCI6IjIxNzMifQ', + }), + ); + }); + }); + + describe('when fetching data with different statuses', () => { + it('should reset cache when a cache already exists', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge( + CIJobConnectionExistingCache, + CIJobConnectionIncomingCacheRunningStatus, + { + args: runningArgs, + }, + ); + + expect(res.nodes).not.toEqual(CIJobConnectionExistingCache.nodes); + expect(res.nodes).toHaveLength(CIJobConnectionIncomingCacheRunningStatus.nodes.length); + }); + }); + + describe('when incoming data has no nodes', () => { + it('should return existing cache', () => { + const res = cacheConfig.typePolicies.CiJobConnection.merge( + CIJobConnectionExistingCache, + { __typename: 'CiJobConnection', count: 500 }, + { + args: { statuses: 'SUCCESS' }, + }, + ); + + const expectedResponse = { + ...CIJobConnectionExistingCache, + statuses: 'SUCCESS', + }; + + expect(res).toEqual(expectedResponse); + }); + }); +}); diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb index 7c6cf137a1e..87f99878a4d 100644 --- a/spec/graphql/types/issue_type_spec.rb +++ b/spec/graphql/types/issue_type_spec.rb @@ -265,7 +265,10 @@ RSpec.describe GitlabSchema.types['Issue'] do context 'for an incident' do before do - issue.update!(issue_type: Issue.issue_types[:incident]) + issue.update!( + issue_type: Issue.issue_types[:incident], + work_item_type: WorkItems::Type.default_by_type(:incident) + ) end it { is_expected.to be_nil } diff --git a/spec/helpers/integrations_helper_spec.rb b/spec/helpers/integrations_helper_spec.rb index 9822f9fac05..8be847e1c6c 100644 --- a/spec/helpers/integrations_helper_spec.rb +++ b/spec/helpers/integrations_helper_spec.rb @@ -165,7 +165,8 @@ RSpec.describe IntegrationsHelper do with_them do before do - issue.update!(issue_type: issue_type) + issue.assign_attributes(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type)) + issue.save!(validate: false) end it "return the correct i18n issue type" do diff --git a/spec/helpers/projects/ml/experiments_helper_spec.rb b/spec/helpers/projects/ml/experiments_helper_spec.rb index 004c89e5ca7..9b3c23e1f87 100644 --- a/spec/helpers/projects/ml/experiments_helper_spec.rb +++ b/spec/helpers/projects/ml/experiments_helper_spec.rb @@ -114,7 +114,8 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do 'path_to_artifact' => "/#{project.full_path}/-/packages/#{candidate.artifact.id}", 'experiment_name' => candidate.experiment.name, 'path_to_experiment' => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}", - 'status' => 'running' + 'status' => 'running', + 'path' => "/#{project.full_path}/-/ml/candidates/#{candidate.iid}" } expect(subject['info']).to include(expected_info) diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index dec62815366..d238133b59e 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -160,7 +160,7 @@ RSpec.describe Issue, feature_category: :team_planning do it 'is possible to change type only between selected types' do issue = create(:issue, old_type, project: reusable_project) - issue.work_item_type_id = WorkItems::Type.default_by_type(new_type).id + issue.assign_attributes(work_item_type: WorkItems::Type.default_by_type(new_type), issue_type: new_type) expect(issue.valid?).to eq(is_valid) end @@ -254,7 +254,7 @@ RSpec.describe Issue, feature_category: :team_planning do describe '#ensure_work_item_type' do let_it_be(:issue_type) { create(:work_item_type, :issue, :default) } - let_it_be(:task_type) { create(:work_item_type, :issue, :default) } + let_it_be(:incident_type) { create(:work_item_type, :incident, :default) } let_it_be(:project) { create(:project) } context 'when a type was already set' do @@ -271,9 +271,9 @@ RSpec.describe Issue, feature_category: :team_planning do expect(issue.work_item_type_id).to eq(issue_type.id) expect(WorkItems::Type).not_to receive(:default_by_type) - issue.update!(work_item_type: task_type, issue_type: 'task') + issue.update!(work_item_type: incident_type, issue_type: :incident) - expect(issue.work_item_type_id).to eq(task_type.id) + expect(issue.work_item_type_id).to eq(incident_type.id) end it 'ensures a work item type if updated to nil' do @@ -300,13 +300,23 @@ RSpec.describe Issue, feature_category: :team_planning do expect(issue.work_item_type_id).to be_nil expect(WorkItems::Type).not_to receive(:default_by_type) - issue.update!(work_item_type: task_type, issue_type: 'task') + issue.update!(work_item_type: incident_type, issue_type: :incident) - expect(issue.work_item_type_id).to eq(task_type.id) + expect(issue.work_item_type_id).to eq(incident_type.id) end end end + describe '#check_issue_type_in_sync' do + it 'raises an error if issue_type is out of sync' do + issue = build(:issue, issue_type: :issue, work_item_type: WorkItems::Type.default_by_type(:task)) + + expect do + issue.save! + end.to raise_error(Issue::IssueTypeOutOfSyncError) + end + end + describe '#record_create_action' do it 'records the creation action after saving' do expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_created_action) @@ -1816,7 +1826,7 @@ RSpec.describe Issue, feature_category: :team_planning do with_them do before do - issue.update!(issue_type: issue_type) + issue.update!(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type)) end it do @@ -1836,7 +1846,7 @@ RSpec.describe Issue, feature_category: :team_planning do with_them do before do - issue.update!(issue_type: issue_type) + issue.update!(issue_type: issue_type, work_item_type: WorkItems::Type.default_by_type(issue_type)) end it do diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb index 4cc6bbe01b6..9e5e365ab9d 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -1223,17 +1223,16 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do end context 'when unsupported widget input is sent' do - let_it_be(:test_case) { create(:work_item_type, :default, :test_case) } - let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) } + let_it_be(:work_item) { create(:work_item, :incident, project: project) } let(:input) do { - 'hierarchyWidget' => {} + 'assigneesWidget' => { 'assigneeIds' => [developer.to_gid.to_s] } } end it_behaves_like 'a mutation that returns top-level errors', - errors: ["Following widget keys are not supported by Test Case type: [:hierarchy_widget]"] + errors: ["Following widget keys are not supported by Incident type: [:assignees_widget]"] end end end diff --git a/spec/requests/projects/ml/candidates_controller_spec.rb b/spec/requests/projects/ml/candidates_controller_spec.rb index 3b36ec9fdad..78c8e99e3f3 100644 --- a/spec/requests/projects/ml/candidates_controller_spec.rb +++ b/spec/requests/projects/ml/candidates_controller_spec.rb @@ -64,7 +64,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do describe 'DELETE #destroy' do let_it_be(:candidate_for_deletion) do - create(:ml_candidates, experiment: experiment, user: user) + create(:ml_candidates, project: project, experiment: experiment, user: user) end let(:candidate_iid) { candidate_for_deletion.iid } @@ -76,6 +76,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do it 'deletes the experiment', :aggregate_failures do expect(response).to have_gitlab_http_status(:found) expect(flash[:notice]).to eq('Candidate removed') + expect(response).to redirect_to("/#{project.full_path}/-/ml/experiments/#{experiment.iid}") expect { Ml::Candidate.find(id: candidate_for_deletion.id) }.to raise_error(ActiveRecord::RecordNotFound) end diff --git a/spec/serializers/issue_sidebar_basic_entity_spec.rb b/spec/serializers/issue_sidebar_basic_entity_spec.rb index 64a271e359a..f24e379ec67 100644 --- a/spec/serializers/issue_sidebar_basic_entity_spec.rb +++ b/spec/serializers/issue_sidebar_basic_entity_spec.rb @@ -44,7 +44,10 @@ RSpec.describe IssueSidebarBasicEntity do context 'for an incident issue' do before do - issue.update!(issue_type: Issue.issue_types[:incident]) + issue.update!( + issue_type: Issue.issue_types[:incident], + work_item_type: WorkItems::Type.default_by_type(:incident) + ) end it 'is present and true' do diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 5e10d1d216c..4b31a041342 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -57,7 +57,15 @@ RSpec.describe Boards::Issues::ListService, feature_category: :team_planning do end context 'when filtering' do - let_it_be(:incident) { create(:labeled_issue, project: project, milestone: m1, labels: [development, p1], issue_type: 'incident') } + let_it_be(:incident) do + create( + :labeled_issue, + :incident, + project: project, + milestone: m1, + labels: [development, p1] + ) + end context 'when filtering by type' do it 'only returns the specified type' do diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_no_needs.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_no_needs.yml new file mode 100644 index 00000000000..12c51828628 --- /dev/null +++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_no_needs.yml @@ -0,0 +1,31 @@ +config: + test1: + stage: test + script: exit 0 + needs: [] + + test2: + stage: test + when: on_failure + script: exit 0 + needs: [] + +init: + expect: + pipeline: pending + stages: + test: pending + jobs: + test1: pending + test2: skipped + +transitions: + - event: success + jobs: [test1] + expect: + pipeline: success + stages: + test: success + jobs: + test1: success + test2: skipped diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_no_prev_stage.yml b/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_no_prev_stage.yml new file mode 100644 index 00000000000..57b3aa9ae80 --- /dev/null +++ b/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_no_prev_stage.yml @@ -0,0 +1,29 @@ +config: + test1: + stage: test + script: exit 0 + + test2: + stage: test + when: on_failure + script: exit 0 + +init: + expect: + pipeline: pending + stages: + test: pending + jobs: + test1: pending + test2: skipped + +transitions: + - event: success + jobs: [test1] + expect: + pipeline: success + stages: + test: success + jobs: + test1: success + test2: skipped diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index fecfc3f3d64..bca6a3cd4f9 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -172,37 +172,5 @@ RSpec.describe Issues::BuildService, feature_category: :team_planning do end end end - - describe 'setting issue type' do - context 'with a corresponding WorkItems::Type' do - let_it_be(:type_issue_id) { WorkItems::Type.default_issue_type.id } - let_it_be(:type_incident_id) { WorkItems::Type.default_by_type(:incident).id } - - where(:issue_type, :current_user, :work_item_type_id, :resulting_issue_type) do - nil | ref(:guest) | ref(:type_issue_id) | 'issue' - 'issue' | ref(:guest) | ref(:type_issue_id) | 'issue' - 'incident' | ref(:guest) | ref(:type_issue_id) | 'issue' - 'incident' | ref(:reporter) | ref(:type_incident_id) | 'incident' - # update once support for test_case is enabled - 'test_case' | ref(:guest) | ref(:type_issue_id) | 'issue' - # update once support for requirement is enabled - 'requirement' | ref(:guest) | ref(:type_issue_id) | 'issue' - 'invalid' | ref(:guest) | ref(:type_issue_id) | 'issue' - # ensure that we don't set a value which has a permission check but is an invalid issue type - 'project' | ref(:guest) | ref(:type_issue_id) | 'issue' - end - - with_them do - let(:user) { current_user } - - it 'builds an issue' do - issue = build_issue(issue_type: issue_type) - - expect(issue.issue_type).to eq(resulting_issue_type) - expect(issue.work_item_type_id).to eq(work_item_type_id) - end - end - end - end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index df47780bc89..88dcc870ee7 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -574,36 +574,6 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do end context 'Quick actions' do - context 'as work item' do - let(:opts) do - { - title: "My work item", - work_item_type: work_item_type, - description: "/shrug" - } - end - - context 'when work item type is not the default Issue' do - let(:work_item_type) { create(:work_item_type, namespace: project.namespace) } - - it 'saves the work item without applying the quick action' do - expect(result).to be_success - expect(issue).to be_persisted - expect(issue.description).to eq("/shrug") - end - end - - context 'when work item type is the default Issue' do - let(:work_item_type) { WorkItems::Type.default_by_type(:issue) } - - it 'saves the work item and applies the quick action' do - expect(result).to be_success - expect(issue).to be_persisted - expect(issue.description).to eq(" ¯\\_(ツ)_/¯") - end - end - end - context 'with assignee, milestone, and contact in params and command' do let_it_be(:contact) { create(:contact, group: group) } @@ -696,6 +666,23 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do expect(issue.labels).to eq([label]) end end + + context 'when using promote_to_incident' do + let(:opts) { { title: 'Title', description: '/promote_to_incident' } } + + before do + project.add_developer(user) + end + + it 'creates an issue with the correct issue type' do + expect { result }.to change(Issue, :count).by(1) + + created_issue = Issue.last + + expect(created_issue.issue_type).to eq('incident') + expect(created_issue.work_item_type).to eq(WorkItems::Type.default_by_type('incident')) + end + end end context 'resolving discussions' do @@ -864,5 +851,49 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do subject.execute end end + + describe 'setting issue type' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:guest) { user.tap { |u| project.add_guest(u) } } + let_it_be(:reporter) { assignee.tap { |u| project.add_reporter(u) } } + + context 'with a corresponding WorkItems::Type' do + let_it_be(:type_issue_id) { WorkItems::Type.default_issue_type.id } + let_it_be(:type_incident_id) { WorkItems::Type.default_by_type(:incident).id } + + where(:issue_type, :current_user, :work_item_type_id, :resulting_issue_type) do + nil | ref(:guest) | ref(:type_issue_id) | 'issue' + 'issue' | ref(:guest) | ref(:type_issue_id) | 'issue' + 'incident' | ref(:guest) | ref(:type_issue_id) | 'issue' + 'incident' | ref(:reporter) | ref(:type_incident_id) | 'incident' + # update once support for test_case is enabled + 'test_case' | ref(:guest) | ref(:type_issue_id) | 'issue' + # update once support for requirement is enabled + 'requirement' | ref(:guest) | ref(:type_issue_id) | 'issue' + 'invalid' | ref(:guest) | ref(:type_issue_id) | 'issue' + # ensure that we don't set a value which has a permission check but is an invalid issue type + 'project' | ref(:guest) | ref(:type_issue_id) | 'issue' + end + + with_them do + let(:user) { current_user } + let(:params) { { title: 'title', issue_type: issue_type } } + let(:issue) do + described_class.new( + container: project, + current_user: user, + params: params, + spam_params: spam_params + ).execute[:issue] + end + + it 'creates an issue' do + expect(issue.issue_type).to eq(resulting_issue_type) + expect(issue.work_item_type_id).to eq(work_item_type_id) + end + end + end + end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index a5a18562ca5..167bff2a492 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1493,31 +1493,5 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning let(:existing_issue) { create(:issue, project: project) } let(:issuable) { described_class.new(container: project, current_user: user, params: params).execute(existing_issue) } end - - context 'with quick actions' do - context 'as work item' do - let(:opts) { { description: "/shrug" } } - - context 'when work item type is not the default Issue' do - let(:issue) { create(:work_item, :task, description: "") } - - it 'does not apply the quick action' do - expect do - update_issue(opts) - end.to change(issue, :description).to("/shrug") - end - end - - context 'when work item type is the default Issue' do - let(:issue) { create(:work_item, :issue, description: "") } - - it 'does not apply the quick action' do - expect do - update_issue(opts) - end.to change(issue, :description).to(" ¯\\_(ツ)_/¯") - end - end - end - end end end diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index b474285e67e..78a17aed707 100644 --- a/spec/services/notes/quick_actions_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -182,7 +182,7 @@ RSpec.describe Notes::QuickActionsService, feature_category: :team_planning do context 'on an incident' do before do - issue.update!(issue_type: :incident) + issue.update!(issue_type: :incident, work_item_type: WorkItems::Type.default_by_type(:incident)) end it 'leaves the note empty' do @@ -224,7 +224,7 @@ RSpec.describe Notes::QuickActionsService, feature_category: :team_planning do context 'on an incident' do before do - issue.update!(issue_type: :incident) + issue.update!(issue_type: :incident, work_item_type: WorkItems::Type.default_by_type(:incident)) end it 'leaves the note empty' do diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb index 0e9e49bc877..46e598c3f11 100644 --- a/spec/services/work_items/create_service_spec.rb +++ b/spec/services/work_items/create_service_spec.rb @@ -81,6 +81,37 @@ RSpec.describe WorkItems::CreateService, feature_category: :team_planning do end end + context 'when applying quick actions' do + let(:work_item) { service_result[:work_item] } + let(:opts) do + { + title: 'My work item', + work_item_type: work_item_type, + description: '/shrug' + } + end + + context 'when work item type is not the default Issue' do + let(:work_item_type) { create(:work_item_type, :task, namespace: group) } + + it 'saves the work item without applying the quick action' do + expect(service_result).to be_success + expect(work_item).to be_persisted + expect(work_item.description).to eq('/shrug') + end + end + + context 'when work item type is the default Issue' do + let(:work_item_type) { WorkItems::Type.default_by_type(:issue) } + + it 'saves the work item and applies the quick action' do + expect(service_result).to be_success + expect(work_item).to be_persisted + expect(work_item.description).to eq(' ¯\_(ツ)_/¯') + end + end + end + context 'when params are valid' do it 'created instance is a WorkItem' do expect(Issuable::CommonSystemNotesService).to receive_message_chain(:new, :execute) diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb index 5647f8c085c..2cf52ee853a 100644 --- a/spec/services/work_items/update_service_spec.rb +++ b/spec/services/work_items/update_service_spec.rb @@ -44,6 +44,33 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do end end + context 'when applying quick actions' do + let(:opts) { { description: "/shrug" } } + + context 'when work item type is not the default Issue' do + before do + task_type = WorkItems::Type.default_by_type(:task) + work_item.update_columns(issue_type: task_type.base_type, work_item_type_id: task_type.id) + end + + it 'does not apply the quick action' do + expect do + update_work_item + end.to change(work_item, :description).to('/shrug') + end + end + + context 'when work item type is the default Issue' do + let(:issue) { create(:work_item, :issue, description: '') } + + it 'applies the quick action' do + expect do + update_work_item + end.to change(work_item, :description).to(' ¯\_(ツ)_/¯') + end + end + end + context 'when title is changed' do let(:opts) { { title: 'changed' } } diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb index 78863fe1169..c68d53db01e 100644 --- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb +++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb @@ -914,9 +914,9 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context end context 'filtering by item type' do - let_it_be(:incident_item) { create(factory, issue_type: :incident, project: project1) } - let_it_be(:objective) { create(factory, issue_type: :objective, project: project1) } - let_it_be(:key_result) { create(factory, issue_type: :key_result, project: project1) } + let_it_be(:incident_item) { create(factory, :incident, project: project1) } + let_it_be(:objective) { create(factory, :objective, project: project1) } + let_it_be(:key_result) { create(factory, :key_result, project: project1) } context 'no type given' do let(:params) { { issue_types: [] } } diff --git a/spec/workers/incident_management/close_incident_worker_spec.rb b/spec/workers/incident_management/close_incident_worker_spec.rb index bf967a42ceb..3c2e69a4675 100644 --- a/spec/workers/incident_management/close_incident_worker_spec.rb +++ b/spec/workers/incident_management/close_incident_worker_spec.rb @@ -36,7 +36,7 @@ RSpec.describe IncidentManagement::CloseIncidentWorker, feature_category: :incid context 'when issue type is not incident' do before do - issue.update!(issue_type: :issue) + issue.update!(issue_type: :issue, work_item_type: WorkItems::Type.default_by_type(:issue)) end it_behaves_like 'does not call the close issue service' diff --git a/yarn.lock b/yarn.lock index d1bc955c14e..1143743e7f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1085,10 +1085,19 @@ resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e" integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg== +"@gitlab/cluster-client@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@gitlab/cluster-client/-/cluster-client-1.2.0.tgz#3b56da46748403354b5af73678b17db3851cbe53" + integrity sha512-2emHgfOF9CdibzwXJ2yZVf2d+ez8b67O47qa+pwlG+NnYatjfIcj9Pkzs4kBcO/9+j2lVH/EegPDyEkZZt8Irg== + dependencies: + axios "^0.24.0" + core-js "^3.29.1" + "@gitlab/eslint-plugin@18.3.2": version "18.3.2" resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-18.3.2.tgz#dc4d5b487e26a1473106c1a3e34ae3ea219d4dd1" integrity sha512-Lz0RnEW5isZ/jkeHcr2k6NqaHISwgKeWN/vkWUU5J4Ax7oYPR0CgA2KO/dEnOvIPmGfbnUKowsekBmmy5SUQHA== + dependencies: "@babel/core" "^7.17.0" "@babel/eslint-parser" "^7.17.0" @@ -1827,182 +1836,181 @@ dom-accessibility-api "^0.5.1" pretty-format "^26.4.2" -"@tiptap/core@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.220.tgz#ced4b8f13ad6361f957275510bd0c005de29d18c" - integrity sha512-F2Q666xJqijBU5o+GqekqseNgIEMTs6BhsLDaf9DwThhljGLS8RXKnSvQxrxLNrYEPpw39n/G3Qt8YAOk5qR6w== +"@tiptap/core@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.3.tgz#dfd55124b3e7b0482e5ccb8be46eb9c3189167e2" + integrity sha512-jLyVIWAdjjlNzrsRhSE2lVL/7N8228/1R1QtaVU85UlMIwHFAcdzhD8FeiKkqxpTnGpaDVaTy7VNEtEgaYdCyA== -"@tiptap/extension-blockquote@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.0.0-beta.220.tgz#acce6a7d2fda829296e1e0b6386f618ea8ae328e" - integrity sha512-uE1VRU/doQzXsfsZ/JqsbSbXeZYTJnyQkSfHYA2ZYhbEM2XqDEsYkgcmZEJgunUZJpERf+3ZTfTpqaHq29iMMg== +"@tiptap/extension-blockquote@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.0.3.tgz#3ee7aff66a2526501154ca69f3e91e153c58313c" + integrity sha512-rkUcFv2iL6f86DBBHoa4XdKNG2StvkJ7tfY9GoMpT46k3nxOaMTqak9/qZOo79TWxMLYtXzoxtKIkmWsbbcj4A== -"@tiptap/extension-bold@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.0-beta.220.tgz#f10468317fd5c63ebab68be907e33fb138a60ef9" - integrity sha512-KcEuKI85Drug/cCWbDy+HxhYrD+rLXHEBG10DmKPvgPpKHG/2wOau6LwUwyV4muWR8CR2mIO+mEc3yVBD8nNwQ== +"@tiptap/extension-bold@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.0.3.tgz#2a28816195562a39c33f50e626796d14a800784f" + integrity sha512-OGT62fMRovSSayjehumygFWTg2Qn0IDbqyMpigg/RUAsnoOI2yBZFVrdM2gk1StyoSay7gTn2MLw97IUfr7FXg== -"@tiptap/extension-bubble-menu@2.0.0-beta.220", "@tiptap/extension-bubble-menu@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.220.tgz#3fea0c846f73a237f562fdce05671ef1fa025943" - integrity sha512-wthyec7s0vZlTSEAAZEgoFfx/1Arwg1zxDUrrE+YAost/Yn+w4xQksz/ts5Bx90iOk2qsJ+jzzttLRV17Ku7lA== +"@tiptap/extension-bubble-menu@2.0.3", "@tiptap/extension-bubble-menu@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.3.tgz#44b3c4e35fd478c42467d8fb7dbc9532614e5b18" + integrity sha512-lPt1ELrYCuoQrQEUukqjp9xt38EwgPUwaKHI3wwt2Rbv+C6q1gmRsK1yeO/KqCNmFxNqF2p9ZF9srOnug/RZDQ== dependencies: - lodash "^4.17.21" tippy.js "^6.3.7" -"@tiptap/extension-bullet-list@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.0-beta.220.tgz#ffc04992bbee53bc858aab6c082f17419a2236b7" - integrity sha512-QQ/0ZlYy6Hgb+UAc79V+fxvI+AaQf20cbKtBXaR8TIZ0x4FotSma89bKh+CIXMhFiBGXTcYBaYhl7OwACsKtxw== - -"@tiptap/extension-code-block-lowlight@2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.0.0-beta.220.tgz#9496a1989b5385872456f6910a43ac822b4896c1" - integrity sha512-xMwbl5O50yaIGYQF3yrBM7Ft0JYejkuEo161jRElYG+PYvUCvbf2wB5oLNtRknj00WOM01kPXH2xMTuk8fTOPg== - -"@tiptap/extension-code-block@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.0.0-beta.220.tgz#8396b72f634d77d23b9ea01c9a253e8a7f471471" - integrity sha512-fgA7yTfHqhBtMJF7I9FPJ6UWuZPtxOQiN45Iv9LNmFIB6YRucdpmF+daZ27sElu0a+eICZyXwVn4w4iJphifuw== - -"@tiptap/extension-code@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.0.0-beta.220.tgz#3543afeda2b0b240682a36eeb401b00a3da56ab6" - integrity sha512-JKKDZoceagqVXeC1XF/gOkKhLtsbYJYV+MRDorLnQVz4tXcg/SMs5Ez7OM9MxSSior8fIbUFMNsj1/UNlG+tFw== - -"@tiptap/extension-document@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.0-beta.220.tgz#15b4db7a92659eff7efc6d4d877dcf72e3fd61b6" - integrity sha512-2sja4ZvOb4iynHrzinnclCSFgLyo6fJc1fBV5fIYaOgZOYcvz9KK8fgKiq+wIpG58sJEmQ5kcwwBlkXv+NTK+g== - -"@tiptap/extension-dropcursor@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.0-beta.220.tgz#b635fa6cdf9be1027579c7ab6c00e5a811b3b30b" - integrity sha512-BIaA4Lvb3xL9KFN+K6SO2IHqLO6hDmGN2/rGKHFaU3Eh+oiXM2G73KTSS5KIP1u872zY1RpAtswSc4kjv3cuVw== - -"@tiptap/extension-floating-menu@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.220.tgz#35eb154227533ada738c922be2f8cf18426fe4bf" - integrity sha512-+WfcBEedm82ntaVIEQAGz0Om96Rpav7a+4f7e8N4PrLKm6nZ3gBaEkZVQ6vjJ6S/1htiWCv1XosYIwRboPBG0w== +"@tiptap/extension-bullet-list@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.3.tgz#43c4c0c161d5c065f3f87e4bf54d13bd6c55b4c3" + integrity sha512-RtaLiRvZbMTOje+FW5bn+mYogiIgNxOm065wmyLPypnTbLSeHeYkoqVSqzZeqUn+7GLnwgn1shirUe6csVE/BA== + +"@tiptap/extension-code-block-lowlight@2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.0.3.tgz#42cad47b048d4657cb0d554a890abd1bc7072451" + integrity sha512-thFXcFdFyHF0/dr9sqBedjj0Vt14k3m52YVc4l65+d65wRuHp4f8suu8T2ZGRJwqLCE3NIrvwQTSHhzjIqJVxQ== + +"@tiptap/extension-code-block@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.0.3.tgz#4ce08b4f3c5af166d3cc00e91ba5b989f01fee63" + integrity sha512-F4xMy18EwgpyY9f5Te7UuF7UwxRLptOtCq1p2c2DfxBvHDWhAjQqVqcW/sq/I/WuED7FwCnPLyyAasPiVPkLPw== + +"@tiptap/extension-code@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.0.3.tgz#74d88073faedd1fc52d6ed3de4eed8fde80ff4bf" + integrity sha512-LsVCKVxgBtkstAr1FjxN8T3OjlC76a2X8ouoZpELMp+aXbjqyanCKzt+sjjUhE4H0yLFd4v+5v6UFoCv4EILiw== + +"@tiptap/extension-document@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.0.3.tgz#b58af5b4f71c0acea953a7ebe8b1d24341bfaf68" + integrity sha512-PsYeNQQBYIU9ayz1R11Kv/kKNPFNIV8tApJ9pxelXjzcAhkjncNUazPN/dyho60mzo+WpsmS3ceTj/gK3bCtWA== + +"@tiptap/extension-dropcursor@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.3.tgz#205d02c70b200810572d0b7e264bbdb718343ad0" + integrity sha512-McthMrfusn6PjcaynJLheZJcXto8TaIW5iVitYh8qQrDXr31MALC/5GvWuiswmQ8bAXiWPwlLDYE/OJfwtggaw== + +"@tiptap/extension-floating-menu@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.3.tgz#8d9943246aa3247442c1993f235617094fe705b5" + integrity sha512-zN1vRGRvyK3pO2aHRmQSOTpl4UJraXYwKYM009n6WviYKUNm0LPGo+VD4OAtdzUhPXyccnlsTv2p6LIqFty6Bg== dependencies: tippy.js "^6.3.7" -"@tiptap/extension-gapcursor@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.220.tgz#07c96f7adc354d19b6209ea1e080188fb8d63de5" - integrity sha512-W5N2Ey+thufUOrs2TFGpEGBGue7ZEhcUXvxcsZlGbrjVa9Y+4rEp68Du4y7yM0hCeSj2GGwiV+uPzkc0CSDE/g== - -"@tiptap/extension-hard-break@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.0-beta.220.tgz#8ff432615d9c9090c3d59c2a745c88e4f39ab1a3" - integrity sha512-oY3454o53YNFbuokzyGzG4PdMHkIYreY3nrALioZ0SwYeoFNcGA6Zcn4rDRfdp+QvbbiHfeBTR/CpWF13HZYTg== - -"@tiptap/extension-heading@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.0.0-beta.220.tgz#b4889de7b3f152ff88a119d6cb6a22537eff73a2" - integrity sha512-7mrHRj++UaZ26C2Gjwb0WKWAzpiKb8TOYkVC2uMaCwaNhLDXpFEwZ7RtJRSTNBHkIGnMO46BH8Z0qlkFMmk9Jw== - -"@tiptap/extension-highlight@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.0.0-beta.220.tgz#1bf2954524b99bb393dad46b5613b84aa660713f" - integrity sha512-+h4seFq99b0dCmShVlSc44PBQUiW4xBXze61V6ZNILLkfzo27wrj0W+I3WrdSXX9uz3wwE/BR+3T8m1Ro8lHng== - -"@tiptap/extension-history@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.0-beta.220.tgz#6370b28872b29288d655cd14211efb8dc76daba0" - integrity sha512-qNL2a9UhnlmCs4y2iQYrfeMB8vEX3bHozBJanHu0PWNQJcj90R5xqorBp/bRcqZdi0kuQfxcTnGHtLUpN/U0TA== - -"@tiptap/extension-horizontal-rule@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.0-beta.220.tgz#4b8eaf081b38359235312308ebd59950705c7b10" - integrity sha512-XMIs4R+4BoH5LpIxey513mZuus0XLHqjVayqtf03enmjBTLWzkixvvWLPLw4a47FJL5Q8l4REFHxjNifRzOKkg== - -"@tiptap/extension-image@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.0-beta.220.tgz#c197b0dbd2f5d7a08f91e63cb8ca98c1972159a4" - integrity sha512-xyzlY/cupj/7AVqybQDaPaJ3SwKqe12xMWQlWxhhksuNpbQ6RGHrJz0DBSe61kIkaTZmIUBw055IFEMOPFF53g== - -"@tiptap/extension-italic@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.220.tgz#94e442689f69e694a2a983eabcae0ccc803262b9" - integrity sha512-aWAgqoR8fql9fJ7T/ZrEqovkEjZXbUpvlvWEvdBDMG3id8ZTGNDpdDKdvI6J/Rl5ZGPIg1TpHJtd+UixheWQsQ== - -"@tiptap/extension-link@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.0-beta.220.tgz#c9954613cd1e0a0f1527853b732ef50dff734eac" - integrity sha512-vjEA8cE37ZZVVgPHSpttw3kbJoClb+ya/BVukDtJ1h6C7mIR1rqzNxTgpbnXJuA8xww0JOjpa5dpzEgcs294fA== +"@tiptap/extension-gapcursor@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.3.tgz#e098b78c4a169e1630dc6531d68b7f365de59c2f" + integrity sha512-6I9EzzsYOyyqDvDvxIK6Rv3EXB+fHKFj8ntHO8IXmeNJ6pkhOinuXVsW6Yo7TcDYoTj4D5I2MNFAW2rIkgassw== + +"@tiptap/extension-hard-break@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.3.tgz#aa7805d825e5244bdccc508da18c781e231b2859" + integrity sha512-RCln6ARn16jvKTjhkcAD5KzYXYS0xRMc0/LrHeV8TKdCd4Yd0YYHe0PU4F9gAgAfPQn7Dgt4uTVJLN11ICl8sQ== + +"@tiptap/extension-heading@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.0.3.tgz#5e9e779f33f366afcf729d9f68ef49721f825e11" + integrity sha512-f0IEv5ms6aCzL80WeZ1qLCXTkRVwbpRr1qAETjg3gG4eoJN18+lZNOJYpyZy3P92C5KwF2T3Av00eFyVLIbb8Q== + +"@tiptap/extension-highlight@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.0.3.tgz#4a52de6666dfe4a80b018aa43805d2d220e90219" + integrity sha512-NrtibY8cZkIjZMQuHRrKd4php+plOvAoSo8g3uVFu275I/Ixt5HqJ53R4voCXs8W8BOBRs2HS2QX8Cjh79XhtA== + +"@tiptap/extension-history@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.0.3.tgz#8936c15aa46f2ddeada1c3d9abe2888d58d08c30" + integrity sha512-00KHIcJ8kivn2ARI6NQYphv2LfllVCXViHGm0EhzDW6NQxCrriJKE3tKDcTFCu7LlC5doMpq9Z6KXdljc4oVeQ== + +"@tiptap/extension-horizontal-rule@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.3.tgz#5c67db2c0bf3bc14a8aab80df584bee5aa23fbeb" + integrity sha512-SZRUSh07b/M0kJHNKnfBwBMWrZBEm/E2LrK1NbluwT3DBhE+gvwiEdBxgB32zKHNxaDEXUJwUIPNC3JSbKvPUA== + +"@tiptap/extension-image@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.3.tgz#048484b2e059d4bed78f97f08651bd57b41855a9" + integrity sha512-hS9ZJwz0md07EHsC+o4NuuJkhCZsZn7TuRz/2CvRSj2fWFIz+40CyNAHf/2J0qNugG9ommXaemetsADeEZP9ag== + +"@tiptap/extension-italic@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.3.tgz#2d9d5d8ccf3c38266f745029c2ec0646c075c1fc" + integrity sha512-cfS5sW0gu7qf4ihwnLtW/QMTBrBEXaT0sJl3RwkhjIBg/65ywJKE5Nz9ewnQHmDeT18hvMJJ1VIb4j4ze9jj9A== + +"@tiptap/extension-link@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.0.3.tgz#4714a4c23d04032e75b5b8364a9c532f7a385aba" + integrity sha512-H72tXQ5rkVCkAhFaf08fbEU7EBUCK0uocsqOF+4th9sOlrhfgyJtc8Jv5EXPDpxNgG5jixSqWBo0zKXQm9s9eg== dependencies: linkifyjs "^4.1.0" -"@tiptap/extension-list-item@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.0-beta.220.tgz#c2fcff1fb9148d303d78b0336032a6353a86ff6c" - integrity sha512-+O0ivwxPP2l/m9PAowb2ytDT/cM5kwu0s1W5MUsHPIqf+M6ahnl4ESjhWZfDHUzvjqPq6MTbqoQLHbB1KS/N7w== - -"@tiptap/extension-ordered-list@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.0-beta.220.tgz#1fac8e8c2f8c0187e23ede59764fd031d5d1a83a" - integrity sha512-j3DmxJfwmNxFfMnvO7glmGlhYeZSIUnRrKnZu2KkpD6OcGJSh9y/yfnYwcuK80XbzEG/jKKIw0M2yRveOvyVwA== - -"@tiptap/extension-paragraph@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.0-beta.220.tgz#d552dfdeeab9856e9eb8f0a7cf850f37d7cced69" - integrity sha512-ZGCzNGFYV4wa3l1nXtDIaYp7O6f0DrGTSl3alKkDTQe3SOmzXS2HjgWl9yPw8VXpU9W5mMGhXd+nGn/jUk+f/A== - -"@tiptap/extension-strike@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.0-beta.220.tgz#2beb02d2d8807056ff3ea4ea74d9f6abba42bf78" - integrity sha512-cIM2ma6mzk08pijOn+KS3ZoHWaUVsVT+OF3m6xewjwJdC0ILg9nApEOhPFrhbeDcxcPmJMlgBl/xeUrEu1HQMg== - -"@tiptap/extension-subscript@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-subscript/-/extension-subscript-2.0.0-beta.220.tgz#3b9ebd181804f411f7755b8a73d1863dc72c5b8a" - integrity sha512-+C6nyAU4aaeCMvtBI1CJrMseE+YYqLUmmUVOK4ka3ZjmYkn1n+Tduf0ZGQHYmSSMDHPqQ8KsN+AQwaeSWKM/dA== - -"@tiptap/extension-superscript@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-superscript/-/extension-superscript-2.0.0-beta.220.tgz#93289e25bce0fb13608d7d41e74a13a3faf4d3c6" - integrity sha512-h7Qh8Jqb5r84hS0GhhQdNPFk+6AZhvbOKv/4dP6g9S5mRc287WlfhTrbpMdHI/p0r5vKkpLmAXpNCn6IImd3jQ== - -"@tiptap/extension-table-cell@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.0.0-beta.220.tgz#b3ae21a72a2012cb5e2656d91c90b7424672e6f8" - integrity sha512-JvX9CTaDBBbI1Qra7pwhsv0vD6Y3A+X6PL7EYVrqIHZlmWq7Lz2ELxjx8RkWyp2LzowVNgZwUu2i3yHakaX5oA== - -"@tiptap/extension-table-header@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.0.0-beta.220.tgz#3f6050847bc978dbcb117a9fa7dd16e13dd3b633" - integrity sha512-oOCBxrOuHCy4feuZKcBU9WWxi2SqBwfn/rmzSU6loKK8rR1+0olyAYu8IREb6DMmemTxl0ITp74hBxKeZyzjrA== - -"@tiptap/extension-table-row@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.0.0-beta.220.tgz#5339c2be44cbbf871768a809a1f3694bef2031e0" - integrity sha512-DbYfrzLREulL+xOx74XAuhuqHUNi0t9hXDzG6RYdPiNnMhX/HhmTIV7bLNjEGxy6rOX0LhDzrBpNlA1elYUrwQ== - -"@tiptap/extension-table@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.0.0-beta.220.tgz#42731e23e98d2c074e4e0460525718e57ebfc6a9" - integrity sha512-wdA957lSwIPtaSEAGw/KDXvhKAv28XkooHctY8FxqxEtvyMyCA8v0YXuOhGny/Uz6VZE+vdRiESMjwRU4ZQQ4g== - -"@tiptap/extension-task-item@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.0.0-beta.220.tgz#4c83d7b55587da9c8ed51cca6cd0edee13ff188d" - integrity sha512-dta4V3GkL3C+gYUUkv26gxvCD11JYE7XYp4GSED/1X/3aHOdV9HcYRtIVnHqb4YwfuX/AJyIDfjhxc2tNGevkQ== - -"@tiptap/extension-task-list@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-task-list/-/extension-task-list-2.0.0-beta.220.tgz#ba349d84dfb9fd5dff90bfea8fb234cd90383d78" - integrity sha512-Hix7/Er4T4xKz4uLTxniJaDtcctmooaxoHiHv4yDUOXZYiK5BZypr8cbCcUaoD3qpfGe8O5JBzY2sbwk0PkNwA== - -"@tiptap/extension-text@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.0-beta.220.tgz#3f51d4aac11c16d79cf8ca22502898b67f5bc2f5" - integrity sha512-3tnffc2YMjNyv7Lbad6fx9wYDE/Buz8vhx76M2AOSrjYbzmTJf7mLkgdlPM0VTy7FGZD5CGgHJAgYNt5HIqPkQ== - -"@tiptap/pm@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.0.0-beta.220.tgz#04e4c98e4d042ea8d67148ec6676f7078c6bac5a" - integrity sha512-O9mGcmwUpEr630HY9RylIyZJKnpXi3xWINWNiAEfRJ1br5j5pHRoVRJQ1HzU+6+Z+i/8qp3zRHGLTBqihaZETA== +"@tiptap/extension-list-item@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.0.3.tgz#2bca673b1ed83fdc00cb208f4d5c57d4d44ddb22" + integrity sha512-p7cUsk0LpM1PfdAuFE8wYBNJ3gvA0UhNGR08Lo++rt9UaCeFLSN1SXRxg97c0oa5+Ski7SrCjIJ5Ynhz0viTjQ== + +"@tiptap/extension-ordered-list@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.3.tgz#d1e5d6fc240545dbba7f7e6666bebd658fc3b4ad" + integrity sha512-ZB3MpZh/GEy1zKgw7XDQF4FIwycZWNof1k9WbDZOI063Ch4qHZowhVttH2mTCELuyvTMM/o9a8CS7qMqQB48bw== + +"@tiptap/extension-paragraph@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.0.3.tgz#88d332158c70622d36849256f90e43ca4d226dfe" + integrity sha512-a+tKtmj4bU3GVCH1NE8VHWnhVexxX5boTVxsHIr4yGG3UoKo1c5AO7YMaeX2W5xB5iIA+BQqOPCDPEAx34dd2A== + +"@tiptap/extension-strike@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.3.tgz#4ec0001db5f51f86d06da22364114f20f073d4b3" + integrity sha512-RO4/EYe2iPD6ifDHORT8fF6O9tfdtnzxLGwZIKZXnEgtweH+MgoqevEzXYdS+54Wraq4TUQGNcsYhe49pv7Rlw== + +"@tiptap/extension-subscript@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-subscript/-/extension-subscript-2.0.3.tgz#26be9609b52dcdc1ff0f0f00e9e3bc01dd464f78" + integrity sha512-XFAEUaKxWRmTq7ePEF4aj7knelJPr2fTz0y/iSXydtS094LKwBHBzxatIZY3phrgfpDc+f51ycwarsgz27UJfg== + +"@tiptap/extension-superscript@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-superscript/-/extension-superscript-2.0.3.tgz#7d57b2517a2f2e1ffb603edba5f05c6631bfe3a7" + integrity sha512-5EBjUvkw2SXL1e8C1i0UF26/GBNHxEbiNQKw7Shy88omVa4HTY+D8KWC/j29ZW/IomUbGPlbpXp1z+1TETzmyw== + +"@tiptap/extension-table-cell@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.0.3.tgz#44369bafdad3bb5b4f96296a8e93701673079b3a" + integrity sha512-d0vpwQfRIOhqKJdoiOJybwWhjnug3QA4Mkgccp378moDRyOer3hPKavG1Ljgz087qHrN4WfdUlMGEvasYsWE7w== + +"@tiptap/extension-table-header@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.0.3.tgz#bb35953da353f757202efab6e0c0d5d390a70f51" + integrity sha512-SnGl1U6usRRS6LyAjSdhaCYLF6NWbGhjVFSmiPrjb0pOzsiVeDOiUNCyUAIYaDNnjAF2pfK6+H+uHzYPqTi+/w== + +"@tiptap/extension-table-row@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.0.3.tgz#fb7fd381435b06942dfbc2ba475072c25bf0a478" + integrity sha512-tyqeXmQLNSBsYyiNsnQuJMxNbz6dYt+P5W58+h10mjbt+hERA5+alQQyP06O2DggsT3Z0LPt7QRAlNmOBe7cyQ== + +"@tiptap/extension-table@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.0.3.tgz#780b946ca8526ac8a4044bdec4c9c720ad8ba89a" + integrity sha512-8swHqm8vRM1w9WzaAhLmY24gGoTozctz4KHKBjvFY/Ka0yXabT0+hoCCdkZLnXWi15H3pbHs2HnDBaTGL9bZTw== + +"@tiptap/extension-task-item@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.0.3.tgz#b3845b51af565dec4c2c30c73ddbbc2b9e0bd297" + integrity sha512-13u1Q769WiSNcjFieYAMuJyWXNaY9yOdw6WFg9tQg4EZ5h6+2DaxB0qmu6I3pH+wwSn2UkCkXIirAo/k7wnzbw== + +"@tiptap/extension-task-list@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-task-list/-/extension-task-list-2.0.3.tgz#7e32dd518d7bd5359faab43fb48b37e2d83f5937" + integrity sha512-NdW0RtMF2L96qy+j946mTB5Av6Qn5L3vGVWFmJA6/JPXr9Uj/grItCmqUQKHfPBSFow7UqBY82ODblP+GQFgew== + +"@tiptap/extension-text@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.0.3.tgz#12b6400a31ac6d35cbaf1822600f4c425457902f" + integrity sha512-LvzChcTCcPSMNLUjZe/A9SHXWGDHtvk73fR7CBqAeNU0MxhBPEBI03GFQ6RzW3xX0CmDmjpZoDxFMB+hDEtW1A== + +"@tiptap/pm@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.0.3.tgz#e8bb47df765fc1b7acd52f2800c52d7ff945c5ec" + integrity sha512-I9dsInD89Agdm1QjFRO9dmJtU1ldVSILNPW0pEhv9wYqYVvl4HUj/JMtYNqu2jWrCHNXQcaX/WkdSdvGJtmg5g== dependencies: prosemirror-changeset "^2.2.0" prosemirror-collab "^1.3.0" @@ -2023,18 +2031,18 @@ prosemirror-transform "^1.7.0" prosemirror-view "^1.28.2" -"@tiptap/suggestion@^2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.0-beta.220.tgz#2dc05f65e89006ffaad9f2b6a3468311a305e5ee" - integrity sha512-lYb2HOAKJLjEBbTx5VXA32wRryQiMwaKkNfr3v6UhlwoNgD6NkCYID08UJbpMV7iM+iFQp9408D/vVWFwvOuKg== +"@tiptap/suggestion@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.0.3.tgz#3f25e20f50de6748f2b65a88e264d9b5887ca16a" + integrity sha512-1y3palQStGZq13UtHjouZ50k4sotM+N56cIlFeygIv3gqdai2zGPaPQtqV9FOVVQizXpUbQMTlPSDC5Ej4SPnQ== -"@tiptap/vue-2@2.0.0-beta.220": - version "2.0.0-beta.220" - resolved "https://registry.yarnpkg.com/@tiptap/vue-2/-/vue-2-2.0.0-beta.220.tgz#a3d9d84fc3cb6f1130bcd7e23fab9a4c56034312" - integrity sha512-GGK2M/pBVZSh2E0y1JXWVW7vllKvc2b/AwqPFZmbOGLKmxbH/xeaIeqOvt2w8b8RiA3G7UOq2lUGyjhn0/PnoQ== +"@tiptap/vue-2@2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@tiptap/vue-2/-/vue-2-2.0.3.tgz#076778985d1e5ccbefb414b05c4c9804bb377258" + integrity sha512-So2cl/W11Xt1MQqK47uNrddf08ruI2ScGHaBG2WZnYDtqJfwlAChRXi67fOeo/Y1vWy/69ekv5kLeQYWw9YJAg== dependencies: - "@tiptap/extension-bubble-menu" "^2.0.0-beta.220" - "@tiptap/extension-floating-menu" "^2.0.0-beta.220" + "@tiptap/extension-bubble-menu" "^2.0.3" + "@tiptap/extension-floating-menu" "^2.0.3" "@tootallnate/once@2": version "2.0.0" @@ -3662,9 +3670,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001370: - version "1.0.30001388" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001388.tgz#88e01f4591cbd81f9f665f3f078c66b509fbe55d" - integrity sha512-znVbq4OUjqgLxMxoNX2ZeeLR0d7lcDiE5uJ4eUiWdml1J1EkxbnQq6opT9jb9SMfJxB0XA16/ziHwni4u1I3GQ== + version "1.0.30001478" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz#0ef8a1cf8b16be47a0f9fc4ecfc952232724b32a" + integrity sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw== canvas-confetti@^1.4.0: version "1.4.0" |