[Kiali] Component library (#3987)

Signed-off-by: Alberto Gutierrez <aljesusg@gmail.com>
This commit is contained in:
Alberto Gutierrez 2025-05-21 17:02:28 +02:00 committed by GitHub
parent a1078e6793
commit cdad6af6f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 4254 additions and 50 deletions

View File

@ -0,0 +1,5 @@
---
'@backstage-community/plugin-kiali': minor
---
Create kiali-react component library

View File

@ -46,7 +46,7 @@ metadata:
- core
- servicemesh
annotations:
kiali.io/provider: kubernetes
kiali.io/provider: Kubernetes
kiali.io/namespace: bookinfo
spec:
type: service

View File

@ -14,7 +14,8 @@
"pluginPackages": [
"@backstage-community/plugin-kiali",
"@backstage-community/plugin-kiali-backend",
"@backstage-community/plugin-kiali-common"
"@backstage-community/plugin-kiali-common",
"@backstage-community/plugin-kiali-react"
]
},
"exports": {

View File

@ -46,7 +46,8 @@
"pluginPackages": [
"@backstage-community/plugin-kiali",
"@backstage-community/plugin-kiali-backend",
"@backstage-community/plugin-kiali-common"
"@backstage-community/plugin-kiali-common",
"@backstage-community/plugin-kiali-react"
]
},
"sideEffects": false,

View File

@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

View File

