Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
68caf5fd88
commit
00ab3a60fe
|
|
@ -4,7 +4,7 @@ import { stringify, parse } from 'yaml';
|
|||
import { set, omit, trim } from 'lodash';
|
||||
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
|
||||
import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
|
||||
import getAllRunners from '~/ci/runner/graphql/list/all_runners.query.graphql';
|
||||
import getRunnerTags from '../../graphql/queries/runner_tags.query.graphql';
|
||||
import { DRAWER_CONTAINER_CLASS, JOB_TEMPLATE, i18n } from './constants';
|
||||
import { removeEmptyObj, trimFields } from './utils';
|
||||
import JobSetupItem from './accordion_items/job_setup_item.vue';
|
||||
|
|
@ -48,7 +48,7 @@ export default {
|
|||
},
|
||||
apollo: {
|
||||
runners: {
|
||||
query: getAllRunners,
|
||||
query: getRunnerTags,
|
||||
update(data) {
|
||||
return data?.runners?.nodes || [];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
query getRunnerTags {
|
||||
runners {
|
||||
nodes {
|
||||
id
|
||||
tagList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,9 @@ import {
|
|||
GlTooltipDirective,
|
||||
GlPopover,
|
||||
} from '@gitlab/ui';
|
||||
import semverLt from 'semver/functions/lt';
|
||||
import semverInc from 'semver/functions/inc';
|
||||
import semverPrerelease from 'semver/functions/prerelease';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
|
@ -134,18 +137,26 @@ export default {
|
|||
isVersionMismatch(agent) {
|
||||
return agent.versions.length > 1;
|
||||
},
|
||||
// isVersionOutdated determines if the agent version is outdated compared to the KAS / GitLab version
|
||||
// using the following heuristics:
|
||||
// - KAS Version is used as *server* version if available, otherwise the GitLab version is used.
|
||||
// - returns `outdated` if the agent has a different major version than the server
|
||||
// - returns `outdated` if the agents minor version is at least two proper versions older than the server
|
||||
// - *proper* -> not a prerelease version. Meaning that server prereleases (with `-rcN`) suffix are counted as the previous minor version
|
||||
//
|
||||
// Note that it does NOT support if the agent is newer than the server version.
|
||||
isVersionOutdated(agent) {
|
||||
if (!agent.versions.length) return false;
|
||||
|
||||
const [agentMajorVersion, agentMinorVersion] = this.getAgentVersionString(agent).split('.');
|
||||
const [serverMajorVersion, serverMinorVersion] = this.serverVersion.split('.');
|
||||
const agentVersion = this.getAgentVersionString(agent);
|
||||
let allowableAgentVersion = semverInc(agentVersion, 'minor');
|
||||
|
||||
const majorVersionMismatch = agentMajorVersion !== serverMajorVersion;
|
||||
const isServerPrerelease = Boolean(semverPrerelease(this.serverVersion));
|
||||
if (isServerPrerelease) {
|
||||
allowableAgentVersion = semverInc(allowableAgentVersion, 'minor');
|
||||
}
|
||||
|
||||
// We should warn user if their current GitLab and agent versions are more than 1 minor version apart:
|
||||
const minorVersionMismatch = Math.abs(agentMinorVersion - serverMinorVersion) > 1;
|
||||
|
||||
return majorVersionMismatch || minorVersionMismatch;
|
||||
return semverLt(allowableAgentVersion, this.serverVersion);
|
||||
},
|
||||
|
||||
getVersionPopoverTitle(agent) {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export default {
|
|||
language="python"
|
||||
:code="code"
|
||||
:max-height="maxHeight"
|
||||
class="gl-border"
|
||||
class="gl-border gl-p-4!"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
<script>
|
||||
import JSONTable from '~/behaviors/components/json_table.vue';
|
||||
import Prompt from '../prompt.vue';
|
||||
import { convertHtmlTableToJson } from './dataframe_util';
|
||||
|
||||
export default {
|
||||
name: 'DataframeOutput',
|
||||
components: {
|
||||
Prompt,
|
||||
JSONTable,
|
||||
},
|
||||
props: {
|
||||
count: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
rawCode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showOutput() {
|
||||
return this.index === 0;
|
||||
},
|
||||
dataframeAsJSONTable() {
|
||||
return {
|
||||
...convertHtmlTableToJson(this.rawCode),
|
||||
caption: '',
|
||||
hasFilter: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="output">
|
||||
<prompt type="Out" :count="count" :show-output="showOutput" />
|
||||
<j-s-o-n-table v-bind="dataframeAsJSONTable" class="gl-overflow-auto" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { sanitize } from '~/lib/dompurify';
|
||||
|
||||
/**
|
||||
* Converts a dataframe in the output of a Jupyter Notebook cell to a json object
|
||||
*
|
||||
* @param {string} input - the dataframe
|
||||
* @param {DOMParser} parser - the html parser
|
||||
* @returns {Object} The converted JSON object with an `items` property containing the rows.
|
||||
*/
|
||||
export function convertHtmlTableToJson(input, domParser) {
|
||||
const parser = domParser || new DOMParser();
|
||||
const htmlDoc = parser.parseFromString(sanitize(input), 'text/html');
|
||||
|
||||
if (!htmlDoc) return { fields: [], items: [] };
|
||||
|
||||
const columnNames = [...htmlDoc.querySelectorAll('table > thead th')].map(
|
||||
(head) => head.innerText,
|
||||
);
|
||||
|
||||
if (!columnNames) return { fields: [], items: [] };
|
||||
|
||||
const itemValues = [...htmlDoc.querySelectorAll('table > tbody > tr')].map((row) =>
|
||||
[...row.querySelectorAll('td')].map((item) => item.innerText),
|
||||
);
|
||||
|
||||
return {
|
||||
fields: columnNames.map((column) => ({
|
||||
key: column === '' ? 'index' : column,
|
||||
label: column,
|
||||
sortable: true,
|
||||
})),
|
||||
items: itemValues.map((values, itemIndex) => ({
|
||||
index: itemIndex,
|
||||
...Object.fromEntries(values.map((value, index) => [columnNames[index + 1], value])),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function isDataframe(output) {
|
||||
const htmlData = output.data['text/html'];
|
||||
if (!htmlData) return false;
|
||||
|
||||
return htmlData.slice(0, 20).some((line) => line.includes('dataframe'));
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import ImageOutput from './image.vue';
|
|||
import LatexOutput from './latex.vue';
|
||||
import MarkdownOutput from './markdown.vue';
|
||||
import ErrorOutput from './error.vue';
|
||||
import DataframeOutput from './dataframe.vue';
|
||||
import { isDataframe } from './dataframe_util';
|
||||
|
||||
const TEXT_MARKDOWN = 'text/markdown';
|
||||
const ERROR_OUTPUT_TYPE = 'error';
|
||||
|
|
@ -66,6 +68,8 @@ export default {
|
|||
return ImageOutput;
|
||||
} else if (output.data['image/jpeg']) {
|
||||
return ImageOutput;
|
||||
} else if (isDataframe(output)) {
|
||||
return DataframeOutput;
|
||||
} else if (output.data['text/html']) {
|
||||
return HtmlOutput;
|
||||
} else if (output.data['text/latex']) {
|
||||
|
|
|
|||
|
|
@ -491,7 +491,7 @@ span.idiff {
|
|||
// element stretching over multiple rows we instead create a repeating background image
|
||||
// for the line
|
||||
background: repeating-linear-gradient(to right, var(--gray-100, $gray-100), var(--gray-100, $gray-100) 1px, transparent 1px, transparent 14px);
|
||||
background-size: calc(var(--level) * 14px);
|
||||
background-size: calc(var(--level) * 14px) 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 14px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,9 +291,9 @@
|
|||
|
||||
.project-cell {
|
||||
@include gl-display-table-cell;
|
||||
@include gl-border-b;
|
||||
@include gl-vertical-align-top;
|
||||
@include gl-py-4;
|
||||
border-bottom: 1px solid $gray-50;
|
||||
}
|
||||
|
||||
.project-row:last-of-type {
|
||||
|
|
|
|||
|
|
@ -295,6 +295,13 @@ module Types
|
|||
def detailed_merge_status
|
||||
::MergeRequests::Mergeability::DetailedMergeStatusService.new(merge_request: object).execute
|
||||
end
|
||||
|
||||
# This is temporary to fix a bug where `committers` is already loaded and memoized
|
||||
# and calling it again with a certain GraphQL query can cause the Rails to to throw
|
||||
# a ActiveRecord::ImmutableRelation error
|
||||
def committers
|
||||
object.commits.committers
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
%p
|
||||
= _('Hi %{username}!') % { username: sanitize_name(@user.name) }
|
||||
%p
|
||||
= html_escape(_('A new personal access token, named %{token_name}, has been created.')) % { token_name: @token_name }
|
||||
= html_escape(_('A new personal access token, named %{code_start}%{token_name}%{code_end}, has been created.')) % { code_start: '<code>'.html_safe, token_name: @token_name, code_end: '</code>'.html_safe }
|
||||
%p
|
||||
- pat_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
|
||||
= html_escape(_('You can check it in your %{pat_link_start}personal access tokens%{pat_link_end} settings.')) % { pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe }
|
||||
|
|
|
|||
|
|
@ -4,9 +4,37 @@ group: Source Code
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Introduction to GitLab Flow **(FREE)**
|
||||
# Introduction to Git workflows **(FREE)**
|
||||
|
||||
With Git, you can use a variety of branching strategies and workflows.
|
||||
Having a structured workflow for collaboration in complex projects is
|
||||
crucial for several reasons:
|
||||
|
||||
- **Code organization**: Keep the codebase organized, prevent
|
||||
overlapping work, and ensure focused efforts towards a common goal.
|
||||
|
||||
- **Version control**: Allow simultaneous work on different features
|
||||
without conflicts, maintaining code stability.
|
||||
|
||||
- **Code quality**: A code review and approval process helps maintain high
|
||||
code quality and adherence to coding standards.
|
||||
|
||||
- **Traceability and accountability**: Enable tracking of changes and their authors,
|
||||
simplifying issue identification and responsibility assignment.
|
||||
|
||||
- **Easier onboarding**: Help new team members quickly grasp the
|
||||
development process, and start contributing effectively.
|
||||
|
||||
- **Time and resource management**: Enable better planning, resource
|
||||
allocation, and meeting deadlines, ensuring an efficient development
|
||||
process.
|
||||
|
||||
- **CI/CD**: Incorporate automated testing and deployment
|
||||
processes, streamlining the release cycle and delivering high-quality
|
||||
software consistently.
|
||||
|
||||
A structured workflow promotes organization, efficiency, and code
|
||||
quality, leading to a more successful and streamlined development process.
|
||||
|
||||
Because the default workflow is not specifically defined, many organizations
|
||||
end up with workflows that are too complicated, not clearly defined, or
|
||||
|
|
@ -14,12 +42,51 @@ not integrated with their issue tracking systems.
|
|||
|
||||
Your organization can use GitLab with any workflow you choose.
|
||||
|
||||
However, if you are looking for guidance on best practices, you can use
|
||||
the GitLab Flow. This workflow combines [feature-driven development](https://en.wikipedia.org/wiki/Feature-driven_development)
|
||||
and [feature branches](https://martinfowler.com/bliki/FeatureBranch.html) with issue tracking.
|
||||
## Workflow types
|
||||
|
||||
While this workflow used at GitLab, you can choose whichever workflow
|
||||
suits your organization best.
|
||||
Here are some of the most common Git workflows.
|
||||
|
||||
### Centralized workflow
|
||||
|
||||
Best suited for small teams transitioning from a centralized version
|
||||
control system like SVN. All team members work on a single branch,
|
||||
usually `main`, and push their changes directly to the central
|
||||
repository.
|
||||
|
||||
### Feature branch workflow
|
||||
|
||||
Developers create separate branches for each feature or bugfix,
|
||||
keeping the 'main' branch stable. When a feature is complete, the
|
||||
developer submits a pull request or merge request to integrate the
|
||||
changes back into the `main` branch after a code review.
|
||||
|
||||
### Forking workflow
|
||||
|
||||
Commonly used in open-source projects, this workflow allows external
|
||||
contributors to work without direct access to the main repository.
|
||||
Developers create a fork (a personal copy) of the main repository,
|
||||
make changes in their fork, and then submit a pull request or merge
|
||||
request to have their changes integrated into the main repository.
|
||||
|
||||
### Git flow workflow
|
||||
|
||||
This workflow is best for projects with a structured release cycle.
|
||||
It introduces two long-lived branches: `main` for production-ready
|
||||
code and `develop` for integrating features. Additional branches like
|
||||
`feature`, `release`, and `hotfix` are used for specific purposes,
|
||||
ensuring a strict and organized development process.
|
||||
|
||||
### GitLab/GitHub flow
|
||||
|
||||
A simplified workflow primarily used for web development and
|
||||
continuous deployment. It combines aspects of the Feature branch
|
||||
workflow and the Git flow workflow. Developers create feature branches
|
||||
from `main`, and after the changes are complete, they are merged back
|
||||
into the `main` branch, which is then immediately deployed.
|
||||
|
||||
Each of these Git workflows has its advantages and is suited to
|
||||
different project types and team structures. Below the most popular
|
||||
workflows are reviewed in more details.
|
||||
|
||||
## Git workflow
|
||||
|
||||
|
|
@ -99,6 +166,16 @@ This flow is clean and straightforward, and many organizations have adopted it w
|
|||
Atlassian recommends [a similar strategy](https://www.atlassian.com/blog/git/simple-git-workflow-is-simple), although they rebase feature branches.
|
||||
Merging everything into the `main` branch and frequently deploying means you minimize the amount of unreleased code. This approach is in line with lean and continuous delivery best practices.
|
||||
However, this flow still leaves a lot of questions unanswered regarding deployments, environments, releases, and integrations with issues.
|
||||
|
||||
## Introduction to GitLab Flow **(FREE)**
|
||||
|
||||
However, if you are looking for guidance on best practices, you can use
|
||||
the GitLab Flow. This workflow combines [feature-driven development](https://en.wikipedia.org/wiki/Feature-driven_development)
|
||||
and [feature branches](https://martinfowler.com/bliki/FeatureBranch.html) with issue tracking.
|
||||
|
||||
While this workflow used at GitLab, you can choose whichever workflow
|
||||
suits your organization best.
|
||||
|
||||
With GitLab flow, we offer additional guidance for these questions.
|
||||
|
||||
## Production branch with GitLab flow
|
||||
|
|
@ -113,7 +190,8 @@ While this is possible in some cases, such as SaaS applications, there are some
|
|||
|
||||
In these cases, you can create a production branch that reflects the deployed code.
|
||||
You can deploy a new version by merging `main` into the `production` branch.
|
||||
While not shown in the graph below, the work on the `main` branch works just like in GitHub flow, i.e. with feature-branches being merged into `main`.
|
||||
While not shown in the graph below, the work on the `main` branch works just like in GitHub flow:
|
||||
with feature branches being merged into `main`.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
|
|
@ -255,7 +333,8 @@ In GitLab, each change to the codebase starts with an issue in the issue trackin
|
|||
If there is no issue yet, create the issue if the change requires more than an hour's work.
|
||||
In many organizations, raising an issue is part of the development process because they are used in sprint planning.
|
||||
The issue title should describe the desired state of the system.
|
||||
For example, the issue title "As an administrator, I want to remove users without receiving an error" is better than "Administrators can't remove users."
|
||||
For example, the issue title `As an administrator, I want to remove users without receiving an error`
|
||||
is better than "Administrators can't remove users."
|
||||
|
||||
When you are ready to code, create a branch for the issue from the `main` branch.
|
||||
This branch is the place for any work related to this change.
|
||||
|
|
@ -267,7 +346,7 @@ When you are done or want to discuss the code, open a merge request.
|
|||
A merge request is an online place to discuss the change and review the code.
|
||||
|
||||
If you open the merge request but do not assign it to anyone, it is a [draft merge request](../user/project/merge_requests/drafts.md).
|
||||
These are used to discuss the proposed implementation but are not ready for inclusion in the `main` branch yet.
|
||||
Drafts are used to discuss the proposed implementation but are not ready for inclusion in the `main` branch yet.
|
||||
Start the title of the merge request with `[Draft]`, `Draft:` or `(Draft)` to prevent it from being merged before it's ready.
|
||||
|
||||
When you think the code is ready, assign the merge request to a reviewer.
|
||||
|
|
@ -356,7 +435,11 @@ Sometimes you can reuse recorded resolutions (`rerere`), but merging is better,
|
|||
Atlassian has [a more thorough explanation of the tradeoffs between merging and rebasing](https://www.atlassian.com/blog/git/git-team-workflows-merge-or-rebase) on their blog.
|
||||
|
||||
A good way to prevent creating many merge commits is to not frequently merge `main` into the feature branch.
|
||||
There are three reasons to merge in `main`: utilizing new code, resolving merge conflicts, and updating long-running branches.
|
||||
Three reasons to merge in `main`:
|
||||
|
||||
1. Utilizing new code.
|
||||
1. Resolving merge conflicts.
|
||||
1. Updating long-running branches.
|
||||
|
||||
If you need to use some code that was introduced in `main` after you created the feature branch, you can often solve this by just cherry-picking a commit.
|
||||
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ This workflow has a weaker security model and is not recommended for production
|
|||
GitLab supports the following Kubernetes versions. You can upgrade your
|
||||
Kubernetes version to a supported version at any time:
|
||||
|
||||
- 1.26 (support ends on March 22, 2024 or when 1.29 becomes supported)
|
||||
- 1.25 (support ends on October 22, 2023 or when 1.28 becomes supported)
|
||||
- 1.24 (support ends on July 22, 2023 or when 1.27 becomes supported)
|
||||
- 1.23 (support ends on February 22, 2023 or when 1.26 becomes supported)
|
||||
|
||||
GitLab aims to support a new minor Kubernetes version three months after its initial release. GitLab supports at least three production-ready Kubernetes minor
|
||||
versions at any given time.
|
||||
|
|
|
|||
|
|
@ -138,6 +138,23 @@ By default, the Helm installation command generated by GitLab:
|
|||
|
||||
To see the full list of customizations available, see the Helm chart's [default values file](https://gitlab.com/gitlab-org/charts/gitlab-agent/-/blob/main/values.yaml).
|
||||
|
||||
##### Use the agent when KAS is behind a self-signed certificate
|
||||
|
||||
When [KAS]((../../../../administration/clusters/kas.md) is behind a self-signed certificate,
|
||||
you can set the value of `config.caCert` to the certificate. For example:
|
||||
|
||||
```shell
|
||||
helm update --install gitlab-agent gitlab/gitlab-agent \
|
||||
--set-file config.caCert=my-custom-ca.pem
|
||||
```
|
||||
|
||||
In this example, `my-custom-ca.pem` is the path to a local file that contains
|
||||
the CA certificate used by KAS. The certificate is automatically stored in a
|
||||
config map and mounted in the `agentk` pod.
|
||||
|
||||
If KAS is installed with the GitLab chart, and the chart is configured to provide
|
||||
an [auto-generated self-signed wildcard certificate](https://docs.gitlab.com/charts/installation/tls.html#option-4-use-auto-generated-self-signed-wildcard-certificate), you can extract the CA certificate from the `RELEASE-wildcard-tls-ca` secret.
|
||||
|
||||
##### Use the agent behind an HTTP proxy
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351867) in GitLab 15.0, the GitLab agent Helm chart supports setting environment variables.
|
||||
|
|
|
|||
|
|
@ -1758,6 +1758,9 @@ msgstr ""
|
|||
msgid "A new personal access token has been created"
|
||||
msgstr ""
|
||||
|
||||
msgid "A new personal access token, named %{code_start}%{token_name}%{code_end}, has been created."
|
||||
msgstr ""
|
||||
|
||||
msgid "A new personal access token, named %{token_name}, has been created."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@
|
|||
"remark-parse": "^10.0.1",
|
||||
"remark-rehype": "^10.1.0",
|
||||
"scrollparent": "^2.0.1",
|
||||
"semver": "^7.3.4",
|
||||
"sentrybrowser5": "npm:@sentry/browser@5.30.0",
|
||||
"sentrybrowser7": "npm:@sentry/browser@^7.21.1",
|
||||
"sortablejs": "^1.10.2",
|
||||
|
|
@ -234,8 +235,8 @@
|
|||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-no-jquery": "2.7.0",
|
||||
"eslint-plugin-no-unsanitized": "^4.0.2",
|
||||
"gettext-extractor": "^3.5.3",
|
||||
"gettext-extractor-vue": "^5.0.0",
|
||||
"gettext-extractor": "^3.7.0",
|
||||
"gettext-extractor-vue": "^5.1.0",
|
||||
"glob": "^7.1.6",
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"istanbul-lib-report": "^3.0.0",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ extractor.addMessageTransformFunction(ensureSingleLine);
|
|||
|
||||
const jsParser = extractor.createJsParser([
|
||||
// Place all the possible expressions to extract here:
|
||||
JsExtractors.callExpression('__', {
|
||||
JsExtractors.callExpression(['__', 's__'], {
|
||||
arguments: {
|
||||
text: 0,
|
||||
},
|
||||
|
|
@ -30,15 +30,13 @@ const jsParser = extractor.createJsParser([
|
|||
textPlural: 1,
|
||||
},
|
||||
}),
|
||||
JsExtractors.callExpression('s__', {
|
||||
arguments: {
|
||||
text: 0,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const vueParser = decorateJSParserWithVueSupport(jsParser, {
|
||||
vue2TemplateCompiler,
|
||||
// All of our expressions contain `__`.
|
||||
// So we can safely ignore parsing files _not_ containing it.
|
||||
guard: '__',
|
||||
});
|
||||
|
||||
function printJson() {
|
||||
|
|
|
|||
|
|
@ -5,16 +5,13 @@ import { stringify } from 'yaml';
|
|||
import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
|
||||
import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
|
||||
import ImageItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue';
|
||||
import getAllRunners from '~/ci/runner/graphql/list/all_runners.query.graphql';
|
||||
import getRunnerTags from '~/ci/pipeline_editor/graphql/queries/runner_tags.query.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import {
|
||||
mockAllRunnersQueryResponse,
|
||||
mockLintResponse,
|
||||
mockCiYml,
|
||||
} from 'jest/ci/pipeline_editor/mock_data';
|
||||
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
|
||||
import { mockRunnersTagsQueryResponse, mockLintResponse, mockCiYml } from '../../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
|
@ -36,7 +33,7 @@ describe('Job assistant drawer', () => {
|
|||
|
||||
const createComponent = () => {
|
||||
mockApollo = createMockApollo([
|
||||
[getAllRunners, jest.fn().mockResolvedValue(mockAllRunnersQueryResponse)],
|
||||
[getRunnerTags, jest.fn().mockResolvedValue(mockRunnersTagsQueryResponse)],
|
||||
]);
|
||||
|
||||
wrapper = mountExtended(JobAssistantDrawer, {
|
||||
|
|
@ -58,6 +55,15 @@ describe('Job assistant drawer', () => {
|
|||
expect(findJobSetupItem().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('job setup item should have tag options', () => {
|
||||
expect(findJobSetupItem().props('tagOptions')).toEqual([
|
||||
{ id: 'tag1', name: 'tag1' },
|
||||
{ id: 'tag2', name: 'tag2' },
|
||||
{ id: 'tag3', name: 'tag3' },
|
||||
{ id: 'tag4', name: 'tag4' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should contain image accordion', () => {
|
||||
expect(findImageItem().exists()).toBe(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -583,86 +583,31 @@ export const mockCommitCreateResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const mockAllRunnersQueryResponse = {
|
||||
export const mockRunnersTagsQueryResponse = {
|
||||
data: {
|
||||
runners: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Runner/1',
|
||||
description: 'test',
|
||||
runnerType: 'PROJECT_TYPE',
|
||||
shortSha: 'DdTYMQGS',
|
||||
version: '15.6.1',
|
||||
ipAddress: '127.0.0.1',
|
||||
active: true,
|
||||
locked: true,
|
||||
jobCount: 0,
|
||||
jobExecutionStatus: 'IDLE',
|
||||
tagList: ['tag1', 'tag2', 'tag3'],
|
||||
createdAt: '2022-11-29T09:37:43Z',
|
||||
contactedAt: null,
|
||||
status: 'NEVER_CONTACTED',
|
||||
userPermissions: {
|
||||
updateRunner: true,
|
||||
deleteRunner: true,
|
||||
__typename: 'RunnerPermissions',
|
||||
},
|
||||
groups: null,
|
||||
ownerProject: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
name: '123',
|
||||
nameWithNamespace: 'Administrator / 123',
|
||||
webUrl: 'http://127.0.0.1:3000/root/test',
|
||||
__typename: 'Project',
|
||||
},
|
||||
tagList: ['tag1', 'tag2'],
|
||||
__typename: 'CiRunner',
|
||||
upgradeStatus: 'NOT_AVAILABLE',
|
||||
adminUrl: 'http://127.0.0.1:3000/admin/runners/1',
|
||||
editAdminUrl: 'http://127.0.0.1:3000/admin/runners/1/edit',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Runner/2',
|
||||
description: 'test',
|
||||
runnerType: 'PROJECT_TYPE',
|
||||
shortSha: 'DdTYMQGA',
|
||||
version: '15.6.1',
|
||||
ipAddress: '127.0.0.1',
|
||||
active: true,
|
||||
locked: true,
|
||||
jobCount: 0,
|
||||
jobExecutionStatus: 'IDLE',
|
||||
tagList: ['tag3', 'tag4'],
|
||||
createdAt: '2022-11-29T09:37:43Z',
|
||||
contactedAt: null,
|
||||
status: 'NEVER_CONTACTED',
|
||||
userPermissions: {
|
||||
updateRunner: true,
|
||||
deleteRunner: true,
|
||||
__typename: 'RunnerPermissions',
|
||||
},
|
||||
groups: null,
|
||||
ownerProject: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
name: '123',
|
||||
nameWithNamespace: 'Administrator / 123',
|
||||
webUrl: 'http://127.0.0.1:3000/root/test',
|
||||
__typename: 'Project',
|
||||
},
|
||||
tagList: ['tag2', 'tag3'],
|
||||
__typename: 'CiRunner',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Runner/3',
|
||||
tagList: ['tag2', 'tag4'],
|
||||
__typename: 'CiRunner',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Runner/4',
|
||||
tagList: [],
|
||||
__typename: 'CiRunner',
|
||||
upgradeStatus: 'NOT_AVAILABLE',
|
||||
adminUrl: 'http://127.0.0.1:3000/admin/runners/2',
|
||||
editAdminUrl: 'http://127.0.0.1:3000/admin/runners/2/edit',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor:
|
||||
'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ',
|
||||
endCursor:
|
||||
'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ',
|
||||
__typename: 'PageInfo',
|
||||
},
|
||||
__typename: 'CiRunnerConnection',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ const defaultConfigHelpUrl =
|
|||
|
||||
const provideData = {
|
||||
gitlabVersion: '14.8',
|
||||
kasVersion: '14.8',
|
||||
kasVersion: '14.8.0',
|
||||
};
|
||||
const propsData = {
|
||||
const defaultProps = {
|
||||
agents: clusterAgents,
|
||||
};
|
||||
|
||||
|
|
@ -26,9 +26,6 @@ const DeleteAgentButtonStub = stubComponent(DeleteAgentButton, {
|
|||
const outdatedTitle = I18N_AGENT_TABLE.versionOutdatedTitle;
|
||||
const mismatchTitle = I18N_AGENT_TABLE.versionMismatchTitle;
|
||||
const mismatchOutdatedTitle = I18N_AGENT_TABLE.versionMismatchOutdatedTitle;
|
||||
const outdatedText = sprintf(I18N_AGENT_TABLE.versionOutdatedText, {
|
||||
version: provideData.kasVersion,
|
||||
});
|
||||
const mismatchText = I18N_AGENT_TABLE.versionMismatchText;
|
||||
|
||||
describe('AgentTable', () => {
|
||||
|
|
@ -43,123 +40,134 @@ describe('AgentTable', () => {
|
|||
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
|
||||
const findDeleteAgentButton = () => wrapper.findAllComponents(DeleteAgentButton);
|
||||
|
||||
beforeEach(() => {
|
||||
const createWrapper = ({ provide = provideData, propsData = defaultProps } = {}) => {
|
||||
wrapper = mountExtended(AgentTable, {
|
||||
propsData,
|
||||
provide: provideData,
|
||||
provide,
|
||||
stubs: {
|
||||
DeleteAgentButton: DeleteAgentButtonStub,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
describe('agent table', () => {
|
||||
it.each`
|
||||
agentName | link | lineNumber
|
||||
${'agent-1'} | ${'/agent-1'} | ${0}
|
||||
${'agent-2'} | ${'/agent-2'} | ${1}
|
||||
`('displays agent link for $agentName', ({ agentName, link, lineNumber }) => {
|
||||
expect(findAgentLink(lineNumber).text()).toBe(agentName);
|
||||
expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it.each`
|
||||
agentName | link | lineNumber
|
||||
${'agent-1'} | ${'/agent-1'} | ${0}
|
||||
${'agent-2'} | ${'/agent-2'} | ${1}
|
||||
`('displays agent link for $agentName', ({ agentName, link, lineNumber }) => {
|
||||
expect(findAgentLink(lineNumber).text()).toBe(agentName);
|
||||
expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
|
||||
});
|
||||
|
||||
it.each`
|
||||
status | iconName | lineNumber
|
||||
${'Never connected'} | ${'status-neutral'} | ${0}
|
||||
${'Connected'} | ${'status-success'} | ${1}
|
||||
${'Not connected'} | ${'status-alert'} | ${2}
|
||||
`(
|
||||
'displays agent connection status as "$status" at line $lineNumber',
|
||||
({ status, iconName, lineNumber }) => {
|
||||
expect(findStatusText(lineNumber).text()).toBe(status);
|
||||
expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
lastContact | lineNumber
|
||||
${'Never'} | ${0}
|
||||
${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1}
|
||||
${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2}
|
||||
`(
|
||||
'displays agent last contact time as "$lastContact" at line $lineNumber',
|
||||
({ lastContact, lineNumber }) => {
|
||||
expect(findLastContactText(lineNumber).text()).toBe(lastContact);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
agentConfig | link | lineNumber
|
||||
${'.gitlab/agents/agent-1'} | ${'/agent/full/path'} | ${0}
|
||||
${'Default configuration'} | ${defaultConfigHelpUrl} | ${1}
|
||||
`(
|
||||
'displays config file path as "$agentPath" at line $lineNumber',
|
||||
({ agentConfig, link, lineNumber }) => {
|
||||
const findLink = findConfiguration(lineNumber).findComponent(GlLink);
|
||||
|
||||
expect(findLink.attributes('href')).toBe(link);
|
||||
expect(findConfiguration(lineNumber).text()).toBe(agentConfig);
|
||||
},
|
||||
);
|
||||
|
||||
it('displays actions menu for each agent', () => {
|
||||
expect(findDeleteAgentButton()).toHaveLength(clusterAgents.length);
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
status | iconName | lineNumber
|
||||
${'Never connected'} | ${'status-neutral'} | ${0}
|
||||
${'Connected'} | ${'status-success'} | ${1}
|
||||
${'Not connected'} | ${'status-alert'} | ${2}
|
||||
`(
|
||||
'displays agent connection status as "$status" at line $lineNumber',
|
||||
({ status, iconName, lineNumber }) => {
|
||||
expect(findStatusText(lineNumber).text()).toBe(status);
|
||||
expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
lastContact | lineNumber
|
||||
${'Never'} | ${0}
|
||||
${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1}
|
||||
${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2}
|
||||
`(
|
||||
'displays agent last contact time as "$lastContact" at line $lineNumber',
|
||||
({ lastContact, lineNumber }) => {
|
||||
expect(findLastContactText(lineNumber).text()).toBe(lastContact);
|
||||
},
|
||||
);
|
||||
|
||||
describe.each`
|
||||
agent | version | podsNumber | versionMismatch | versionOutdated | title | texts | lineNumber
|
||||
${'agent-1'} | ${''} | ${1} | ${false} | ${false} | ${''} | ${''} | ${0}
|
||||
${'agent-2'} | ${'14.8'} | ${2} | ${false} | ${false} | ${''} | ${''} | ${1}
|
||||
${'agent-3'} | ${'14.5'} | ${1} | ${false} | ${true} | ${outdatedTitle} | ${[outdatedText]} | ${2}
|
||||
${'agent-4'} | ${'14.7'} | ${2} | ${true} | ${false} | ${mismatchTitle} | ${[mismatchText]} | ${3}
|
||||
${'agent-5'} | ${'14.3'} | ${2} | ${true} | ${true} | ${mismatchOutdatedTitle} | ${[mismatchText, outdatedText]} | ${4}
|
||||
agentMockIdx | agentVersion | kasVersion | versionMismatch | versionOutdated | title
|
||||
${0} | ${''} | ${'14.8.0'} | ${false} | ${false} | ${''}
|
||||
${1} | ${'14.8.0'} | ${'14.8.0'} | ${false} | ${false} | ${''}
|
||||
${2} | ${'14.6.0'} | ${'14.8.0'} | ${false} | ${true} | ${outdatedTitle}
|
||||
${3} | ${'14.7.0'} | ${'14.8.0'} | ${true} | ${false} | ${mismatchTitle}
|
||||
${4} | ${'14.3.0'} | ${'14.8.0'} | ${true} | ${true} | ${mismatchOutdatedTitle}
|
||||
${5} | ${'14.6.0'} | ${'14.8.0-rc1'} | ${false} | ${false} | ${''}
|
||||
${6} | ${'14.8.0'} | ${'15.0.0'} | ${false} | ${true} | ${outdatedTitle}
|
||||
${7} | ${'14.8.0'} | ${'15.0.0-rc1'} | ${false} | ${true} | ${outdatedTitle}
|
||||
${8} | ${'14.8.0'} | ${'14.8.10'} | ${false} | ${false} | ${''}
|
||||
`(
|
||||
'agent version column at line $lineNumber',
|
||||
({
|
||||
agent,
|
||||
version,
|
||||
podsNumber,
|
||||
versionMismatch,
|
||||
versionOutdated,
|
||||
title,
|
||||
texts,
|
||||
lineNumber,
|
||||
}) => {
|
||||
const findIcon = () => findVersionText(lineNumber).findComponent(GlIcon);
|
||||
const findPopover = () => wrapper.findByTestId(`popover-${agent}`);
|
||||
const versionWarning = versionMismatch || versionOutdated;
|
||||
'when agent version is "$agentVersion", KAS version is "$kasVersion" and version mismatch is "$versionMismatch"',
|
||||
({ agentMockIdx, agentVersion, kasVersion, versionMismatch, versionOutdated, title }) => {
|
||||
const currentAgent = clusterAgents[agentMockIdx];
|
||||
|
||||
it('shows the correct agent version', () => {
|
||||
expect(findVersionText(lineNumber).text()).toBe(version);
|
||||
const findIcon = () => findVersionText(0).findComponent(GlIcon);
|
||||
const findPopover = () => wrapper.findByTestId(`popover-${currentAgent.name}`);
|
||||
|
||||
const versionWarning = versionMismatch || versionOutdated;
|
||||
const outdatedText = sprintf(I18N_AGENT_TABLE.versionOutdatedText, {
|
||||
version: kasVersion,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
provide: { gitlabVersion: '14.8', kasVersion },
|
||||
propsData: { agents: [currentAgent] },
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the correct agent version text', () => {
|
||||
expect(findVersionText(0).text()).toBe(agentVersion);
|
||||
});
|
||||
|
||||
if (versionWarning) {
|
||||
it(`shows a warning icon when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated} and the number of pods is ${podsNumber}`, () => {
|
||||
it('shows a warning icon', () => {
|
||||
expect(findIcon().props('name')).toBe('warning');
|
||||
});
|
||||
|
||||
it(`renders correct title for the popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
|
||||
expect(findPopover().props('title')).toBe(title);
|
||||
});
|
||||
|
||||
it(`renders correct text for the popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
|
||||
texts.forEach((text) => {
|
||||
expect(findPopover().text()).toContain(text);
|
||||
if (versionMismatch) {
|
||||
it(`renders correct text for the popover when agent versions mismatch is ${versionMismatch}`, () => {
|
||||
expect(findPopover().text()).toContain(mismatchText);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (versionOutdated) {
|
||||
it(`renders correct text for the popover when agent versions outdated is ${versionOutdated}`, () => {
|
||||
expect(findPopover().text()).toContain(outdatedText);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
it(`doesn't show a warning icon with a popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated} and the number of pods is ${podsNumber}`, () => {
|
||||
it(`doesn't show a warning icon with a popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
|
||||
expect(findIcon().exists()).toBe(false);
|
||||
expect(findPopover().exists()).toBe(false);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
agentConfig | link | lineNumber
|
||||
${'.gitlab/agents/agent-1'} | ${'/agent/full/path'} | ${0}
|
||||
${'Default configuration'} | ${defaultConfigHelpUrl} | ${1}
|
||||
`(
|
||||
'displays config file path as "$agentPath" at line $lineNumber',
|
||||
({ agentConfig, link, lineNumber }) => {
|
||||
const findLink = findConfiguration(lineNumber).findComponent(GlLink);
|
||||
|
||||
expect(findLink.attributes('href')).toBe(link);
|
||||
expect(findConfiguration(lineNumber).text()).toBe(agentConfig);
|
||||
},
|
||||
);
|
||||
|
||||
it('displays actions menu for each agent', () => {
|
||||
expect(findDeleteAgentButton()).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ export const clusterAgents = [
|
|||
connections: {
|
||||
nodes: [
|
||||
{
|
||||
metadata: { version: 'v14.8' },
|
||||
metadata: { version: 'v14.8.0' },
|
||||
},
|
||||
{
|
||||
metadata: { version: 'v14.8' },
|
||||
metadata: { version: 'v14.8.0' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -61,7 +61,7 @@ export const clusterAgents = [
|
|||
connections: {
|
||||
nodes: [
|
||||
{
|
||||
metadata: { version: 'v14.5' },
|
||||
metadata: { version: 'v14.6.0' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -82,10 +82,10 @@ export const clusterAgents = [
|
|||
connections: {
|
||||
nodes: [
|
||||
{
|
||||
metadata: { version: 'v14.7' },
|
||||
metadata: { version: 'v14.7.0' },
|
||||
},
|
||||
{
|
||||
metadata: { version: 'v14.8' },
|
||||
metadata: { version: 'v14.8.0' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -106,10 +106,94 @@ export const clusterAgents = [
|
|||
connections: {
|
||||
nodes: [
|
||||
{
|
||||
metadata: { version: 'v14.5' },
|
||||
metadata: { version: 'v14.5.0' },
|
||||
},
|
||||
{
|
||||
metadata: { version: 'v14.3' },
|
||||
metadata: { version: 'v14.3.0' },
|
||||
},
|
||||
],
|
||||
},
|
||||
tokens: {
|
||||
nodes: [
|
||||
{
|
||||
lastUsedAt: connectedTimeInactive,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'agent-6',
|
||||
id: 'agent-6-id',
|
||||
webPath: '/agent-6',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
connections: {
|
||||
nodes: [
|
||||
{
|
||||
metadata: { version: 'v14.6.0' },
|
||||
},
|
||||
],
|
||||
},
|
||||
tokens: {
|
||||
nodes: [
|
||||
{
|
||||
lastUsedAt: connectedTimeInactive,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'agent-7',
|
||||
id: 'agent-7-id',
|
||||
webPath: '/agent-7',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
connections: {
|
||||
nodes: [
|
||||
{
|
||||
metadata: { version: 'v14.8.0' },
|
||||
},
|
||||
],
|
||||
},
|
||||
tokens: {
|
||||
nodes: [
|
||||
{
|
||||
lastUsedAt: connectedTimeInactive,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'agent-8',
|
||||
id: 'agent-8-id',
|
||||
webPath: '/agent-8',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
connections: {
|
||||
nodes: [
|
||||
{
|
||||
metadata: { version: 'v14.8.0' },
|
||||
},
|
||||
],
|
||||
},
|
||||
tokens: {
|
||||
nodes: [
|
||||
{
|
||||
lastUsedAt: connectedTimeInactive,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'agent-9',
|
||||
id: 'agent-9-id',
|
||||
webPath: '/agent-9',
|
||||
status: 'inactive',
|
||||
lastContact: connectedTimeInactive.getTime(),
|
||||
connections: {
|
||||
nodes: [
|
||||
{
|
||||
metadata: { version: 'v14.8.0' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import DataframeOutput from '~/notebook/cells/output/dataframe.vue';
|
||||
import JSONTable from '~/behaviors/components/json_table.vue';
|
||||
import { outputWithDataframe } from '../../mock_data';
|
||||
|
||||
describe('~/notebook/cells/output/DataframeOutput', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent(rawCode) {
|
||||
wrapper = shallowMount(DataframeOutput, {
|
||||
propsData: {
|
||||
rawCode,
|
||||
count: 0,
|
||||
index: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const findTable = () => wrapper.findComponent(JSONTable);
|
||||
|
||||
describe('with valid dataframe', () => {
|
||||
beforeEach(() => createComponent(outputWithDataframe.data['text/html'].join('')));
|
||||
|
||||
it('mounts the table', () => {
|
||||
expect(findTable().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('table caption is empty', () => {
|
||||
expect(findTable().props().caption).toEqual('');
|
||||
});
|
||||
|
||||
it('allows filtering', () => {
|
||||
expect(findTable().props().hasFilter).toBe(true);
|
||||
});
|
||||
|
||||
it('sets the correct fields', () => {
|
||||
expect(findTable().props().fields).toEqual([
|
||||
{ key: 'index', label: '', sortable: true },
|
||||
{ key: 'column_1', label: 'column_1', sortable: true },
|
||||
{ key: 'column_2', label: 'column_2', sortable: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it('sets the correct items', () => {
|
||||
expect(findTable().props().items).toEqual([
|
||||
{ index: 0, column_1: 'abc de f', column_2: 'a' },
|
||||
{ index: 1, column_1: 'True', column_2: '0.1' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid dataframe', () => {
|
||||
it('still displays the table', () => {
|
||||
createComponent('dataframe');
|
||||
|
||||
expect(findTable().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { isDataframe, convertHtmlTableToJson } from '~/notebook/cells/output/dataframe_util';
|
||||
import { outputWithDataframeContent } from '../../mock_data';
|
||||
import sanitizeTests from './html_sanitize_fixtures';
|
||||
|
||||
describe('notebook/cells/output/dataframe_utils', () => {
|
||||
describe('isDataframe', () => {
|
||||
describe('when output data has no text/html', () => {
|
||||
it('is is not a dataframe', () => {
|
||||
const input = { data: { 'image/png': ['blah'] } };
|
||||
|
||||
expect(isDataframe(input)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when output data has no text/html, but no mention of dataframe', () => {
|
||||
it('is is not a dataframe', () => {
|
||||
const input = { data: { 'text/html': ['blah'] } };
|
||||
|
||||
expect(isDataframe(input)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when output data has text/html, but no mention of dataframe in the first 20 lines', () => {
|
||||
it('is is not a dataframe', () => {
|
||||
const input = { data: { 'text/html': [...new Array(20).fill('a'), 'dataframe'] } };
|
||||
|
||||
expect(isDataframe(input)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when output data has text/html, and includes "dataframe" within the first 20 lines', () => {
|
||||
it('is is not a dataframe', () => {
|
||||
const input = { data: { 'text/html': ['dataframe'] } };
|
||||
|
||||
expect(isDataframe(input)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertHtmlTableToJson', () => {
|
||||
it('converts table correctly', () => {
|
||||
const input = outputWithDataframeContent;
|
||||
|
||||
const output = {
|
||||
fields: [
|
||||
{ key: 'index', label: '', sortable: true },
|
||||
{ key: 'column_1', label: 'column_1', sortable: true },
|
||||
{ key: 'column_2', label: 'column_2', sortable: true },
|
||||
],
|
||||
items: [
|
||||
{ index: 0, column_1: 'abc de f', column_2: 'a' },
|
||||
{ index: 1, column_1: 'True', column_2: '0.1' },
|
||||
],
|
||||
};
|
||||
|
||||
expect(convertHtmlTableToJson(input)).toEqual(output);
|
||||
});
|
||||
|
||||
describe('sanitizes input before parsing table', () => {
|
||||
it('sanitizes input html', () => {
|
||||
const parser = new DOMParser();
|
||||
const spy = jest.spyOn(parser, 'parseFromString');
|
||||
const input = 'hello<style>p {width:50%;}</style><script>alert(1)</script>';
|
||||
|
||||
convertHtmlTableToJson(input, parser);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('hello', 'text/html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('does not include harmful html', () => {
|
||||
const makeDataframeWithHtml = (html) => {
|
||||
return [
|
||||
'<table border="1" class="dataframe">\n',
|
||||
' <thead>\n',
|
||||
' <tr style="text-align: right;">\n',
|
||||
' <th></th>\n',
|
||||
' <th>column_1</th>\n',
|
||||
' </tr>\n',
|
||||
' </thead>\n',
|
||||
' <tbody>\n',
|
||||
' <tr>\n',
|
||||
' <th>0</th>\n',
|
||||
` <td>${html}</td>\n`,
|
||||
' </tr>\n',
|
||||
' </tbody>\n',
|
||||
'</table>\n',
|
||||
'</div>',
|
||||
];
|
||||
};
|
||||
|
||||
it.each([
|
||||
['table', 0],
|
||||
['style', 1],
|
||||
['iframe', 2],
|
||||
['svg', 3],
|
||||
])('sanitizes output for: %p', (tag, index) => {
|
||||
const inputHtml = makeDataframeWithHtml(sanitizeTests[index][1].input);
|
||||
const convertedHtml = convertHtmlTableToJson(inputHtml).items[0].column_1;
|
||||
|
||||
expect(convertedHtml).not.toContain(tag);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dataframe is invalid', () => {
|
||||
it('returns empty', () => {
|
||||
const input = [' dataframe', ' blah'];
|
||||
|
||||
expect(convertHtmlTableToJson(input)).toEqual({ fields: [], items: [] });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,13 @@ import { mount } from '@vue/test-utils';
|
|||
import json from 'test_fixtures/blob/notebook/basic.json';
|
||||
import Output from '~/notebook/cells/output/index.vue';
|
||||
import MarkdownOutput from '~/notebook/cells/output/markdown.vue';
|
||||
import { relativeRawPath, markdownCellContent } from '../../mock_data';
|
||||
import DataframeOutput from '~/notebook/cells/output/dataframe.vue';
|
||||
import {
|
||||
relativeRawPath,
|
||||
markdownCellContent,
|
||||
outputWithDataframe,
|
||||
outputWithDataframeContent,
|
||||
} from '../../mock_data';
|
||||
|
||||
describe('Output component', () => {
|
||||
let wrapper;
|
||||
|
|
@ -105,6 +111,16 @@ describe('Output component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Dataframe output', () => {
|
||||
it('renders DataframeOutput component', () => {
|
||||
createComponent(outputWithDataframe);
|
||||
|
||||
expect(wrapper.findComponent(DataframeOutput).props('rawCode')).toBe(
|
||||
outputWithDataframeContent.join(''),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default to plain text', () => {
|
||||
beforeEach(() => {
|
||||
const unknownType = json.cells[6];
|
||||
|
|
|
|||
|
|
@ -6,3 +6,47 @@ export const errorOutputContent = [
|
|||
'\u001b[0;32m/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_79203/294318627.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mTo\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m',
|
||||
"\u001b[0;31mNameError\u001b[0m: name 'To' is not defined",
|
||||
];
|
||||
export const outputWithDataframeContent = [
|
||||
'<div>\n',
|
||||
'<style scoped>\n',
|
||||
' .dataframe tbody tr th:only-of-type {\n',
|
||||
' vertical-align: middle;\n',
|
||||
' }\n',
|
||||
'\n',
|
||||
' .dataframe tbody tr th {\n',
|
||||
' vertical-align: top;\n',
|
||||
' }\n',
|
||||
'\n',
|
||||
' .dataframe thead th {\n',
|
||||
' text-align: right;\n',
|
||||
' }\n',
|
||||
'</style>\n',
|
||||
'<table border="1" class="dataframe">\n',
|
||||
' <thead>\n',
|
||||
' <tr style="text-align: right;">\n',
|
||||
' <th></th>\n',
|
||||
' <th>column_1</th>\n',
|
||||
' <th>column_2</th>\n',
|
||||
' </tr>\n',
|
||||
' </thead>\n',
|
||||
' <tbody>\n',
|
||||
' <tr>\n',
|
||||
' <th>0</th>\n',
|
||||
' <td>abc de f</td>\n',
|
||||
' <td>a</td>\n',
|
||||
' </tr>\n',
|
||||
' <tr>\n',
|
||||
' <th>1</th>\n',
|
||||
' <td>True</td>\n',
|
||||
' <td>0.1</td>\n',
|
||||
' </tr>\n',
|
||||
' </tbody>\n',
|
||||
'</table>\n',
|
||||
'</div>',
|
||||
];
|
||||
|
||||
export const outputWithDataframe = {
|
||||
data: {
|
||||
'text/html': outputWithDataframeContent,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
16
yarn.lock
16
yarn.lock
|
|
@ -6293,17 +6293,17 @@ get-value@^2.0.3, get-value@^2.0.6:
|
|||
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
|
||||
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
|
||||
|
||||
gettext-extractor-vue@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/gettext-extractor-vue/-/gettext-extractor-vue-5.0.0.tgz#dc463868d49e14097c4545c8ed4851d8d3edd6dd"
|
||||
integrity sha512-OSuEJlOexxkjYQL2SGf385oWIiy4weWMfUp/ZlOWzMSz0a+HB/Hlv0S4KFTz4A4GuOp3gu15qwXl61GQJ/u+1w==
|
||||
gettext-extractor-vue@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/gettext-extractor-vue/-/gettext-extractor-vue-5.1.0.tgz#faabdc2398751d9ac05bbcaa2e60d679b1634af5"
|
||||
integrity sha512-LFzDyTJGsZO7KPEO7cZKVEMwpLiuC7AEbcZLZt/PEeujE+kRXEFGOQWWSumi1oOtTD2+hdpC6wQG2G8EQybqMg==
|
||||
dependencies:
|
||||
glob "^7.1.6"
|
||||
|
||||
gettext-extractor@^3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.5.3.tgz#6ed46931c154a7485a80fa8b91b835ff7b8d0411"
|
||||
integrity sha512-9EgJ+hmbtAbATdMIvCj4WnrkeDWH6fv1z+IJJ1XCxdcUMGx6JQdVVFTdzJkSyIHh4td53ngoB5EQbavbKJU9Og==
|
||||
gettext-extractor@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.7.0.tgz#6dd1742dfd9cfebf5fb6dd02eea65c75cc28df09"
|
||||
integrity sha512-rLXrJIp2XgXzfIVNaGKj9KnJeI/Gn6FfbPurBXZtkRKwd60JCyaa3l/HlMW6SW/lcO9qfRvqD6C1A5C9yoLhUQ==
|
||||
dependencies:
|
||||
"@types/glob" "5 - 7"
|
||||
"@types/parse5" "^5"
|
||||
|
|
|
|||
Loading…
Reference in New Issue