Skip to content

Commit

Permalink
feat: Allow datasource name pattern to be configured from app setting…
Browse files Browse the repository at this point in the history
…s page

Include datasource selection on every page where it is relevant
  • Loading branch information
tiithansen committed May 25, 2024
1 parent bca4f7f commit aeb7453
Show file tree
Hide file tree
Showing 19 changed files with 227 additions and 205 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ This plugin relies on presence of default kube-state-metrics and node-exporter m

### Current limitations

* Datasource is not configurable yet and it is hardcoded with a name `prometheus`.
* It expects the presence of `cluster` label on all the metrics.

### Screenshots
Expand Down Expand Up @@ -46,4 +45,5 @@ If you have any feature requests, improvements or suggestions, please create an
2. Run `npm install` to install the dependencies.
3. Run `npm run dev` to start development server.
4. Run `docker-compose up` to start Grafana with the plugin.
5. Open Grafana at `http://localhost:3000/a/k8s-app/workloads`.
5. Navigate to `http://localhost:3000/plugins/k8s-app` and configure the plugins datasource.
6. Open Grafana at `http://localhost:3000/a/k8s-app/workloads`.
91 changes: 35 additions & 56 deletions src/components/AppConfig/AppConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import React, { useState, ChangeEvent } from 'react';
import { Button, Field, Input, useStyles2, FieldSet, SecretInput } from '@grafana/ui';
import { Button, Field, Input, useStyles2, FieldSet } from '@grafana/ui';
import { PluginConfigPageProps, AppPluginMeta, PluginMeta, GrafanaTheme2 } from '@grafana/data';
import { getBackendSrv, locationService } from '@grafana/runtime';
import { css } from '@emotion/css';
import { testIds } from '../testIds';
import { lastValueFrom } from 'rxjs';

export type JsonData = {
apiUrl?: string;
isApiKeySet?: boolean;
datasource?: string;
isclusterLabelEnabled?: boolean;
clusterLabel?: string;
};

type State = {
// The URL to reach our custom API.
apiUrl: string;
// Tells us if the API key secret is set.
// Set to `true` ONLY if it has already been set and haven't been changed.
// (We unfortunately need an auxiliray variable for this, as `secureJsonData` is never exposed to the browser after it is set)
isApiKeySet: boolean;
// An secret key for our custom API.
apiKey: string;
// The regex pattern to match datasource
datasource: string;

// Enable cluster label
isClusterLabelEnabled: boolean;

// Cluster label name
clusterLabel: string;
};

interface Props extends PluginConfigPageProps<AppPluginMeta<JsonData>> {}
Expand All @@ -28,29 +29,29 @@ export const AppConfig = ({ plugin }: Props) => {
const s = useStyles2(getStyles);
const { enabled, pinned, jsonData } = plugin.meta;
const [state, setState] = useState<State>({
apiUrl: jsonData?.apiUrl || '',
apiKey: '',
isApiKeySet: Boolean(jsonData?.isApiKeySet),
datasource: jsonData?.datasource || 'prometheus',
isClusterLabelEnabled: true,
clusterLabel: jsonData?.clusterLabel || 'cluster',
});

const onResetApiKey = () =>
const onChangeDatasource = (event: ChangeEvent<HTMLInputElement>) => {
setState({
...state,
apiKey: '',
isApiKeySet: false,
datasource: event.target.value.trim(),
});
};

const onChangeApiKey = (event: ChangeEvent<HTMLInputElement>) => {
const onChangeClusterLabel = (event: ChangeEvent<HTMLInputElement>) => {
setState({
...state,
apiKey: event.target.value.trim(),
});
};
clusterLabel: event.target.value.trim(),
})
}