@ -0,0 +1,37 @@
# @backstage/plugin-kiali-react
> Shared web components for the Kiali plugin in Backstage
This package provides a reusable set of UI components, utilities, and hooks designed to support the development of the [Kiali plugin for Backstage](https://github.com/backstage/community-plugins/blob/main/workspaces/kiali). It aims to keep the plugin codebase clean and modular, while enabling faster iteration and better maintainability.
## ✨ Features
- Reusable UI components tailored to Kiali data and workflows
- Shared logic for interfacing with Istio/Kiali APIs
- Designed to integrate seamlessly within Backstage plugins
- Focused on developer experience and visual consistency
## 🧱 Usage
Import and use the shared components within your Kiali Backstage plugin:
```tsx
import { TrafficGraph } from '@backstage/plugin-kiali-react';
<TrafficGraph model={} />;
```
📁 Project Structure
``bash
src/
├── components/ # Shared React components
├── hooks/ # Reusable custom hooks
└── utils/ # Utility functions
```
🤝 Contributing
Contributions, issues, and feature requests are welcome! Please open a PR or issue in the main plugin repo if it relates to this shared library.
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { Model } from '@patternfly/react-topology';
import { getAllThemes } from '@redhat-developer/red-hat-developer-hub-theme';
import { TrafficGraph } from '../src';
import graphModel from './__data__/bookinfoGraphModel.json';
createDevApp()
.registerPlugin()
.addThemes(getAllThemes())
.addPage({
element: <TrafficGraph model={graphModel as Model} />,
title: 'TrafficGraph',
path: '/kiali/trafficGraph',
});

View File

@ -0,0 +1,63 @@
{
"name": "@backstage-community/plugin-kiali-react",
"version": "0.1.0",
"license": "Apache-2.0",
"private": true,
"description": "Web library for the kiali plugin",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/community-plugins",
"directory": "workspaces/kiali/plugins/kiali-react"
},
"backstage": {
"role": "web-library",
"pluginId": "kiali",
"pluginPackages": [
"@backstage-community/plugin-kiali",
"@backstage-community/plugin-kiali-backend",
"@backstage-community/plugin-kiali-common",
"@backstage-community/plugin-kiali-react"
]
},
"sideEffects": false,
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage-community/plugin-kiali-common": "workspace:^",
"@backstage/core-plugin-api": "^1.10.6",
"@material-ui/core": "^4.9.13",
"@patternfly/react-core": "^5.1.1",
"@patternfly/react-icons": "^5.1.1",
"@patternfly/react-topology": "5.4.1",
"typestyle": "^2.4.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.32.0",
"@backstage/dev-utils": "^1.1.9",
"@backstage/test-utils": "^1.7.7",
"@redhat-developer/red-hat-developer-hub-theme": "0.4.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,15 @@
## API Report File for "@backstage-community/plugin-kiali-react"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { JSX as JSX_2 } from 'react/jsx-runtime';
import { Model } from '@patternfly/react-topology';
// Warning: (ae-missing-release-tag) "TrafficGraph" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TrafficGraph: (props: { model: Model }) => JSX_2.Element;
// (No @packageDocumentation comment for this package)
```

View File

@ -0,0 +1,214 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Badge, Tooltip, TooltipPosition } from '@patternfly/react-core';
import { CSSProperties, default as React } from 'react';
import { style } from 'typestyle';
import { PFColors } from './PfColors';
export type PFBadgeType = {
badge: string;
tt?: React.ReactFragment;
style?: React.CSSProperties;
};
// PF Badges used by Kiali, keep alphabetized
// avoid duplicate badge letters, especially if they may appear on the same page
export const PFBadges: { [key: string]: PFBadgeType } = Object.freeze({
App: {
badge: 'A',
tt: 'Application',
style: { backgroundColor: PFColors.Green500 },
} as PFBadgeType,
Adapter: { badge: 'A', tt: 'Adapter' } as PFBadgeType,
AttributeManifest: { badge: 'AM', tt: 'Attribute Manifest' } as PFBadgeType,
AuthorizationPolicy: {
badge: 'AP',
tt: 'Authorization Policy',
} as PFBadgeType,
Cluster: {
badge: 'C',
tt: 'Cluster',
style: { backgroundColor: PFColors.Blue300 },
} as PFBadgeType,
ClusterRBACConfig: {
badge: 'CRC',
tt: 'Cluster RBAC Configuration',
} as PFBadgeType,
Container: {
badge: 'C',
tt: 'Container',
style: { backgroundColor: PFColors.Blue300 },
} as PFBadgeType,
DestinationRule: { badge: 'DR', tt: 'Destination Rule' } as PFBadgeType,
EnvoyFilter: { badge: 'EF', tt: 'Envoy Filter' } as PFBadgeType,
ExternalService: { badge: 'ES', tt: 'External Service' } as PFBadgeType,
FaultInjectionAbort: {
badge: 'FI',
tt: 'Fault Injection: Abort',
style: { backgroundColor: PFColors.Purple500 },
} as PFBadgeType,
FaultInjectionDelay: {
badge: 'FI',
tt: 'Fault Injection: Delay',
style: { backgroundColor: PFColors.Purple500 },
} as PFBadgeType,
FederatedService: { badge: 'FS', tt: 'Federated Service' } as PFBadgeType,
Gateway: { badge: 'G', tt: 'Gateway' } as PFBadgeType,
HTTPRoute: { badge: 'HTTP', tt: 'HTTPRoute' } as PFBadgeType,
K8sGateway: { badge: 'G', tt: 'Gateway (K8s)' } as PFBadgeType,
K8sHTTPRoute: { badge: 'HTTP', tt: 'HTTPRoute (K8s)' } as PFBadgeType,
Handler: { badge: 'H', tt: 'Handler' },
Host: { badge: 'H', tt: 'Host' },
Instance: { badge: 'I', tt: 'Instance' },
MeshPolicy: { badge: 'MP', tt: 'Mesh Policy' } as PFBadgeType,
MirroredWorkload: {
badge: 'MI',
tt: 'Mirrored Workload',
style: { backgroundColor: PFColors.Purple500 },
} as PFBadgeType,
Namespace: {
badge: 'NS',
tt: 'Namespace',
style: { backgroundColor: PFColors.Green600 },
} as PFBadgeType,
Operation: { badge: 'O', tt: 'Operation' } as PFBadgeType,
PeerAuthentication: { badge: 'PA', tt: 'Peer Authentication' } as PFBadgeType,
Pod: {
badge: 'P',
tt: 'Pod',
style: { backgroundColor: PFColors.Cyan300 },
} as PFBadgeType,
Policy: { badge: 'P', tt: 'Policy' } as PFBadgeType,
RBACConfig: { badge: 'RC', tt: 'RBAC Configuration' } as PFBadgeType,
RequestAuthentication: {
badge: 'RA',
tt: 'Request Authentication',
} as PFBadgeType,
RequestRetry: {
badge: 'RR',
tt: 'Request Retry',
style: { backgroundColor: PFColors.Purple500 },
} as PFBadgeType,
RequestTimeout: {
badge: 'RT',
tt: 'Request Timeout',
style: { backgroundColor: PFColors.Purple500 },
} as PFBadgeType,
Rule: { badge: 'R', tt: 'Rule' } as PFBadgeType,
Service: {
badge: 'S',
tt: 'Service',
style: { backgroundColor: PFColors.LightGreen500 },
} as PFBadgeType,
ServiceEntry: { badge: 'SE', tt: 'Service Entry' } as PFBadgeType,
ServiceRole: { badge: 'SR', tt: 'Service Role' } as PFBadgeType,
ServiceRoleBinding: {
badge: 'SRB',
tt: 'Service Role Binding',
} as PFBadgeType,
Sidecar: { badge: 'SC', tt: 'Istio Sidecar Proxy' } as PFBadgeType,
WasmPlugin: { badge: 'WP', tt: 'Istio Wasm Plugin' } as PFBadgeType,
Telemetry: { badge: 'TM', tt: 'Istio Telemetry' } as PFBadgeType,
Template: { badge: 'T', tt: 'Template' } as PFBadgeType,
Unknown: { badge: 'U', tt: 'Unknown' } as PFBadgeType,
VirtualService: { badge: 'VS', tt: 'Virtual Service' } as PFBadgeType,
Waypoint: { badge: 'W', tt: 'Waypoint proxy' } as PFBadgeType,
Workload: {
badge: 'W',
tt: 'Workload',
style: { backgroundColor: PFColors.Blue500 },
} as PFBadgeType,
WorkloadEntry: { badge: 'WE', tt: 'Workload Entry' } as PFBadgeType,
WorkloadGroup: { badge: 'WG', tt: 'Workload Group' } as PFBadgeType,
});
// This is styled for consistency with OpenShift Console. See console: public/components/_resource.scss
export const kialiBadge = style({
backgroundColor: PFColors.Badge,
color: PFColors.White,
borderRadius: '20px',
flexShrink: 0,
fontFamily: 'var(--pf-v5-global--FontFamily--text)',
fontSize: 'var(--kiali-global--font-size)',
lineHeight: '16px',
marginRight: '4px',
minWidth: '1.5em',
padding: '1px 4px',
textAlign: 'center',
whiteSpace: 'nowrap',
});
export const kialiBadgeSmall = style({
backgroundColor: PFColors.Badge,
color: PFColors.White,
borderRadius: '20px',
flexShrink: 0,
fontFamily: 'var(--pf-v5-global--FontFamily--text)',
fontSize: '12px',
lineHeight: '13px',
marginRight: '5px',
minWidth: '1.3em',
padding: '1px 3px',
textAlign: 'center',
whiteSpace: 'nowrap',
});
type PFBadgeProps = {
badge: PFBadgeType;
isRead?: boolean;
keyValue?: string;
position?: string; // default=auto
size?: 'global' | 'sm';
style?: CSSProperties;
tooltip?: React.ReactFragment;
};
export class PFBadge extends React.PureComponent<PFBadgeProps> {
render() {
const key = this.props.keyValue || `pfbadge-${this.props.badge.badge}`;
const ttKey = `tt-${key}`;
const BadgeStyle = { ...this.props.badge.style, ...this.props.style };
const tooltip = this.props.tooltip || this.props.badge.tt;
const className = this.props.size === 'sm' ? kialiBadgeSmall : kialiBadge;
const badge = (
<Badge
className={className}
id={key}
isRead={this.props.isRead || false}
key={key}
style={BadgeStyle}
>
{this.props.badge.badge}
</Badge>
);
return !tooltip ? (
badge
) : (
<Tooltip
content={<>{tooltip}</>}
id={ttKey}
key={ttKey}
position={
(this.props.position as TooltipPosition) || TooltipPosition.auto
}
>
{badge}
</Tooltip>
);
}
}

