Skip to content

Commit

Permalink
feat: Basic table views for ingresses and services
Browse files Browse the repository at this point in the history
  • Loading branch information
tiithansen committed Jul 13, 2024
1 parent 5a3349f commit 62e5a98
Show file tree
Hide file tree
Showing 13 changed files with 470 additions and 0 deletions.
14 changes: 14 additions & 0 deletions src/common/promql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ export abstract class PromQLVectorExpression extends PromQLExpression {
}
}

class PromQLParenthesisExpression extends PromQLVectorExpression {
constructor(private expr: PromQLExpression) {
super();
}

stringify() {
return `(${this.expr.stringify()})`;
}
}

enum LogicalOperators {
AND = 'and',
OR = 'or',
Expand Down Expand Up @@ -448,4 +458,8 @@ export class PromQL {
static labelReplace(exp: PromQLVectorExpression, dest: string, sourceLabel: string, replacement: string, regex: string) {
return new PromQLLabelReplaceFunction(exp, dest, sourceLabel, replacement, regex);
}

static parenthesis(expr: PromQLVectorExpression) {
return new PromQLParenthesisExpression(expr)
}
}
2 changes: 2 additions & 0 deletions src/components/Routes/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Workloads } from '../../pages/Workloads';
import { prefixRoute } from '../../utils/utils.routing';
import { ROUTES } from '../../constants';
import { usePluginProps } from 'utils/utils.plugin';
import { Network } from 'pages/Network';

export const Routes = () => {

Expand All @@ -15,6 +16,7 @@ export const Routes = () => {
<Switch>
<Route path={prefixRoute(`${ROUTES.Clusters}`)} component={Clusters} />
<Route path={prefixRoute(`${ROUTES.Workloads}`)} component={Workloads} />
<Route path={prefixRoute(`${ROUTES.Network}`)} component={Network} />
<Redirect to={prefixRoute(ROUTES.Clusters)} />
</Switch>
);
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum ROUTES {
Namespaces = 'namespaces',
Nodes = 'nodes',
Workloads = 'workloads',
Network = 'network',
}

export const DATASOURCE_REF = {
Expand Down
34 changes: 34 additions & 0 deletions src/metrics/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,38 @@ export const Metrics = {
instance: 'instance',
}
},
// Ingress
kubeIngressPath: {
name: 'kube_ingress_path',
labels:{
cluster: 'cluster',
}
},
kubeIngressInfo: {
name: 'kube_ingress_info',
labels:{
cluster: 'cluster',
ingressClass: 'ingressclass',
}
},
kubeIngressClassInfo: {
name: 'kube_ingressclass_info',
labels:{
cluster: 'cluster',
}
},
// Services
kubeServiceInfo: {
name: 'kube_service_info',
labels:{
cluster: 'cluster',
}
},
kubeServiceSpecType: {
name: 'kube_service_spec_type',
labels:{
cluster: 'cluster',
type: 'type',
}
},
}
63 changes: 63 additions & 0 deletions src/pages/Network/Network.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
SceneApp,
SceneAppPage,
VariableValueSelectors,
SceneControlsSpacer,
SceneTimePicker,
SceneRefreshPicker,
} from '@grafana/scenes';
import { ROUTES } from '../../constants';
import React, { useMemo } from 'react';
import { prefixRoute } from 'utils/utils.routing';
import { usePluginProps } from 'utils/utils.plugin';
import { createTimeRange, createTopLevelVariables } from '../../common/variableHelpers';
import { getIngressesScene } from './tabs/Ingresses/Ingresses';
import { getServicesScene } from './tabs/Services/Services';

function getScene({ datasource }: { datasource: string }) {

const variables = createTopLevelVariables({ datasource })
const timeRange = createTimeRange()

return new SceneApp({
pages: [
new SceneAppPage({
title: 'Network',
url: prefixRoute(`${ROUTES.Network}`),
$timeRange: timeRange,
controls: [
new VariableValueSelectors({}),
new SceneControlsSpacer(),
new SceneTimePicker({ isOnCanvas: true }),
new SceneRefreshPicker({
intervals: ['5s', '1m', '1h'],
isOnCanvas: true,
}),
],
$variables: variables,
tabs: [
new SceneAppPage({
title: 'Ingresses',
url: prefixRoute(`${ROUTES.Network}/ingresses`),
getScene: getIngressesScene,
}),
new SceneAppPage({
title: 'Services',
url: prefixRoute(`${ROUTES.Network}/services`),
getScene: getServicesScene,
}),
],
getScene: getIngressesScene,
}),
]
})
}

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

return <scene.Component model={scene} />;
};
1 change: 1 addition & 0 deletions src/pages/Network/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Network'
112 changes: 112 additions & 0 deletions src/pages/Network/tabs/Ingresses/Ingresses.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
EmbeddedScene,
SceneFlexLayout,
SceneFlexItem,
SceneVariableSet,
TextBoxVariable,
VariableValueSelectors,
} from '@grafana/scenes';
import { createNamespaceVariable } from 'common/variableHelpers';
import { IngressesQueryBuilder } from './Queries';
import { TableRow } from './types';
import { AsyncTable, Column } from 'components/AsyncTable';
import { SortingState } from 'common/sortingHelpers';
import { ROUTES } from '../../../../constants';
import { prefixRoute } from 'utils/utils.routing';