const onChangeApiUrl = (event: ChangeEvent<HTMLInputElement>) => {
const onChangeClusterLabelEnabled = (event: ChangeEvent<HTMLInputElement>) => {
setState({
...state,
apiUrl: event.target.value.trim(),
isClusterLabelEnabled: Boolean(event.target.value.trim()),
});
};

Expand Down Expand Up @@ -99,31 +100,17 @@ export const AppConfig = ({ plugin }: Props) => {
</FieldSet>

{/* CUSTOM SETTINGS */}
<FieldSet label="API Settings" className={s.marginTopXl}>
{/* API Key */}
<Field label="API Key" description="A secret key for authenticating to our custom API">
<SecretInput
width={60}
data-testid={testIds.appConfig.apiKey}
id="api-key"
value={state?.apiKey}
isConfigured={state.isApiKeySet}
placeholder={'Your secret API key'}
onChange={onChangeApiKey}
onReset={onResetApiKey}
/>
</Field>

<FieldSet label="Settings" className={s.marginTopXl}>
{/* API Url */}
<Field label="API Url" description="" className={s.marginTop}>
<Field label="Datasource" description="" className={s.marginTop}>
<Input
width={60}
id="api-url"
data-testid={testIds.appConfig.apiUrl}
label={`API Url`}
value={state?.apiUrl}
placeholder={`E.g.: http://mywebsite.com/api/v1`}
onChange={onChangeApiUrl}
id="datasource"
data-testid={testIds.appConfig.datasource}
label={`Datasource pattern`}
value={state?.datasource}
placeholder={`E.g.: Prometheus`}
onChange={onChangeDatasource}
/>
</Field>

Expand All @@ -136,21 +123,13 @@ export const AppConfig = ({ plugin }: Props) => {
enabled,
pinned,
jsonData: {
apiUrl: state.apiUrl,
isApiKeySet: true,
datasource: state.datasource,
},
// This cannot be queried later by the frontend.
// We don't want to override it in case it was set previously and left untouched now.
secureJsonData: state.isApiKeySet
? undefined
: {
apiKey: state.apiKey,
},
})
}
disabled={Boolean(!state.apiUrl || (!state.isApiKeySet && !state.apiKey))}
disabled={Boolean(!state.datasource)}
>
Save API settings
Save
</Button>
</div>
</FieldSet>
Expand Down
5 changes: 5 additions & 0 deletions src/components/Routes/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { Clusters } from '../../pages/Clusters';
import { Workloads } from '../../pages/Workloads';
import { prefixRoute } from '../../utils/utils.routing';
import { ROUTES } from '../../constants';
import { usePluginProps } from 'utils/utils.plugin';