View File

@ -0,0 +1,252 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// PF colors, and moreover, use the defined color variables such that any changes made by PF are
// picked up when the PF version is updated. The preferred, standard way, is in CSS styling. In
// those cases we can directly let CSS resolve the PF var. So, whenever possible use the PFColors
// enum below. In certain cases (like in cytoscape), we need the explicit hex value. In that case
// we must actually get the computed value. We do this as soon as we get an initial document (in
// StartupInitializer.tsx). In those cases use PFColorVals. Note that those values are not
// available until they can be computed, so don't use them in constants or before they are
// available.
// Colors used by Kiali for CSS styling
export enum PFColors {
Black100 = 'var(--pf-v5-global--palette--black-100)',
Black150 = 'var(--pf-v5-global--palette--black-150)',
Black200 = 'var(--pf-v5-global--palette--black-200)',
Black300 = 'var(--pf-v5-global--palette--black-300)',
Black400 = 'var(--pf-v5-global--palette--black-400)',
Black500 = 'var(--pf-v5-global--palette--black-500)',
Black600 = 'var(--pf-v5-global--palette--black-600)',
Black700 = 'var(--pf-v5-global--palette--black-700)',
Black800 = 'var(--pf-v5-global--palette--black-800)',
Black900 = 'var(--pf-v5-global--palette--black-900)',
Black1000 = 'var(--pf-v5-global--palette--black-1000)',
Blue50 = 'var(--pf-v5-global--palette--blue-50)',
Blue200 = 'var(--pf-v5-global--palette--blue-200)',
Blue300 = 'var(--pf-v5-global--palette--blue-300)',
Blue400 = 'var(--pf-v5-global--palette--blue-400)',
Blue500 = 'var(--pf-v5-global--palette--blue-500)',
Blue600 = 'var(--pf-v5-global--palette--blue-600)',
Cyan300 = 'var(--pf-v5-global--palette--cyan-300)',
Gold400 = 'var(--pf-v5-global--palette--gold-400)',
Green300 = 'var(--pf-v5-global--palette--green-300)',
Green400 = 'var(--pf-v5-global--palette--green-400)',
Green500 = 'var(--pf-v5-global--palette--green-500)',
Green600 = 'var(--pf-v5-global--palette--green-600)',
LightBlue400 = 'var(--pf-v5-global--palette--light-blue-400)',
LightGreen400 = 'var(--pf-v5-global--palette--light-green-400)',
LightGreen500 = 'var(--pf-v5-global--palette--light-green-500)',
Orange50 = 'var(--pf-v5-global--palette--orange-50)',
Orange400 = 'var(--pf-v5-global--palette--orange-400)',
Purple100 = 'var(--pf-v5-global--palette--purple-100)',
Purple200 = 'var(--pf-v5-global--palette--purple-200)',
Purple500 = 'var(--pf-v5-global--palette--purple-500)',
Red50 = 'var(--pf-v5-global--palette--red-50)',
Red100 = 'var(--pf-v5-global--palette--red-100)',
Red200 = 'var(--pf-v5-global--palette--red-200)',
Red500 = 'var(--pf-v5-global--palette--red-500)',
White = 'var(--pf-v5-global--palette--white)',
// semantic kiali colors
Active = 'var(--pf-v5-global--active-color--400)',
ActiveText = 'var(--pf-v5-global--primary-color--200)',
Badge = 'var(--pf-v5-global--palette--blue-300)',
Replay = 'var(--pf-v5-global--active-color--300)',
Link = 'var(--pf-v5-global--link--Color)',
// Health/Alert colors https://www.patternfly.org/v4/design-guidelines/styles/colors
Danger = 'var(--pf-v5-global--danger-color--100)',
Info = 'var(--pf-v5-global--info-color--100)',
InfoBackground = 'var(--pf-v5-global--info-color--200)',
Success = 'var(--pf-v5-global--success-color--100)',
SuccessBackground = 'var(--pf-v5-global--success-color--200)',
Warning = 'var(--pf-v5-global--warning-color--100)',
// chart-specific color values, for rates charts where 4xx is really Danger not Warning
ChartDanger = 'var(--pf-v5-global--danger-color--300)',
ChartOther = 'var(--pf-v5-global--palette-black-1000)',
ChartWarning = 'var(--pf-v5-global--danger-color--100)',
// PF background colors (compatible with dark mode)
BackgroundColor100 = 'var(--pf-v5-global--BackgroundColor--100)',
BackgroundColor150 = 'var(--pf-v5-global--BackgroundColor--150)',
BackgroundColor200 = 'var(--pf-v5-global--BackgroundColor--200)',
// PF standard colors (compatible with dark mode)
Color100 = 'var(--pf-v5-global--Color--100)',
Color200 = 'var(--pf-v5-global--Color--200)',
ColorLight100 = 'var(--pf-v5-global--Color--light-100)',
ColorLight200 = 'var(--pf-v5-global--Color--light-200)',
ColorLight300 = 'var(--pf-v5-global--Color--light-300)',
// PF border colors (compatible with dark mode)
BorderColor100 = 'var(--pf-v5-global--BorderColor--100)',
BorderColor200 = 'var(--pf-v5-global--BorderColor--200)',
BorderColor300 = 'var(--pf-v5-global--BorderColor--300)',
BorderColorLight100 = 'var(--pf-v5-global--BorderColor--light-100)',
}
// The hex string value of the PF CSS variable
export type PFColorVal = string;
// Color values used by Kiali outside of CSS (i.e. when we must have the actual hex value)
export type PFColorValues = {
Black100: PFColorVal;
Black150: PFColorVal;
Black200: PFColorVal;
Black300: PFColorVal;
Black400: PFColorVal;
Black500: PFColorVal;
Black600: PFColorVal;
Black700: PFColorVal;
Black1000: PFColorVal;
Blue50: PFColorVal;
Blue300: PFColorVal;
Blue600: PFColorVal;
Red50: PFColorVal;
Orange50: PFColorVal;
Gold400: PFColorVal;
Green400: PFColorVal;
Purple200: PFColorVal;
White: PFColorVal;
// Health/Alert colors https://www.patternfly.org/v4/design-guidelines/styles/colors
Danger: PFColorVal;
Success: PFColorVal;
Warning: PFColorVal;
// PF colors (compatible with dark mode)
BackgroundColor100: PFColorVal;
BackgroundColor200: PFColorVal;
Color100: PFColorVal;
Color200: PFColorVal;
BorderColor100: PFColorVal;
BorderColor200: PFColorVal;
BorderColor300: PFColorVal;
};
export let PFColorVals: PFColorValues;
/*
Extract color from var
Input : var(--pf-v5-global--palette--black-100)
Output: --pf-v5-global--palette--black-100
- In case there is not var then return the same input
*/
const getColor = (val: string) => {
return val.indexOf('var(') === 0 ? val.split('(').pop()!.split(')')[0] : val;
};
export const setPFColorVals = (element: Element) => {
PFColorVals = {
// color values used by kiali
Black100: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Black100)),
Black150: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Black150)),
Black200: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Black200)),
Black300: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Black300)),
Black400: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Black400)),
Black500: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Black500)),
Black600: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Black600)),
Black700: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Black700)),
Black1000: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Black1000)),
Blue50: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Blue50)),
Blue300: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Blue300)),
Blue600: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Blue600)),
Red50: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Red50)),
Orange50: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Orange50)),
Gold400: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Gold400)),
Green400: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Green400)),
Purple200: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Purple200)),
White: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.White)),
// status color values used by kiali
Danger: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Danger)),
Success: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Success)),
Warning: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Warning)),
// PF colors (compatible with dark mode)
BackgroundColor100: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.BackgroundColor100)),
BackgroundColor200: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.BackgroundColor200)),
Color100: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Color100)),
Color200: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.Color200)),
BorderColor100: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.BorderColor100)),
BorderColor200: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.BorderColor200)),
BorderColor300: window
.getComputedStyle(element)
.getPropertyValue(getColor(PFColors.BorderColor300)),
};
};

