Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge "End to end tests" branch including react test harness and end-to-end playwright harness #50

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions dist/testables.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { MapCanvas } from './components/MapCanvas.component.js';
import { PubSub } from './components/lib/pubsub.js';
import { signals } from './signals.js';
import { CustomTextArea } from './components/CustomTextArea';
import { MapPanel } from './MapPanel';
export { MapPanel };
export { CustomTextArea };
export { MapCanvas };
export { PubSub };
Expand Down
2 changes: 1 addition & 1 deletion dist/testables.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions e2e/grafana-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ interface Dashboard {
* If one already exists for the given URL the fileId resolves to, the matching datasource is returned unless the
* forceCreate flag is set.
*
* @param {string | null} fileId
* @param {boolean} forceCreate
* @param {string} datasourceName The name for the new datasource
* @param {boolean} forceCreate Optional. Set to true to attempt to create a new datasource with a name duplicated by another datasource.
* @returns
*/
export const createDatasource = async (datasourceName, forceCreate = false): Promise<IDataSource> => {
Expand Down Expand Up @@ -73,8 +73,8 @@ export const createDatasource = async (datasourceName, forceCreate = false): Pro
}

/**
* Deletes a given datasource through its UID dsUid.
* @param {string} dsUid
* Deletes a given datasource through its datasource UID dsUid.
* @param {string} dsUid The datasource UID
*/
export const deleteDatasource = async (dsUid: string) => {
const { basicAuthHeader, protocolHostPort } = await getHostInfo(credentials);
Expand Down
2 changes: 1 addition & 1 deletion e2e/interfaces/TopologyEdge.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export interface ITopologyEdge {
name: string;
meta: {
endpoint_identifiers: {
pops: [string, string]
names: [string, string]
}
},
coordinates: [number, number, number];
Expand Down
132 changes: 116 additions & 16 deletions src/components/CustomTextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';

import { StandardEditorProps, StringFieldConfigSettings } from '@grafana/data';
import { TextArea } from '@grafana/ui';
sanchezelton marked this conversation as resolved.
Show resolved Hide resolved
import { monospacedFontSize } from '../options';

interface CustomTextAreaSettings extends StringFieldConfigSettings {
Expand All @@ -13,6 +12,13 @@ interface Props extends StandardEditorProps<string, CustomTextAreaSettings> {
suffix?: ReactNode;
}

interface ValidationState {
isPristine: boolean;
isTouched: boolean;
isValid: boolean;
errorMessage?: string;
}

function unescape(str) {
return String(str)
.replace(/&amp;/g, '&')
Expand All @@ -21,15 +27,81 @@ function unescape(str) {
.replace(/&quot;/g, '"');
}

function validateMapJsonStr(inStr: string, currentValidationState: ValidationState): ValidationState {
let isValid = true;
let validationFailedMsg: null | string = null;
try {
const parsedObj = JSON.parse(inStr);
if (typeof(parsedObj) !== 'object') {
throw new Error("Bad topology object");
}
if (!Array.isArray(parsedObj.edges) || !Array.isArray(parsedObj.nodes)) {
throw new Error("Missing or bad edges or nodes from topology object");
}
for (const edge of parsedObj.edges) {
const { name, meta, coordinates } = edge;
if (
!name || typeof(name) !== 'string' ||
!meta && typeof(meta) !== 'object' ||
!meta.endpoint_identifiers || typeof(meta.endpoint_identifiers) != 'object' ||
!Array.isArray(meta.endpoint_identifiers?.names) ||
!coordinates || !Array.isArray(coordinates) ||
coordinates.some((coordinate) => {
return !Array.isArray(coordinate)
|| coordinate.length != 2
|| coordinate.some((coord)=>{ return !Number.isFinite(coord)})
})
) {
throw new Error("Bad edge definition");
}
}
for (const node of parsedObj.nodes) {
const { name, meta, coordinate } = node;
if (
!name || typeof(name) !== 'string' ||
(!!meta && typeof(meta) !== 'object') ||
!coordinate || !Array.isArray(coordinate) ||
coordinate.length !== 2 || !Number.isFinite(coordinate[0]) ||
!Number.isFinite(coordinate[1])
) {
throw new Error("Bad node definition");
}
}
} catch (e: any) {
isValid = false;
if (e instanceof Error) {
validationFailedMsg = e.message;
}
}
const newValidationState: any = {
isPristine: isValid ? currentValidationState.isPristine : false,
isTouched: isValid ? currentValidationState.isTouched : false,
isValid: isValid,
errorMessage: null,
};
if (!isValid && validationFailedMsg) {
newValidationState.errorMessage = validationFailedMsg;
}
return newValidationState;
}

export const CustomTextArea: React.FC<Props> = ({ value, onChange, item, suffix }) => {
let textareaRef = useRef<HTMLTextAreaElement>(null);
let [validationState, setValidationState] = useState({
isPristine: true,
isTouched: false,
isValid: false
} as ValidationState);
let [currentEditorValue, setCurrentEditorValue] = useState(value);

const onValueChange = useCallback(
(e: React.SyntheticEvent) => {
let nextValue = value ?? '';
if (e.hasOwnProperty('key')) {
// handling keyboard event
const evt = e as React.KeyboardEvent<HTMLInputElement>;
// if we're not in a <textarea>, the enter key should trigger
// essentially a blur equivalent
if (evt.key === 'Enter' && !item.settings?.useTextarea) {
nextValue = unescape(evt.currentTarget.value.trim());
}
Expand All @@ -41,11 +113,22 @@ export const CustomTextArea: React.FC<Props> = ({ value, onChange, item, suffix
if (nextValue === value) {
return; // no change
}
const newValidationState = validateMapJsonStr(nextValue, {
...validationState,
isPristine: false,
isTouched: true,
});
setValidationState(newValidationState);
setCurrentEditorValue(nextValue);
if (!newValidationState.isValid){
return; // invalid input; don't fire onchange
}
onChange(nextValue === '' ? undefined : nextValue);
},
[value, item.settings?.useTextarea, onChange]
);

// set component initial state
useEffect(() => {
if (!!textareaRef.current) {
// ensure that the js 'value' property stays in sync with the actual DOM value
Expand All @@ -55,23 +138,40 @@ export const CustomTextArea: React.FC<Props> = ({ value, onChange, item, suffix
}
});

const attribs = {};
// when the value changes externally, update the component's initial state
useEffect(()=>{
setCurrentEditorValue(value);
}, [value])

let attribs = {
style: {
width: "100%",
resize: "none",
}
} as any;
if (item.settings?.isMonospaced) {
attribs['style'] = {
fontFamily: "monospace",
fontSize: item.settings?.fontSize || monospacedFontSize
};
attribs.style.fontFamily = "monospace";
attribs.style.fontSize = item.settings?.fontSize || monospacedFontSize;
}

return (
<TextArea
{...attribs}
placeholder={item.settings?.placeholder}
defaultValue={value || ''}
rows={(item.settings?.useTextarea && item.settings.rows) || 5}
onBlur={onValueChange}
onKeyDown={onValueChange}
ref={textareaRef}
/>
<div>
<textarea
{...attribs}
placeholder={item.settings?.placeholder}
defaultValue={currentEditorValue || ''}
rows={(item.settings?.useTextarea && item.settings.rows) || 5}
onBlur={onValueChange}
onChange={onValueChange}
ref={textareaRef}
/>
{
!validationState.isValid ?
<div className='validation-error' style={{ marginTop: "2px", fontSize:"10px", color: "red" }}>
{validationState.errorMessage}
</div>
: null
}
</div>
);
};
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
"sourceMap": true,
"outDir": "$$ts-jest$$"
},
"compileOnSave": true,
"exclude": ["public", "dist", "node_modules", "src/components/lib"]
}
Loading