CloudMonitoring: Migrate config editor from angular to react (#33645)

* fix broken config ctrl

* replace angular config with react config editor

* remove not used code

* add extra linebreak

* add noopener to link

* only test jwt props that we actually need
This commit is contained in:
Erik Sundell 2021-05-10 14:51:19 +02:00 committed by GitHub
parent 1c58fd380f
commit 1a59117343
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 204 additions and 247 deletions

View File

@ -0,0 +1,110 @@
import React, { PureComponent } from 'react';
import { Select, FieldSet, InlineField, Alert } from '@grafana/ui';
import { DataSourcePluginOptionsEditorProps, onUpdateDatasourceJsonDataOptionSelect } from '@grafana/data';
import { AuthType, authTypes, CloudMonitoringOptions, CloudMonitoringSecureJsonData } from '../../types';
import { JWTConfig } from './JWTConfig';
export type Props = DataSourcePluginOptionsEditorProps<CloudMonitoringOptions, CloudMonitoringSecureJsonData>;
export class ConfigEditor extends PureComponent<Props> {
render() {
const { options, onOptionsChange } = this.props;
const { secureJsonFields, jsonData } = options;
if (!jsonData.hasOwnProperty('authenticationType')) {
jsonData.authenticationType = AuthType.JWT;
}
return (
<>
<div className="gf-form-group">
<div className="grafana-info-box">
<h4>Google Cloud Monitoring Authentication</h4>
<p>
There are two ways to authenticate the Google Cloud Monitoring plugin - either by uploading a Service
Account key file or by automatically retrieving credentials from the Google metadata server. The latter
option is only available when running Grafana on a GCE virtual machine.
</p>
<h5>Uploading a Service Account Key File</h5>
<p>
There are two ways to authenticate the Google Cloud Monitoring plugin. You can upload a Service Account
key file or automatically retrieve credentials from the Google metadata server. The latter option is only
available when running Grafana on a GCE virtual machine.
</p>
<p>
The <strong>Monitoring Viewer</strong> role provides all the permissions that Grafana needs. The following
API needs to be enabled on GCP for the data source to work:{' '}
<a
className="external-link"
target="_blank"
rel="noopener noreferrer"
href="https://console.cloud.google.com/apis/library/monitoring.googleapis.com"
>
Monitoring API
</a>
</p>
<h5>GCE Default Service Account</h5>
<p>
If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to
automatically retrieve the default project id and authentication token from the metadata server. In order
for this to work, you need to make sure that you have a service account that is setup as the default
account for the virtual machine and that the service account has been given read access to the Google
Cloud Monitoring Monitoring API.
</p>
<p>
Detailed instructions on how to create a Service Account can be found{' '}
<a
className="external-link"
target="_blank"
rel="noopener noreferrer"
href="https://grafana.com/docs/grafana/latest/datasources/google-cloud-monitoring/"
>
in the documentation.
</a>
</p>
</div>
</div>
<FieldSet>
<InlineField label="Authentication type" labelWidth={20}>
<Select
width={40}
value={authTypes.find((x) => x.value === jsonData.authenticationType) || authTypes[0]}
options={authTypes}
defaultValue={jsonData.authenticationType}
onChange={onUpdateDatasourceJsonDataOptionSelect(this.props, 'authenticationType')}
/>
</InlineField>
{jsonData.authenticationType === AuthType.JWT && (
<JWTConfig
isConfigured={secureJsonFields && !!secureJsonFields.jwt}
onChange={({ private_key, client_email, project_id, token_uri }) => {
onOptionsChange({
...options,
secureJsonData: {
...options.secureJsonData,
privateKey: private_key,
},
jsonData: {
...options.jsonData,
defaultProject: project_id,
clientEmail: client_email,
tokenUri: token_uri,
},
});
}}
></JWTConfig>
)}
</FieldSet>
{jsonData.authenticationType === AuthType.GCE && (
<Alert title="" severity="info">
Verify GCE default service account by clicking Save & Test
</Alert>
)}
</>
);
}
}

View File

@ -0,0 +1,82 @@
import React, { FormEvent, useState } from 'react';
import { startCase } from 'lodash';
import { Button, FileUpload, InlineField, Input, useStyles, Alert } from '@grafana/ui';
import { css, cx } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data';
const configKeys = ['project_id', 'private_key', 'client_email', 'token_uri'];
export interface JWT {
token_uri: string;
client_email: string;
private_key: string;
project_id: string;
}
export interface Props {
onChange: (jwt: JWT) => void;
isConfigured: boolean;
}
const validateJson = (json: JWT): json is JWT => {
return !!json.token_uri && !!json.client_email && !!json.project_id && !!json.project_id;
};
export function JWTConfig({ onChange, isConfigured }: Props) {
const styles = useStyles(getStyles);
const [enableUpload, setEnableUpload] = useState<boolean>(!isConfigured);
const [error, setError] = useState<string | null>(null);
return enableUpload ? (
<>
<FileUpload
className={styles}
accept="application/json"
onFileUpload={(event: FormEvent<HTMLInputElement>) => {
if (event?.currentTarget?.files?.length === 1) {
setError(null);
const reader = new FileReader();
const readerOnLoad = () => {
return (e: any) => {
const json = JSON.parse(e.target.result);
if (validateJson(json)) {
onChange(json);
setEnableUpload(false);
} else {
setError('Invalid JWT file');
}
};
};
reader.onload = readerOnLoad();
reader.readAsText(event.currentTarget.files[0]);
} else {
setError('You can only upload one file');
}
}}
>
Upload service account key file
</FileUpload>
{error && <p className={cx(styles, 'alert')}>{error}</p>}
</>
) : (
<>
{configKeys.map((key, i) => (
<InlineField label={startCase(key)} key={i} labelWidth={20} disabled>
<Input width={40} placeholder="configured" />
</InlineField>
))}
<Button variant="secondary" onClick={() => setEnableUpload(true)} className={styles}>
Upload another JWT file
</Button>
<Alert title="" className={styles} severity="info">
Do not forget to save your changes after uploading a file
</Alert>
</>
);
}
export const getStyles = (theme: GrafanaTheme) => css`
margin: ${theme.spacing.md} 0 0;
`;

View File

@ -1,101 +0,0 @@
import DatasourceSrv from 'app/features/plugins/datasource_srv';
import { AuthType, authTypes } from './types';
export interface JWT {
private_key: string;
token_uri: string;
client_email: string;
project_id: string;
}
export class CloudMonitoringConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/cloud-monitoring/partials/config.html';
// Set through angular bindings
declare current: any;
declare meta: any;
datasourceSrv: DatasourceSrv;
jsonText: string;
validationErrors: string[] = [];
inputDataValid: boolean;
authenticationTypes: Array<{ key: AuthType; value: string }>;
defaultAuthenticationType: string;
name: string;
/** @ngInject */
constructor(datasourceSrv: DatasourceSrv) {
this.defaultAuthenticationType = AuthType.JWT;
this.datasourceSrv = datasourceSrv;
this.name = this.meta.name;
this.current.jsonData = this.current.jsonData || {};
this.current.jsonData.authenticationType = this.current.jsonData.authenticationType
? this.current.jsonData.authenticationType
: this.defaultAuthenticationType;
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonFields = this.current.secureJsonFields || {};
this.authenticationTypes = authTypes;
}
save(jwt: JWT) {
this.current.secureJsonData.privateKey = jwt.private_key;
this.current.jsonData.tokenUri = jwt.token_uri;
this.current.jsonData.clientEmail = jwt.client_email;
this.current.jsonData.defaultProject = jwt.project_id;
}
validateJwt(jwt: JWT) {
this.resetValidationMessages();
if (!jwt.private_key || jwt.private_key.length === 0) {
this.validationErrors.push('Private key field missing in JWT file.');
}
if (!jwt.token_uri || jwt.token_uri.length === 0) {
this.validationErrors.push('Token URI field missing in JWT file.');
}
if (!jwt.client_email || jwt.client_email.length === 0) {
this.validationErrors.push('Client Email field missing in JWT file.');
}
if (!jwt.project_id || jwt.project_id.length === 0) {
this.validationErrors.push('Project Id field missing in JWT file.');
}
if (this.validationErrors.length === 0) {
this.inputDataValid = true;
return true;
}
return false;
}
onUpload(json: JWT) {
this.jsonText = '';
if (this.validateJwt(json)) {
this.save(json);
}
}
onPasteJwt(e: any) {
try {
const json = JSON.parse(e.originalEvent.clipboardData.getData('text/plain') || this.jsonText);
if (this.validateJwt(json)) {
this.save(json);
}
} catch (error) {
this.resetValidationMessages();
this.validationErrors.push(`Invalid json: ${error.message}`);
}
}
resetValidationMessages() {
this.validationErrors = [];
this.inputDataValid = false;
this.jsonText = '';
this.current.jsonData = Object.assign({}, { authenticationType: this.current.jsonData.authenticationType });
this.current.secureJsonData = {};
this.current.secureJsonFields = {};
}
}