View File

@ -0,0 +1,41 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ColaLayout,
Graph,
Layout,
LayoutFactory,
} from '@patternfly/react-topology';
import { KialiDagreLayout } from './layouts/KialiDagreLayout';
export const KialiLayoutFactory: LayoutFactory = (
type: string,
graph: Graph,
): Layout => {
switch (type) {
case 'Dagre':
return new KialiDagreLayout(graph, {
linkDistance: 40,
nodeDistance: 25,
marginx: undefined,
marginy: undefined,
ranker: 'network-simplex',
rankdir: 'LR',
});
default:
return new ColaLayout(graph, { layoutOnDrag: false });
}
};

View File

@ -0,0 +1,84 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
action,
createTopologyControlButtons,
defaultControlButtonsOptions,
Model,
TopologyControlBar,
TopologyView,
Visualization,
VisualizationProvider,
VisualizationSurface,
} from '@patternfly/react-topology';
import { useEffect, useState } from 'react';
import { KialiComponentFactory } from './factories/KialiComponentFactory';
import { KialiLayoutFactory } from './factories/KialiLayoutFactory';
const getVisualization = (): Visualization => {
const vis = new Visualization();
vis.registerLayoutFactory(KialiLayoutFactory);
vis.registerComponentFactory(KialiComponentFactory);
vis.setFitToScreenOnLayout(true);
return vis;
};
export const TrafficGraph = (props: { model: Model }) => {
const [controller] = useState(getVisualization());
useEffect(() => {
controller.fromModel(props.model, false);
}, [props.model, controller]);
return (
<TopologyView
controlBar={
<TopologyControlBar
controlButtons={createTopologyControlButtons({
...defaultControlButtonsOptions,
zoomInCallback: action(() => {
controller.getGraph().scaleBy(4 / 3);
}),
zoomOutCallback: action(() => {
controller.getGraph().scaleBy(0.75);
}),
fitToScreenCallback: action(() => {
controller.getGraph().fit(80);
}),
resetViewCallback: action(() => {
controller.getGraph().reset();
controller.getGraph().layout();
}),
legend: false,
zoomInAriaLabel: '',
zoomOutAriaLabel: '',
fitToScreenAriaLabel: '',
resetViewAriaLabel: '',
zoomInTip: '',
zoomOutTip: '',
fitToScreenTip: '',
resetViewTip: '',
})}
/>
}
>
<VisualizationProvider controller={controller}>
<VisualizationSurface state={props.model} />
</VisualizationProvider>
</TopologyView>
);
};

View File

@ -0,0 +1,48 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ComponentFactory,
DefaultGroup,
GraphComponent,
ModelKind,
withPanZoom,
withSelection,
} from '@patternfly/react-topology';
import { KialiEdge } from '../styles/KialiEdge';
import { KialiNode } from '../styles/KialiNode';
export const KialiComponentFactory: ComponentFactory = (
kind: ModelKind,
type: string,
) => {
switch (type) {
case 'group':
return DefaultGroup;
default:
switch (kind) {
case ModelKind.graph:
return withPanZoom()(GraphComponent);
case ModelKind.node:
return KialiNode as any;
case ModelKind.edge:
return withSelection({ multiSelect: false, controlled: false })(
KialiEdge as any,
);
default:
return undefined;
}
}
};

View File

@ -0,0 +1,41 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ColaLayout,
Graph,
Layout,
LayoutFactory,
} from '@patternfly/react-topology';
import { KialiDagreLayout } from '../layouts/KialiDagreLayout';
export const KialiLayoutFactory: LayoutFactory = (
type: string,
graph: Graph,
): Layout => {
switch (type) {
case 'Dagre':
return new KialiDagreLayout(graph, {
linkDistance: 40,
nodeDistance: 25,
marginx: undefined,
marginy: undefined,
ranker: 'network-simplex',
rankdir: 'LR',
});
default:
return new ColaLayout(graph, { layoutOnDrag: false });
}
};

