From cc39d51b7ad16c8b12d2ae789e01b457d49ab5d3 Mon Sep 17 00:00:00 2001 From: amlmtl <> Date: Thu, 18 Apr 2024 13:01:14 +0100 Subject: [PATCH] feat: rework the datasource configuration UI --- pkg/plugin/plugin.go | 7 +- src/components/ConfigEditor.tsx | 198 ++++++++++++++++++++++---------- src/types.ts | 1 - 3 files changed, 139 insertions(+), 67 deletions(-) diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index d91bdf6..8e5138e 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -93,7 +93,6 @@ type SampleDatasource struct { } type datasourceJsonData struct { - IsViya bool `json:"isViya"` UseInternalNetworking bool `json:"useInternalNetworking"` OauthPassThru bool `json:"oauthPassThru"` TlsSkipVerify bool `json:"tlsSkipVerify"` @@ -113,13 +112,13 @@ func (d *SampleDatasource) Dispose() { func (d *SampleDatasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { response := backend.NewQueryDataResponse() - var jsonData datasourceJsonData - err := json.Unmarshal(req.PluginContext.DataSourceInstanceSettings.JSONData, &jsonData) + datasourceJsonData := datasourceJsonData{UseInternalNetworking: true} + err := json.Unmarshal(req.PluginContext.DataSourceInstanceSettings.JSONData, &datasourceJsonData) if err != nil { return nil, err } var authorizationHeaderPtr *string = nil - if jsonData.OauthPassThru && jsonData.IsViya { + if datasourceJsonData.OauthPassThru { authorizationHeader := req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName) authorizationHeaderPtr = &authorizationHeader } diff --git a/src/components/ConfigEditor.tsx b/src/components/ConfigEditor.tsx index 5cb6a08..d17939b 100644 --- a/src/components/ConfigEditor.tsx +++ b/src/components/ConfigEditor.tsx @@ -3,35 +3,79 @@ SPDX-License-Identifier: Apache-2.0 */ -import React, {useEffect, useMemo, useRef} from 'react'; -import {Checkbox, HorizontalGroup, InlineLabel, Select, VerticalGroup} from '@grafana/ui'; +import React, {useMemo, useState} from 'react'; +import {Checkbox, Field, InlineLabel, Input, Select, Stack} from '@grafana/ui'; import {DataSourcePluginOptionsEditorProps, SelectableValue} from '@grafana/data'; import {EspDataSourceOptions} from '../types'; -ConfigEditor.DISCOVERY_DEFAULT_OPTIONS = [ +interface DiscoveryOption { + label: string, + value: string +} + +ConfigEditor.DISCOVERY_DEFAULT_OPTIONS_NO_TLS = [ {label: 'SAS Event Stream Manager', value: 'http://sas-event-stream-manager-app/SASEventStreamManager'}, {label: 'SAS Event Stream Processing Studio', value: 'http://sas-event-stream-processing-studio-app/SASEventStreamProcessingStudio'}, ]; -ConfigEditor.DISCOVERY_DEFAULT_VIYA_OPTIONS = [ +ConfigEditor.DISCOVERY_DEFAULT_OPTIONS_TLS = [ {label: 'SAS Event Stream Manager', value: 'https://sas-event-stream-manager-app/SASEventStreamManager'}, {label: 'SAS Event Stream Processing Studio', value: 'https://sas-event-stream-processing-studio-app/SASEventStreamProcessingStudio'}, ]; -export function ConfigEditor({options, onOptionsChange}: DataSourcePluginOptionsEditorProps) { - const {jsonData} = options; +enum DISCOVERY_TYPE_OPTION_VALUES {DEFAULT, URL} +ConfigEditor.DISCOVERY_TYPE_OPTIONS = [ + {label: "Internal", value: DISCOVERY_TYPE_OPTION_VALUES.DEFAULT}, + {label: "URL", value: DISCOVERY_TYPE_OPTION_VALUES.URL} +]; - const getDiscoveryOptions = (isViya: boolean) => isViya ? ConfigEditor.DISCOVERY_DEFAULT_VIYA_OPTIONS : ConfigEditor.DISCOVERY_DEFAULT_OPTIONS; +ConfigEditor.stringToUrl = (urlString: string) => { + let url; + try { + url = new URL(urlString); + } catch (e: unknown) { + url = null; + } - const deriveSelectedOptionFromUrl = (discoveryServiceUrl: string | null, discoveryOptions: Array>) => { - if (!discoveryServiceUrl) { - return null; - } + return url; +} + +ConfigEditor.getDiscoveryOptions = (tls: boolean) => tls ? ConfigEditor.DISCOVERY_DEFAULT_OPTIONS_TLS : ConfigEditor.DISCOVERY_DEFAULT_OPTIONS_NO_TLS; - const matchingOption = discoveryOptions.find(option => option.value === discoveryServiceUrl); - return matchingOption ?? {value: discoveryServiceUrl, label: discoveryServiceUrl}; +ConfigEditor.deriveSelectedOptionFromUrl = (discoveryServiceUrlString: string) => { + const discoveryServiceUrl = ConfigEditor.stringToUrl(discoveryServiceUrlString); + const isDiscoveryServiceTlsEnabled = discoveryServiceUrl ? ConfigEditor.isDiscoveryServiceUrlTls(discoveryServiceUrl) : true; + const discoveryOptions = ConfigEditor.getDiscoveryOptions(isDiscoveryServiceTlsEnabled); + + return discoveryOptions.find(option => option.value === discoveryServiceUrlString); +} + +ConfigEditor.deriveSelectedDiscoveryTypeFromUrl = (discoveryServiceUrlString: string) => { + if (!discoveryServiceUrlString) { + return DISCOVERY_TYPE_OPTION_VALUES.DEFAULT; + } + + const discoveryServiceUrl = ConfigEditor.stringToUrl(discoveryServiceUrlString); + if (!discoveryServiceUrl) { + return DISCOVERY_TYPE_OPTION_VALUES.URL; } + const isDiscoveryServiceTlsEnabled = ConfigEditor.isDiscoveryServiceUrlTls(discoveryServiceUrl); + const defaultDiscoveryOptions = ConfigEditor.getDiscoveryOptions(isDiscoveryServiceTlsEnabled); + const defaultUrlMatched = defaultDiscoveryOptions.some(option => option.value === discoveryServiceUrlString); + + return defaultUrlMatched ? DISCOVERY_TYPE_OPTION_VALUES.DEFAULT : DISCOVERY_TYPE_OPTION_VALUES.URL; +} + +ConfigEditor.isDiscoveryServiceUrlTls = (discoveryServiceUrl: URL) => { + return discoveryServiceUrl.protocol === "https:"; +} + +export function ConfigEditor({options, onOptionsChange}: Readonly>) { + const {jsonData} = options; + + const [selectedDiscoveryType, setSelectedDiscoveryType] = useState(() => ConfigEditor.deriveSelectedDiscoveryTypeFromUrl(options.url)); + const changePropOptions = (change: Object) => { const newOptions = {...options, ...change}; onOptionsChange(newOptions); @@ -42,69 +86,99 @@ export function ConfigEditor({options, onOptionsChange}: DataSourcePluginOptions changePropOptions({jsonData: newJsonData}); } - const handleDiscoveryServiceProviderChange = (selectedOption: SelectableValue) => { - changePropOptions({url: selectedOption.value}); + const handleDiscoveryServiceUrlChange = (newUrl: string) => { + changePropOptions({url: newUrl}); } const handleTlsSkipVerifyCheckboxChange = (checked: boolean) => { changePropOptionsJsonData({tlsSkipVerify: checked}); } - const handleInternalNetworkingCheckboxChange = (checked: boolean) => { - changePropOptionsJsonData({useInternalNetworking: checked}); + const handleDiscoveryServiceTypeChange = (selectable: SelectableValue) => { + setSelectedDiscoveryType(selectable?.value ?? DISCOVERY_TYPE_OPTION_VALUES.DEFAULT); } - const handleViyaCheckboxChange = (checked: boolean) => { - const isViya = checked; - // Grafana will ignore attempts to reset datasource URLs and will revert to the previously saved value upon a future save, rather than persist a falsy URL. - // To prevent this unwanted behaviour, a default URL is chosen to override the existing URL if possible. - const defaultUrl = getDiscoveryOptions(isViya)?.at(0)?.value; - changePropOptions({ - url: defaultUrl ?? "", - jsonData: {...jsonData, isViya: isViya} - }); + const handleOauthPassthroughCheckboxChange = (checked: boolean) => { + changePropOptionsJsonData({oauthPassThru: checked}); } - const mountEffectRefIsViya = useRef(jsonData.isViya); - const mountEffectRefChangePropOptionsJsonData = useRef(changePropOptionsJsonData); - useEffect(() => { - (async () => { - const isViya = mountEffectRefIsViya.current; - const changePropOptionsJsonData = mountEffectRefChangePropOptionsJsonData.current; - - let jsonDataChanges = new Map(); - jsonDataChanges.set("oauthPassThru", true); - - if (isViya == null) { - let isViya: boolean = await fetch(`${window.location.origin}/SASLogon/`).then((response) => response.ok, () => false); - jsonDataChanges.set("isViya", isViya); - } + const handleTlsCheckboxChange = (checked: boolean) => { + const discoveryServiceUrl = ConfigEditor.stringToUrl(options.url); + if (!discoveryServiceUrl) { + return; + } - const jsonDataDelta = Object.fromEntries(jsonDataChanges); - changePropOptionsJsonData(jsonDataDelta); - })() - }, []); + const isUrlHttps = ConfigEditor.isDiscoveryServiceUrlTls(discoveryServiceUrl) + if (isUrlHttps !== checked) { + discoveryServiceUrl.protocol = checked ? "https:" : "http:"; + changePropOptions({url: discoveryServiceUrl.toString()}); + } + } - const discoveryOptions = getDiscoveryOptions(jsonData.isViya); - const selectedDiscoveryOption = useMemo(() => deriveSelectedOptionFromUrl(options.url, discoveryOptions), [options.url, discoveryOptions]); + const discoveryServiceUrl = ConfigEditor.stringToUrl(options.url); + const isDiscoveryServiceTlsEnabled = discoveryServiceUrl ? ConfigEditor.isDiscoveryServiceUrlTls(discoveryServiceUrl) : true; + const discoveryOptions = ConfigEditor.getDiscoveryOptions(isDiscoveryServiceTlsEnabled); + const selectedDiscoveryOption = useMemo(() => ConfigEditor.deriveSelectedOptionFromUrl(options.url), [options.url]); return ( - - - Discovery service provider - handleViyaCheckboxChange(e.currentTarget.checked)}/> - + + + + ); } + +function DiscoveryTypeForm(props: Readonly<{ type: DISCOVERY_TYPE_OPTION_VALUES + discoveryUrLOptions: DiscoveryOption[], selectedDiscoveryUrlOption: DiscoveryOption | undefined, + url: string, onUrlChange: Function + oauth: boolean | undefined, onOauthChange: Function, + tls: boolean | undefined, onTlsChange: Function, + }>) { + return (<>{props.type === DISCOVERY_TYPE_OPTION_VALUES.DEFAULT ? + : + } + ); +} + +function DiscoveryFormDefault(props: Readonly<{ oauth: boolean | undefined, onOauthChange: Function, + discoveryUrLOptions: DiscoveryOption[], selectedDiscoveryUrlOption: DiscoveryOption | undefined, onUrlChange: Function, + tls: boolean | undefined, onTlsChange: Function + }>) { + return (<> + props.onUrlChange(e.currentTarget.value)}/> + + + ); +} + +function OauthCheckbox(props: Readonly<{value: boolean | undefined, onChange: Function}>) { + return ( props.onChange(e.currentTarget.checked)}/>); +} diff --git a/src/types.ts b/src/types.ts index 470fa7f..bba3cbc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,6 +98,5 @@ export function isField(espObject: EspObject | undefined): espObject is Field { export interface EspDataSourceOptions extends DataSourceJsonData { oauthPassThru: boolean; tlsSkipVerify: boolean; - isViya: boolean; useInternalNetworking: boolean; }