View File

@ -1,13 +1,14 @@
import { DataSourcePlugin } from '@grafana/data';
import CloudMonitoringDatasource from './datasource';
import { QueryEditor } from './components/QueryEditor';
import { CloudMonitoringConfigCtrl } from './config_ctrl';
import { ConfigEditor } from './components/ConfigEditor/ConfigEditor';
import { CloudMonitoringAnnotationsQueryCtrl } from './annotations_query_ctrl';
import { CloudMonitoringVariableQueryEditor } from './components/VariableQueryEditor';
import { CloudMonitoringQuery } from './types';
export const plugin = new DataSourcePlugin<CloudMonitoringDatasource, CloudMonitoringQuery>(CloudMonitoringDatasource)
.setQueryEditor(QueryEditor)
.setConfigCtrl(CloudMonitoringConfigCtrl)
.setConfigEditor(ConfigEditor)
.setAnnotationQueryCtrl(CloudMonitoringAnnotationsQueryCtrl)
.setVariableQueryEditor(CloudMonitoringVariableQueryEditor);

View File

@ -1,141 +0,0 @@
<div class="gf-form-group">
<div class="grafana-info-box">
<h4>Google Cloud Monitoring Authentication</h4>
<p>
There are two ways to authenticate the Google Cloud Monitoring plugin - either by uploading a Service Account key file or by
automatically retrieving credentials from the Google metadata server. The latter option is only available when
running Grafana on a GCE virtual machine.
</p>
<h5>Uploading a Service Account Key File</h5>
<p>
There are two ways to authenticate the Google Cloud Monitoring plugin. You can upload a Service Account key file or automatically retrieve
credentials from the Google metadata server. The latter option is only available when running Grafana on a GCE virtual machine.
</p>
<p>
The <strong>Monitoring Viewer</strong> role provides all the permissions that Grafana needs. The following API
needs to be enabled on GCP for the data source to work:
<a
class="external-link"
target="_blank"
href="https://console.cloud.google.com/apis/library/monitoring.googleapis.com"
>Monitoring API</a
>
</p>
<h5>GCE Default Service Account</h5>
<p>
If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to
automatically retrieve the default project id and authentication token from the metadata server. In order for this
to work, you need to make sure that you have a service account that is setup as the default account for the
virtual machine and that the service account has been given read access to the Google Cloud Monitoring Monitoring API.
</p>
<p>
Detailed instructions on how to create a Service Account can be found
<a class="external-link" target="_blank" href="https://grafana.com/docs/grafana/latest/datasources/google-cloud-monitoring/_index.md"
>in the documentation.</a
>
</p>
</div>
</div>
<div class="gf-form-group">
<div class="gf-form">
<h3>Authentication</h3>
<info-popover mode="header"
>Upload your Service Account key file or paste in the contents of the file. The file contents will be encrypted
and saved in the Grafana database.</info-popover
>
</div>
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-10">Authentication Type</span>
<div class="gf-form-select-wrapper max-width-24">
<select
class="gf-form-input"
ng-change="ctrl.gceError = ''"
ng-model="ctrl.current.jsonData.authenticationType"
ng-options="f.key as f.value for f in ctrl.authenticationTypes"
></select>
</div>
</div>
</div>
<div
ng-if="ctrl.current.jsonData.authenticationType === ctrl.defaultAuthenticationType && !ctrl.current.jsonData.clientEmail && !ctrl.inputDataValid"
>
<div class="gf-form-group" ng-if="!ctrl.inputDataValid">
<div class="gf-form">
<form>
<dash-upload on-upload="ctrl.onUpload(dash)" btn-text="Upload Service Account key file"></dash-upload>
</form>
</div>
</div>
<div class="gf-form-group">
<h5 class="section-heading" ng-if="!ctrl.inputDataValid">Or paste Service Account key JSON</h5>
<div class="gf-form" ng-if="!ctrl.inputDataValid">
<textarea
rows="10"
data-share-panel-url=""
class="gf-form-input"
ng-model="ctrl.jsonText"
ng-paste="ctrl.onPasteJwt($event)"
></textarea>
</div>
<div ng-repeat="valError in ctrl.validationErrors" class="text-error p-l-1">
<icon name="'exclamation-triangle'"></icon>
{{valError}}
</div>
</div>
</div>
</div>
<div
class="gf-form-group"
ng-if="ctrl.current.jsonData.authenticationType === ctrl.defaultAuthenticationType && (ctrl.inputDataValid || ctrl.current.jsonData.clientEmail)"
>
<h6>Uploaded Key Details</h6>
<div class="gf-form">
<span class="gf-form-label width-10">Project</span>
<input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.defaultProject" />
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Client Email</span>
<input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.clientEmail" />
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Token URI</span>
<input class="gf-form-input width-40" disabled type="text" ng-model="ctrl.current.jsonData.tokenUri" />
</div>
<div class="gf-form" ng-if="ctrl.current.secureJsonFields.privateKey">
<span class="gf-form-label width-10">Private Key</span>
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" />
</div>
<div class="gf-form width-18" style="margin-top: 24px;">
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.resetValidationMessages()"
>Reset Service Account Key
</a>
<info-popover mode="right-normal">
Reset to clear the uploaded key and upload a new file.
</info-popover>
</div>
</div>
<p
class="gf-form-label"
ng-hide="ctrl.current.secureJsonFields.privateKey || ctrl.current.jsonData.authenticationType !== ctrl.defaultAuthenticationType"
>
<icon name="'save'"></icon> Do not forget to save your changes after uploading a file.
</p>
<div class="gf-form" ng-if="ctrl.gceError">
<pre class="gf-form-pre alert alert-error">{{ctrl.gceError}}</pre>
</div>
<p class="gf-form-label" ng-show="ctrl.current.jsonData.authenticationType !== ctrl.defaultAuthenticationType">
<icon name="'save'"></icon> Verify GCE default service account by clicking Save & Test
</p>

View File

@ -5,9 +5,9 @@ export enum AuthType {
GCE = 'gce',
}
export const authTypes = [
{ value: 'Google JWT File', key: AuthType.JWT },
{ value: 'GCE Default Service Account', key: AuthType.GCE },
export const authTypes: Array<SelectableValue<string>> = [
{ label: 'Google JWT File', value: AuthType.JWT },
{ label: 'GCE Default Service Account', value: AuthType.GCE },
];
export enum MetricFindQueryTypes {
@ -111,6 +111,12 @@ export interface CloudMonitoringOptions extends DataSourceJsonData {
defaultProject?: string;
gceDefaultProject?: string;
authenticationType?: string;
clientEmail?: string;
tokenUri?: string;
}
export interface CloudMonitoringSecureJsonData {
privateKey?: string;
}
export interface AnnotationTarget {