View File

@ -0,0 +1,16 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './TrafficGraph';

View File

@ -0,0 +1,22 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DagreLayout } from '@patternfly/react-topology';
export class KialiDagreLayout extends DagreLayout {
override updateEdgeBendpoints() {
// Your implementation here
}
}

View File

@ -0,0 +1,170 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
DefaultEdge,
Edge,
observer,
ScaleDetailsLevel,
WithSelectionProps,
} from '@patternfly/react-topology';
import useDetailsLevel from '@patternfly/react-topology/dist/esm/hooks/useDetailsLevel';
import { default as React } from 'react';
import { classes, style } from 'typestyle';
import { PFColors } from '../../../components/Pf/PfColors';
// This is our styled edge component registered in stylesComponentFactory.tsx. It is responsible for adding customizations that then get passed down to DefaultEdge. The current customizations:
// data.pathStyle?: React.CSSProperties // additional CSS stylings for the edge/path (not the endpoint).
// data.isFind?: boolean // adds graph-find overlay
// data.isUnhighlighted?: boolean // adds unhighlight effects
// data.hasSpans?: Span[] // adds trace overlay
// add showTag prop and show scaled tag on hover (when showTag is false)
// support [lock] icons on edge tags
const ColorFind = PFColors.Gold400;
const ColorSpan = PFColors.Purple200;
const OverlayOpacity = 0.3;
const OverlayWidth = 30;
type StyleEdgeProps = {
element: Edge;
} & WithSelectionProps;
const tagClass = style({
fontFamily: 'Verdana,Arial,Helvetica,sans-serif,pficon',
});
const StyleEdgeComponent: React.FC<StyleEdgeProps> = ({ element, ...rest }) => {
const data = element.getData();
const detailsLevel = useDetailsLevel();
const cssClasses: string[] = [];
// Change edge color according to the pathStyle
const edgeClass = style({
$nest: {
'& .pf-topology__edge__link': data.pathStyle,
},
});
cssClasses.push(edgeClass);
const edgeHoverClass = style({
$nest: {
'& .pf-topology__edge.pf-m-hover': {
$nest: {
'& .pf-topology__edge__link, & .pf-topology-connector-arrow':
data.pathStyle,
},
},
},
});
cssClasses.push(edgeHoverClass);
// Change connector color according to the pathStyle
const connectorClass = style({
$nest: {
'& .pf-topology-connector-arrow': {
stroke: data.pathStyle.stroke,
fill: data.pathStyle.stroke,
},
},
});
cssClasses.push(connectorClass);
const edgeConnectorArrowHoverStyles = style({
$nest: {
'& .pf-topology__edge.pf-m-hover': {
$nest: {
'& .pf-topology-connector-arrow': {
stroke: data.pathStyle.stroke,
fill: data.pathStyle.stroke,
},
},
},
},
});
cssClasses.push(edgeConnectorArrowHoverStyles);
// If has spans, add the span overlay
if (data.hasSpans) {
const spansClass = style({
$nest: {
'& .pf-topology__edge__background': {
strokeWidth: OverlayWidth,
stroke: ColorSpan,
strokeOpacity: OverlayOpacity,
},
},
});
cssClasses.push(spansClass);
// If isHighlighted, add the highlight overlay
} else if (data.isFind) {
const findClass = style({
$nest: {
'& .pf-topology__edge__background': {
strokeWidth: OverlayWidth,
stroke: ColorFind,
strokeOpacity: OverlayOpacity,
},
},
});
cssClasses.push(findClass);
}
// Set animation duration velocity
if (data.animationDuration) {
const animationClass = style({
$nest: {
'& .pf-topology__edge__link': {
animationDuration: `${data.animationDuration}s`,
},
},
});
cssClasses.push(animationClass);
}
// Set the path style when unhighlighted (opacity)
let opacity = 1;
if (data.isUnhighlighted) {
opacity = 0.1;
}
const passedData = React.useMemo(() => {
const newData = { ...data };
if (detailsLevel !== ScaleDetailsLevel.high) {
newData.showTag = false;
}
Object.keys(newData).forEach(key => {
if (newData[key] === undefined) {
delete newData[key];
}
});
return newData;
}, [data, detailsLevel]);
return (
<g style={{ opacity: opacity }}>
<DefaultEdge
className={classes(...cssClasses)}
element={element}
tagClass={tagClass}
{...rest}
{...passedData}
/>
</g>
);
};
export const KialiEdge = observer(StyleEdgeComponent);

View File

@ -0,0 +1,101 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CubesIcon } from '@patternfly/react-icons';
import {
DefaultGroup,
Node,
ScaleDetailsLevel,
ShapeProps,
WithSelectionProps,
} from '@patternfly/react-topology';
import useDetailsLevel from '@patternfly/react-topology/dist/esm/hooks/useDetailsLevel';
import { default as React } from 'react';
import { PFColors } from '../../../components/Pf/PfColors';
const ICON_PADDING = 20;
export enum DataTypes {
Default,
}
type StyleGroupProps = {
element: Node;
collapsible: boolean;
collapsedWidth?: number;
collapsedHeight?: number;
onCollapseChange?: (group: Node, collapsed: boolean) => void;
getCollapsedShape?: (node: Node) => React.FC<ShapeProps>;
collapsedShadowOffset?: number; // defaults to 10
} & WithSelectionProps;
export function KialiGroup({
element,
collapsedWidth = 75,
collapsedHeight = 75,
...rest
}: StyleGroupProps) {
const data = element.getData();
const detailsLevel = useDetailsLevel();
const passedData = React.useMemo(() => {
const newData = { ...data };
Object.keys(newData).forEach(key => {
if (newData[key] === undefined) {
delete newData[key];
}
});
return newData;
}, [data]);
if (data.isFocused) {
element.setData({ ...data, isFocused: false });
}
const renderIcon = (): React.ReactNode => {
const iconSize =
Math.min(collapsedWidth, collapsedHeight) - ICON_PADDING * 2;
const Component = CubesIcon;
return (
<g
transform={`translate(${(collapsedWidth - iconSize) / 2}, ${
(collapsedHeight - iconSize) / 2
})`}
>
<Component
style={{ color: PFColors.Color200 }}
width={iconSize}
height={iconSize}
/>
</g>
);
};
return (
<g>
<DefaultGroup
element={element}
collapsedWidth={collapsedWidth}
collapsedHeight={collapsedHeight}
showLabel={detailsLevel === ScaleDetailsLevel.high}
{...rest}
{...passedData}
>
{element.isCollapsed() ? renderIcon() : null}
</DefaultGroup>
</g>
);
}

