Sandbox: Add basic e2e tests for datasources inside sandbox (#76226)
* Sandbox: initial dummy datasource plugin for e2e * WIP: tests * Add metrics to plugin.json so it shows up in explore * Fix false positives in frontend-sandbox-datasource.spec.ts * Change typed name to be static * Add code to delete the datasource after tests are complete * Add fail on status code * Update tests for more config * Replace visit with existing page * Delete cleanup code
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>
|
||||||
|
After Width: | Height: | Size: 100 B |
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* This is a dummy plugin to test the frontend sandbox
|
||||||
|
* It is not meant to be used in any other way
|
||||||
|
* This file doesn't require any compilation
|
||||||
|
*/
|
||||||
|
define(['react', '@grafana/data'], function (React, grafanaData) {
|
||||||
|
const { DataSourcePlugin, DataSourceApi, MutableDataFrame, FieldType } = grafanaData;
|
||||||
|
const { useState } = React;
|
||||||
|
|
||||||
|
const QueryEditor = (props) => {
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
|
const handleChange = (event) => {
|
||||||
|
setValue(event.target.value);
|
||||||
|
props.onChange({
|
||||||
|
...props.query,
|
||||||
|
testValue: event.target.value,
|
||||||
|
});
|
||||||
|
props.onRunQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
'div',
|
||||||
|
null,
|
||||||
|
React.createElement('label', { htmlFor: 'inputField' }, 'Dummy input field'),
|
||||||
|
React.createElement('input', {
|
||||||
|
type: 'text',
|
||||||
|
id: 'inputField',
|
||||||
|
'data-testid': 'sandbox-query-editor-query-input',
|
||||||
|
value: value,
|
||||||
|
onChange: handleChange,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfigEditor = (props) => {
|
||||||
|
const { onOptionsChange, options } = props;
|
||||||
|
const value = options.jsonData.input || '';
|
||||||
|
|
||||||
|
const handleChange = (event) => {
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
jsonData: {
|
||||||
|
input: event.target.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
'div',
|
||||||
|
null,
|
||||||
|
React.createElement('label', { htmlFor: 'inputField' }, 'Test Config field'),
|
||||||
|
React.createElement('input', {
|
||||||
|
type: 'text',
|
||||||
|
id: 'inputField',
|
||||||
|
'data-testid': 'sandbox-config-editor-query-input',
|
||||||
|
value: value,
|
||||||
|
onChange: handleChange,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
class BasicDataSource extends DataSourceApi {
|
||||||
|
constructor(instanceSettings) {
|
||||||
|
super(instanceSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a test query it'll generate a logarithmic series starting now-1h
|
||||||
|
async query(options) {
|
||||||
|
const promises = options.targets.map(async (target) => {
|
||||||
|
const query = target;
|
||||||
|
|
||||||
|
const timestamps = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
const startTime = endTime - 3600000; // 1 hour in milliseconds
|
||||||
|
|
||||||
|
// Define the logarithmic base and increment factor
|
||||||
|
const base = 2;
|
||||||
|
const increment = 0.1;
|
||||||
|
|
||||||
|
// Generate the data points
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
// Calculate the timestamp
|
||||||
|
const timestamp = startTime + ((endTime - startTime) / 50) * i;
|
||||||
|
timestamps.push(timestamp);
|
||||||
|
|
||||||
|
// Calculate the value using logarithmic increase
|
||||||
|
const value = Math.pow(base, increment * i);
|
||||||
|
values.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MutableDataFrame({
|
||||||
|
refId: query.refId,
|
||||||
|
fields: [
|
||||||
|
{ name: 'Time', type: FieldType.time, values: timestamps },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: values },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Promise.all(promises).then((data) => ({ data }));
|
||||||
|
}
|
||||||
|
async testDatasource() {
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
message: 'Sandbox Success',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin = new DataSourcePlugin(BasicDataSource).setConfigEditor(ConfigEditor).setQueryEditor(QueryEditor);
|
||||||
|
|
||||||
|
return { plugin };
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json",
|
||||||
|
"type": "datasource",
|
||||||
|
"name": "Sandbox datasource test plugin",
|
||||||
|
"id": "sandbox-test-datasource",
|
||||||
|
"metrics": true,
|
||||||
|
"info": {
|
||||||
|
"keywords": ["explore", "datasource"],
|
||||||
|
"description": "",
|
||||||
|
"author": {
|
||||||
|
"name": "Grafana"
|
||||||
|
},
|
||||||
|
"logos": {
|
||||||
|
"small": "img/logo.svg",
|
||||||
|
"large": "img/logo.svg"
|
||||||
|
},
|
||||||
|
"links": [],
|
||||||
|
"screenshots": [],
|
||||||
|
"version": "1.0.0",
|
||||||
|
"updated": "2023-06-27"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"grafanaDependency": ">=10.0",
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { random } from 'lodash';
|
||||||
|
|
||||||
|
import { e2e } from '../utils';
|
||||||
|
|
||||||
|
const DATASOURCE_ID = 'sandbox-test-datasource';
|
||||||
|
let DATASOURCE_CONNECTION_ID = '';
|
||||||
|
const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance';
|
||||||
|
|
||||||
|
describe('Datasource sandbox', () => {
|
||||||
|
before(() => {
|
||||||
|
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
|
||||||
|
|
||||||
|
e2e.pages.AddDataSource.visit();
|
||||||
|
e2e.pages.AddDataSource.dataSourcePluginsV2('Sandbox datasource test plugin')
|
||||||
|
.scrollIntoView()
|
||||||
|
.should('be.visible') // prevents flakiness
|
||||||
|
.click();
|
||||||
|
e2e.pages.DataSource.name().clear();
|
||||||
|
e2e.pages.DataSource.name().type(DATASOURCE_TYPED_NAME);
|
||||||
|
e2e.pages.DataSource.saveAndTest().click();
|
||||||
|
cy.url().then((url) => {
|
||||||
|
const split = url.split('/');
|
||||||
|
DATASOURCE_CONNECTION_ID = split[split.length - 1];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
beforeEach(() => {
|
||||||
|
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Config Editor', () => {
|
||||||
|
describe('Sandbox disabled', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('Should not render a sandbox wrapper around the datasource config editor', () => {
|
||||||
|
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
|
||||||
|
cy.wait(300); // wait to prevent false positives because cypress checks too fast
|
||||||
|
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Sandbox enabled', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should render a sandbox wrapper around the datasource config editor', () => {
|
||||||
|
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
|
||||||
|
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should store values in jsonData and secureJsonData correctly', () => {
|
||||||
|
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
|
||||||
|
|
||||||
|
const valueToStore = 'test' + random(100);
|
||||||
|
|
||||||
|
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('not.be.disabled');
|
||||||
|
cy.get('[data-testid="sandbox-config-editor-query-input"]').type(valueToStore);
|
||||||
|
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('have.value', valueToStore);
|
||||||
|
|
||||||
|
e2e.pages.DataSource.saveAndTest().click();
|
||||||
|
e2e.pages.DataSource.alert().should('exist').contains('Sandbox Success', {});
|
||||||
|
|
||||||
|
// validate the value was stored
|
||||||
|
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
|
||||||
|
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('not.be.disabled');
|
||||||
|
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('have.value', valueToStore);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Explore Page', () => {
|
||||||
|
describe('Sandbox disabled', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not wrap the query editor in a sandbox wrapper', () => {
|
||||||
|
e2e.pages.Explore.visit();
|
||||||
|
e2e.components.DataSourcePicker.container().should('be.visible').click();
|
||||||
|
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
|
||||||
|
|
||||||
|
cy.wait(300); // wait to prevent false positives because cypress checks too fast
|
||||||
|
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should accept values when typed', () => {
|
||||||
|
e2e.pages.Explore.visit();
|
||||||
|
e2e.components.DataSourcePicker.container().should('be.visible').click();
|
||||||
|
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
|
||||||
|
|
||||||
|
const valueToType = 'test' + random(100);
|
||||||
|
|
||||||
|
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('not.be.disabled');
|
||||||
|
cy.get('[data-testid="sandbox-query-editor-query-input"]').type(valueToType);
|
||||||
|
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('have.value', valueToType);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Sandbox enabled', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should wrap the query editor in a sandbox wrapper', () => {
|
||||||
|
e2e.pages.Explore.visit();
|
||||||
|
e2e.components.DataSourcePicker.container().should('be.visible').click();
|
||||||
|
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
|
||||||
|
|
||||||
|
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should accept values when typed', () => {
|
||||||
|
e2e.pages.Explore.visit();
|
||||||
|
e2e.components.DataSourcePicker.container().should('be.visible').click();
|
||||||
|
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
|
||||||
|
|
||||||
|
const valueToType = 'test' + random(100);
|
||||||
|
|
||||||
|
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('not.be.disabled');
|
||||||
|
cy.get('[data-testid="sandbox-query-editor-query-input"]').type(valueToType);
|
||||||
|
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('have.value', valueToType);
|
||||||
|
|
||||||
|
// typing the query editor should reflect in the url
|
||||||
|
cy.url().should('include', valueToType);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
e2e.flows.revertAllChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
cy.clearCookies();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user