export const Routes = () => {

const props = usePluginProps();
console.log(props);

return (
<Switch>
<Route path={prefixRoute(`${ROUTES.Clusters}`)} component={Clusters} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/testIds.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const testIds = {
appConfig: {
container: 'data-testid ac-container',
apiKey: 'data-testid ac-api-key',
datasource: 'data-testid ac-datasource',
apiUrl: 'data-testid ac-api-url',
submit: 'data-testid ac-submit-form',
},
Expand Down
23 changes: 20 additions & 3 deletions src/pages/Clusters/Clusters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,41 @@ import {
SceneControlsSpacer,
SceneTimePicker,
SceneRefreshPicker,
SceneVariableSet,
DataSourceVariable,
VariableValueSelectors,
} from '@grafana/scenes';
import { ROUTES } from '../../constants';
import React, { useMemo } from 'react';
import { prefixRoute } from 'utils/utils.routing';
import { getOverviewScene } from './tabs/Overview/Overview';
import { getNodesScene } from './tabs/Nodes/Nodes';
import { usePluginProps } from 'utils/utils.plugin';

const timeRange = new SceneTimeRange({
from: 'now-1h',
to: 'now',
});

function getScene() {

function getScene({ datasource }: { datasource: string }) {
return new SceneApp({
pages: [
new SceneAppPage({
title: 'Clusters',
url: prefixRoute(`${ROUTES.Clusters}`),
$timeRange: timeRange,
$variables: new SceneVariableSet({
variables: [
new DataSourceVariable({
name: 'datasource',
label: 'Datasource',
pluginId: 'prometheus',
regex: datasource,
}),
],
}),
controls: [
new VariableValueSelectors({}),
new SceneControlsSpacer(),
new SceneTimePicker({ isOnCanvas: true }),
new SceneRefreshPicker({
Expand All @@ -52,7 +66,10 @@ function getScene() {
}

export const Clusters = () => {
const scene = useMemo(() => getScene(), []);
const props = usePluginProps();
const scene = useMemo(() => getScene({
datasource: props?.meta.jsonData?.datasource || 'prometheus',
}), [props?.meta.jsonData?.datasource]);

return <scene.Component model={scene} />;
};
9 changes: 6 additions & 3 deletions src/pages/Clusters/tabs/Nodes/Nodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ import { getSeriesValue } from 'pages/Workloads/seriesHelpers';
import { CellProps } from 'react-table';
import { NodeMemoryCell } from './NodeMemoryCell';
import { NodeCPUCell } from './NodeCPUCell';
import { resolveVariable } from 'pages/Workloads/variableHelpers';

const clusterVariable = new QueryVariable({
name: 'cluster',
label: 'Cluster',
datasource: {
uid: 'prometheus',
uid: '$datasource',
type: 'prometheus',
},
query: {
Expand All @@ -45,7 +46,7 @@ const searchVariable = new TextBoxVariable({

const nodesQueryRunner = new SceneQueryRunner({
datasource: {
uid: 'prometheus',
uid: '$datasource',
type: 'prometheus',
},
queries: [
Expand Down Expand Up @@ -171,9 +172,11 @@ class TableViz extends SceneObjectBase<TableVizState> {
return;
}

const datasource = resolveVariable(sceneVariables, 'datasource');

asyncQueryRunner({
datasource: {
uid: 'prometheus',
uid: datasource?.toString(),
type: 'prometheus',
},

Expand Down
48 changes: 35 additions & 13 deletions src/pages/Workloads/Workloads.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
SceneControlsSpacer,
SceneTimePicker,
SceneRefreshPicker,
DataSourceVariable,
QueryVariable,
} from '@grafana/scenes';
import { ROUTES } from '../../constants';
import React, { useMemo } from 'react';
Expand All @@ -19,20 +21,37 @@ import { getPodPage } from './pages/PodPage';
import { getCronJobsScene } from './tabs/CronJobs/CronJobs';
import { getJobsScene } from './tabs/Jobs/Jobs';
import { getOverviewScene } from './tabs/Overview/Overview';
import { clusterVariable } from './variables';
import { usePluginProps } from 'utils/utils.plugin';

const variables = new SceneVariableSet({
variables: [
clusterVariable,
]
})
function getScene({ datasource }: { datasource: string }) {

const timeRange = new SceneTimeRange({
from: 'now-1h',
to: 'now',
});
const variables = new SceneVariableSet({
variables: [
new DataSourceVariable({
name: 'datasource',
label: 'Datasource',
pluginId: 'prometheus',
regex: datasource,
}),
new QueryVariable({
name: 'cluster',
label: 'Cluster',
datasource: {
uid: '$datasource',
type: 'prometheus',
},
query: {
refId: 'cluster',
query: 'label_values(kube_namespace_labels, cluster)',
},
}),
],
})

function getScene() {
const timeRange = new SceneTimeRange({
from: 'now-1h',
to: 'now',
});

return new SceneApp({
pages: [
Expand Down Expand Up @@ -92,7 +111,7 @@ function getScene() {
{
routePath: prefixRoute(`${ROUTES.Workloads}/pods/:name`),
getPage(routeMatch, parent) {
return getPodPage(routeMatch, parent, variables.clone(), timeRange.clone());
return getPodPage(routeMatch, parent);
}
},

Expand All @@ -103,7 +122,10 @@ function getScene() {
}

export const Workloads = () => {
const scene = useMemo(() => getScene(), []);
const props = usePluginProps();
const scene = useMemo(() => getScene({
datasource: props?.meta.jsonData?.datasource || 'prometheus',
}), [props?.meta.jsonData?.datasource]);

return <scene.Component model={scene} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ import { RestartsCellBuilder } from '../../components/RestartsCell';
import { createRowQueries } from './Queries';
import { getSeriesValue } from 'pages/Workloads/seriesHelpers';
import { LabelFilters, asyncQueryRunner } from 'pages/Workloads/queryHelpers';
import { resolveVariable } from 'pages/Workloads/variableHelpers';

const namespaceVariable = new QueryVariable({
name: 'namespace',
label: 'Namespace',
datasource: {
uid: 'prometheus',
uid: '$datasource',
type: 'prometheus',
},
query: {
Expand All @@ -55,7 +56,7 @@ function createRootQuery(staticLabelFilters: LabelFilters, variableSet: SceneVar

return new SceneQueryRunner({
datasource: {
uid: 'prometheus',
uid: '$datasource',
type: 'prometheus',
},
queries: [
Expand Down Expand Up @@ -205,9 +206,11 @@ class TableViz extends SceneObjectBase<TableVizState> {
return;
}

const datasource = resolveVariable(sceneVariables, 'datasource')

asyncQueryRunner({
datasource: {
uid: 'prometheus',
uid: datasource?.toString(),
type: 'prometheus',
},
queries: [
Expand Down
Loading

0 comments on commit aeb7453

Please sign in to comment.