View File

@ -0,0 +1,161 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { KeyIcon, TopologyIcon } from '@patternfly/react-icons';
import {
DefaultNode,
getShapeComponent,
Node,
NodeShape,
observer,
ScaleDetailsLevel,
useHover,
WithSelectionProps,
} from '@patternfly/react-topology';
import useDetailsLevel from '@patternfly/react-topology/dist/esm/hooks/useDetailsLevel';
import { default as React } from 'react';
import { style } from 'typestyle';
import { PFColors } from '../../../components/Pf/PfColors';
// This is the registered Node component override that utilizes our customized Node.tsx component.
type StyleNodeProps = {
element: Node;
} & WithSelectionProps;
const renderIcon = (element: Node): React.ReactNode => {
let Component: React.ComponentClass<React.ComponentProps<any>> | undefined;
const data = element.getData();
const isInaccessible = data.isInaccessible;
const isServiceEntry = data.isServiceEntry;
const isBox = data.isBox;
if (isInaccessible && !isServiceEntry && !isBox) {
Component = KeyIcon;
}
const isOutside = data.isOutside;
if (isOutside && !isBox) {
Component = TopologyIcon;
}
// this blurb taken from PFT demo StyleNode.tsx, not sure if it's required
// vv
const { width, height } = element.getDimensions();
const shape = element.getNodeShape();
const iconSize =
(shape === NodeShape.trapezoid ? width : Math.min(width, height)) -
(shape === NodeShape.stadium ? 5 : 20) * 2;
// ^^
return Component ? (
<g
transform={`translate(${(width - iconSize) / 2}, ${
(height - iconSize) / 2
})`}
>
<Component width={iconSize} height={iconSize} />
</g>
) : (
<></>
);
};
const StyleNodeComponent: React.FC<StyleNodeProps> = ({ element, ...rest }) => {
const data = element.getData();
const detailsLevel = useDetailsLevel();
const [hover, hoverRef] = useHover();
const ShapeComponent = getShapeComponent(element);
const ColorFind = PFColors.Gold400;
const ColorSpan = PFColors.Purple200;
const OverlayOpacity = 0.3;
const OverlayWidth = 40;
const traceOverlayStyle = style({
strokeWidth: OverlayWidth,
stroke: ColorSpan,
strokeOpacity: OverlayOpacity,
});
const findOverlayStyle = style({
strokeWidth: OverlayWidth,
stroke: ColorFind,
strokeOpacity: OverlayOpacity,
});
// Set the path style when unhighlighted (opacity)
let opacity = 1;
if (data.isUnhighlighted) {
opacity = 0.1;
}
const passedData = React.useMemo(() => {
const newData = { ...data };
if (detailsLevel !== ScaleDetailsLevel.high) {
newData.tag = undefined;
}
Object.keys(newData).forEach(key => {
if (newData[key] === undefined) {
delete newData[key];
}
});
return newData;
}, [data, detailsLevel]);
const { width, height } = element.getDimensions();
return (
<g
data-test={`node-${data.getLabel}`}
style={{ opacity: opacity }}
ref={hoverRef as any}
>
{data.hasSpans && (
<ShapeComponent
className={traceOverlayStyle}
width={width}
height={height}
element={element}
/>
)}
{data.isFind && (
<ShapeComponent
className={findOverlayStyle}
width={width}
height={height}
element={element}
/>
)}
<DefaultNode
element={element}
{...rest}
{...passedData}
attachments={
hover || detailsLevel === ScaleDetailsLevel.high
? data.attachments
: undefined
}
scaleLabel={hover && detailsLevel !== ScaleDetailsLevel.high}
// scaleNode={hover && detailsLevel === ScaleDetailsLevel.low}
showLabel={hover || detailsLevel === ScaleDetailsLevel.high}
showStatusBackground={detailsLevel === ScaleDetailsLevel.low}
>
{(hover || detailsLevel !== ScaleDetailsLevel.low) &&
renderIcon(element)}
</DefaultNode>
</g>
);
};
export const KialiNode = observer(StyleNodeComponent);

View File

@ -0,0 +1,37 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
DecoratedGraphEdgeData,
Span,
} from '@backstage-community/plugin-kiali-common/types';
import {
EdgeTerminalType,
GraphElement,
NodeStatus,
} from '@patternfly/react-topology';
export type EdgeData = DecoratedGraphEdgeData & {
endTerminalType: EdgeTerminalType;
hasSpans?: Span[];
isFind?: boolean;
isHighlighted?: boolean;
isSelected?: boolean;
isUnhighlighted?: boolean;
onHover?: (element: GraphElement, isMouseIn: boolean) => void;
pathStyle?: React.CSSProperties;
tag?: string;
tagStatus?: NodeStatus;
};

View File

@ -0,0 +1,31 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Namespace } from '@backstage-community/plugin-kiali-common/types';
import {
EdgeLabelMode,
GraphType,
TrafficRate,
} from '@backstage-community/plugin-kiali-common/types';
export type GraphPFSettings = {
activeNamespaces: Namespace[];
edgeLabels: EdgeLabelMode[];
graphType: GraphType;
showOutOfMesh: boolean;
showSecurity: boolean;
showVirtualServices: boolean;
trafficRates: TrafficRate[];
};