const columns: Array<Column<TableRow>> = [
{
id: 'ingress',
header: 'INGRESS',
cellType: 'link',
cellProps: {
urlBuilder: (row: TableRow) => prefixRoute(`${ROUTES.Network}/ingress/${row.namespace}/${row.ingress}`),
},
sortingConfig: {
enabled: true,
type: 'label',
local: true
}
},
{
id: 'namespace',
header: 'NAMESPACE',
cellType: 'link',
cellProps: {
urlBuilder: (row: TableRow) => prefixRoute(`${ROUTES.Clusters}/namespaces/${row.namespace}`),
},
sortingConfig: {
enabled: true,
type: 'label',
local: true
}
},
{
id: 'ingressclass',
header: 'CLASS',
sortingConfig: {
enabled: true,
type: 'value',
local: false
}
},
{
id: 'controller',
header: 'CONTROLLER',
sortingConfig: {
enabled: true,
type: 'value',
local: false
}
},
]

function asyncDataRowMapper(row: TableRow, asyncRowData: Record<string, number[]>) { }

function createRowId(row: TableRow) {
return `${row.namespace}/${row.ingress}`;
}

export function getIngressesScene() {

const variables = new SceneVariableSet({
variables: [
createNamespaceVariable(),
new TextBoxVariable({
name: 'search',
label: 'Search',
value: '',
})
],
});

const defaultSorting: SortingState = {
columnId: 'ingress',
direction: 'asc',
}

const queryBuilder = new IngressesQueryBuilder();

return new EmbeddedScene({
$variables: variables,
controls: [
new VariableValueSelectors({})
],
body: new SceneFlexLayout({
children: [
new SceneFlexItem({
width: '100%',
height: '100%',
body: new AsyncTable<TableRow>({
columns: columns,
$data: queryBuilder.rootQueryBuilder(variables, defaultSorting),
createRowId: createRowId,
queryBuilder: queryBuilder,
asyncDataRowMapper: asyncDataRowMapper,
sorting: defaultSorting,
}),
}),
],
}),
})
}
68 changes: 68 additions & 0 deletions src/pages/Network/tabs/Ingresses/Queries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { SceneVariableSet, SceneVariables, SceneQueryRunner } from "@grafana/scenes";
import { SortingState } from "common/sortingHelpers";
import { ColumnSortingConfig, QueryBuilder } from "components/AsyncTable";
import { TableRow } from "./types";
import { PromQL } from "common/promql";
import { Metrics } from "metrics/metrics";

export class IngressesQueryBuilder implements QueryBuilder<TableRow> {
rootQueryBuilder(variables: SceneVariableSet | SceneVariables, sorting: SortingState, sortingConfig?: ColumnSortingConfig<TableRow> | undefined) {

const baseQuery = PromQL.group(
PromQL.parenthesis(
PromQL.metric(Metrics.kubeIngressPath.name)
.withLabelEquals('cluster', '$cluster')
.withLabelMatches('namespace', '$namespace')
// Get ingress class from kube_ingress_info based on ingress and namespace
.multiply()
.on([
'ingress',
'namespace',
])
.groupLeft(
[
Metrics.kubeIngressInfo.labels.ingressClass,
],
PromQL.metric(Metrics.kubeIngressInfo.name)
.withLabelEquals('cluster', '$cluster')
)
)
// Get controller from kube_ingressclass_info based on ingress class
.multiply()
.on([
'ingressclass',
])
.groupLeft(
[
'controller',
],
PromQL.metric(Metrics.kubeIngressClassInfo.name)
.withLabelEquals('cluster', '$cluster')
)
).by([
'namespace',
'ingress',
'ingressclass',
'controller',
])

return new SceneQueryRunner({
datasource: {
uid: '$datasource',
type: 'prometheus',
},
queries: [
{
refId: 'ingresses',
expr: baseQuery.stringify(),
instant: true,
format: 'table'
},
],
})
}

rowQueryBuilder(rows: TableRow[], variables: SceneVariableSet | SceneVariables) {
return []
}
}
5 changes: 5 additions & 0 deletions src/pages/Network/tabs/Ingresses/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface TableRow {
ingress: string;
namespace: string;
controller: string;
}
55 changes: 55 additions & 0 deletions src/pages/Network/tabs/Services/Queries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { SceneVariableSet, SceneVariables, SceneQueryRunner } from "@grafana/scenes";
import { SortingState } from "common/sortingHelpers";
import { ColumnSortingConfig, QueryBuilder } from "components/AsyncTable";
import { TableRow } from "./types";
import { PromQL } from "common/promql";
import { Metrics } from "metrics/metrics";

export class ServicesQueryBuilder implements QueryBuilder<TableRow> {
rootQueryBuilder(variables: SceneVariableSet | SceneVariables, sorting: SortingState, sortingConfig?: ColumnSortingConfig<TableRow> | undefined) {

const baseQuery = PromQL.group(
PromQL.parenthesis(
PromQL.metric(Metrics.kubeServiceInfo.name)
.withLabelEquals('cluster', '$cluster')
.withLabelMatches('namespace', '$namespace')
// Get ingress class from kube_ingress_info based on ingress and namespace
.multiply()
.on([
'service',
'namespace',
])
.groupLeft(
[
Metrics.kubeServiceSpecType.labels.type,
],
PromQL.metric(Metrics.kubeServiceSpecType.name)
.withLabelEquals('cluster', '$cluster')
)
)
).by([
'namespace',
'service',
'type',
])

return new SceneQueryRunner({
datasource: {
uid: '$datasource',
type: 'prometheus',
},
queries: [
{
refId: 'services',
expr: baseQuery.stringify(),
instant: true,
format: 'table'
},
],
})
}

rowQueryBuilder(rows: TableRow[], variables: SceneVariableSet | SceneVariables) {
return []
}
}
Loading

0 comments on commit 62e5a98

Please sign in to comment.