View File

@ -0,0 +1,93 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
BoxByType,
DecoratedGraphNodeData,
DEGRADED,
FAILURE,
NodeType,
} from '@backstage-community/plugin-kiali-common/types';
import {
BadgeLocation,
GraphElement,
LabelPosition,
NodeShape,
NodeStatus,
} from '@patternfly/react-topology';
export type NodeData = DecoratedGraphNodeData & {
// These are node.data fields that have an impact on the PFT rendering of the node.
// TODO: Is there an actual type defined for these in PFT?
attachments?: React.ReactNode; // ie. decorators
badge?: string;
badgeBorderColor?: string;
badgeClassName?: string;
badgeColor?: string;
badgeLocation?: BadgeLocation;
badgeTextColor?: string;
column?: number;
component?: React.ReactNode;
icon?: React.ReactNode;
isFind?: boolean;
isHighlighted?: boolean;
isSelected?: boolean;
isUnhighlighted?: boolean;
labelIcon?: React.ReactNode;
labelIconClass?: string;
labelIconPadding?: number;
labelPosition?: LabelPosition;
marginX?: number;
onHover?: (element: GraphElement, isMouseIn: boolean) => void;
row?: number;
secondaryLabel?: string;
setLocation?: boolean;
showContextMenu?: boolean;
showStatusDecorator?: boolean;
statusDecoratorTooltip?: React.ReactNode;
truncateLength?: number;
x?: number;
y?: number;
};
export const getNodeStatus = (data: NodeData): NodeStatus => {
if ((data.isBox && data.isBox !== BoxByType.APP) || data.isIdle) {
return NodeStatus.default;
}
switch (data.healthStatus) {
case DEGRADED.name:
return NodeStatus.warning;
case FAILURE.name:
return NodeStatus.danger;
default:
return NodeStatus.success;
}
};
export const getNodeShape = (data: NodeData): NodeShape => {
switch (data.nodeType) {
case NodeType.AGGREGATE:
return NodeShape.hexagon;
case NodeType.APP:
return NodeShape.rect;
case NodeType.SERVICE:
return data.isServiceEntry ? NodeShape.trapezoid : NodeShape.rhombus;
case NodeType.WORKLOAD:
return NodeShape.circle;
default:
return NodeShape.ellipse;
}
};

View File

@ -0,0 +1,18 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NodeModel } from '@patternfly/react-topology';
export type NodeMap = Map<string, NodeModel>;

View File

@ -0,0 +1,20 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// The index file in ./components/ is typically responsible for selecting
// which components are public API and should be exported from the package.
export * from './TrafficGraph';

View File

@ -0,0 +1,16 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './components';

View File

@ -0,0 +1,16 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';

View File

@ -0,0 +1,60 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { style } from 'typestyle';
import { NestedCSSProperties } from 'typestyle/lib/types';
import { PFColors } from '../components/Pf/PfColors';
/*
* 70px is the height of the bottom toolbar (save, reload and cancel buttons)
* 100px is the top margin of the yaml editor (Adjusted with RenderComponentScroll).
* So, substracting 170px from the tab content height.
*/
export const istioAceEditorStyle = style({
'--kiali-yaml-editor-height':
'calc(var(--kiali-details-pages-tab-content-height) - 170px)',
position: 'relative',
minHeight: '200px',
border: `1px solid ${PFColors.BorderColor200}`,
fontSize: 'var(--kiali-global--font-size) !important',
$nest: {
'& div.ace_gutter-cell.ace_info': {
backgroundImage: 'none',
$nest: {
'&::before': {
content: `'\\E92b'`,
fontFamily: 'pficon',
left: '5px',
position: 'absolute',
},
},
},
},
} as NestedCSSProperties);
export const istioValidationErrorStyle = style({
position: 'absolute',
background: 'rgba(204, 0, 0, 0.5)',
});
export const istioValidationWarningStyle = style({
position: 'absolute',
background: 'rgba(236, 122, 8, 0.5)',
});
export const istioValidationInfoStyle = style({
position: 'absolute',
background: PFColors.ColorLight300,
});

View File

@ -0,0 +1,70 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { style } from 'typestyle';
import { NestedCSSProperties } from 'typestyle/lib/types';
import { PFColors } from '../components/Pf/PfColors';
export const containerStyle = style({
overflow: 'auto',
});
// this emulates Select component .pf-v5-c-select__menu
export const menuStyle = style({
fontSize: 'var(--kiali-global--font-size)',
});
// this emulates Select component .pf-v5-c-select__menu but w/o cursor manipulation
export const menuEntryStyle = style({
display: 'inline-block',
width: '100%',
});
// this emulates Select component .pf-v5-c-select__menu-group-title but with less bottom padding to conserve space
export const titleStyle = style({
padding: '0.5rem 1rem 0 1rem',
fontWeight: 700,
color: PFColors.Color200,
});
const itemStyle: NestedCSSProperties = {
alignItems: 'center',
whiteSpace: 'nowrap',
margin: 0,
padding: '0.375rem 1rem',
display: 'inline-block',
};
// this emulates Select component .pf-v5-c-select__menu-item but with less vertical padding to conserve space
export const itemStyleWithoutInfo = style(itemStyle);
// this emulates Select component .pf-v5-c-select__menu-item but with less vertical padding to conserve space
export const itemStyleWithInfo = style({
...itemStyle,
padding: '0.375rem 0 0.375rem 1rem',
});
export const infoStyle = style({
marginLeft: '0.375rem',
});
export const groupMenuStyle = style({
textAlign: 'left',
});
export const kebabToggleStyle = style({
paddingLeft: '0.5rem',
paddingRight: '0.5rem',
});

View File

@ -0,0 +1,32 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { style } from 'typestyle';
import { PFColors } from '../components/Pf/PfColors';
export const healthIndicatorStyle = style({
$nest: {
'& .pf-v5-c-tooltip__content': {
borderWidth: '1px',
textAlign: 'left',
},
'& .pf-v5-c-content ul': {
marginBottom: 'var(--pf-v5-c-content--ul--MarginTop)',
marginTop: 0,
color: PFColors.Color100,
},
},
});

View File

@ -0,0 +1,39 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { style } from 'typestyle';
import { PFColors } from '../components/Pf/PfColors';
export const basicTabStyle = style({
$nest: {
'& .pf-v5-c-tabs__list': {
marginLeft: '20px',
},
'& .pf-v5-c-tab-content': {
overflowY: 'auto',
height: '600px',
},
},
});
export const traceTabStyle = style({
$nest: {
'& .pf-v5-c-tabs__list': {
backgroundColor: PFColors.BackgroundColor100,
borderBottom: `1px solid ${PFColors.BorderColor100}`,
},
},
});

View File

@ -16,7 +16,8 @@
"pluginPackages": [
"@backstage-community/plugin-kiali",
"@backstage-community/plugin-kiali-backend",
"@backstage-community/plugin-kiali-common"
"@backstage-community/plugin-kiali-common",
"@backstage-community/plugin-kiali-react"
]
},
"sideEffects": false,
@ -31,6 +32,7 @@
},
"dependencies": {
"@backstage-community/plugin-kiali-common": "workspace:^",
"@backstage-community/plugin-kiali-react": "workspace:^",
"@backstage/catalog-model": "^1.7.3",
"@backstage/core-components": "^0.17.1",
"@backstage/core-plugin-api": "^1.10.6",
@ -90,6 +92,7 @@
"canvas": "^3.0.0",
"cross-fetch": "4.1.0",
"jest-canvas-mock": "2.5.2",
"msw": "1.3.5",
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "^6.0.0",

View File

@ -21,21 +21,12 @@ import {
GraphType,
TrafficRate,
} from '@backstage-community/plugin-kiali-common/types';
import { TrafficGraph } from '@backstage-community/plugin-kiali-react';
import { Entity } from '@backstage/catalog-model';
import { Content, InfoCard } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { CircularProgress, useTheme } from '@material-ui/core';
import {
action,
createTopologyControlButtons,
defaultControlButtonsOptions,
Model,
TopologyControlBar,
TopologyView,
Visualization,
VisualizationProvider,
VisualizationSurface,
} from '@patternfly/react-topology';
import { Model, Visualization } from '@patternfly/react-topology';
import { default as React, useRef, useState } from 'react';
import { useAsyncFn, useDebounce } from 'react-use';
import { DefaultSecondaryMasthead } from '../../components/DefaultSecondaryMasthead/DefaultSecondaryMasthead';
@ -241,41 +232,7 @@ function TrafficGraphPage(props: { view?: string; entity?: Entity }) {
onRefresh={refresh}
/>
)}
<TopologyView
controlBar={
<TopologyControlBar
controlButtons={createTopologyControlButtons({
...defaultControlButtonsOptions,
zoomInCallback: action(() => {
controller.getGraph().scaleBy(4 / 3);
}),
zoomOutCallback: action(() => {
controller.getGraph().scaleBy(0.75);
}),
fitToScreenCallback: action(() => {
controller.getGraph().fit(80);
}),
resetViewCallback: action(() => {
controller.getGraph().reset();
controller.getGraph().layout();
}),
legend: false,
zoomInAriaLabel: '',
zoomOutAriaLabel: '',
fitToScreenAriaLabel: '',
resetViewAriaLabel: '',
zoomInTip: '',
zoomOutTip: '',
fitToScreenTip: '',
resetViewTip: '',
})}
/>
}
>
<VisualizationProvider controller={controller}>
<VisualizationSurface state={model} />
</VisualizationProvider>
</TopologyView>
<TrafficGraph model={model} />
</>
)}
</Content>

View File

@ -2790,11 +2790,35 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage-community/plugin-kiali-react@workspace:^, @backstage-community/plugin-kiali-react@workspace:plugins/kiali-react":
version: 0.0.0-use.local
resolution: "@backstage-community/plugin-kiali-react@workspace:plugins/kiali-react"
dependencies:
"@backstage-community/plugin-kiali-common": "workspace:^"
"@backstage/cli": "npm:^0.32.0"
"@backstage/core-plugin-api": "npm:^1.10.6"
"@backstage/dev-utils": "npm:^1.1.9"
"@backstage/test-utils": "npm:^1.7.7"
"@material-ui/core": "npm:^4.9.13"
"@patternfly/react-core": "npm:^5.1.1"
"@patternfly/react-icons": "npm:^5.1.1"
"@patternfly/react-topology": "npm:5.4.1"
"@redhat-developer/red-hat-developer-hub-theme": "npm:0.4.0"
"@testing-library/jest-dom": "npm:^6.0.0"
"@testing-library/react": "npm:^14.0.0"
react: "npm:^16.13.1 || ^17.0.0 || ^18.0.0"
typestyle: "npm:^2.4.0"
peerDependencies:
react: ^16.13.1 || ^17.0.0 || ^18.0.0
languageName: unknown
linkType: soft
"@backstage-community/plugin-kiali@workspace:^, @backstage-community/plugin-kiali@workspace:plugins/kiali":
version: 0.0.0-use.local
resolution: "@backstage-community/plugin-kiali@workspace:plugins/kiali"
dependencies:
"@backstage-community/plugin-kiali-common": "workspace:^"
"@backstage-community/plugin-kiali-react": "workspace:^"
"@backstage/catalog-model": "npm:^1.7.3"
"@backstage/cli": "npm:^0.32.0"
"@backstage/core-components": "npm:^0.17.1"
@ -2836,6 +2860,7 @@ __metadata:
lodash: "npm:^4.17.21"
micro-memoize: "npm:4.1.3"
moment: "npm:^2.29.4"
msw: "npm:1.3.5"
prop-types: "npm:^15.8.1"
react: "npm:^16.13.1 || ^17.0.0 || ^18.0.0"
react-ace: "npm:9.5.0"