Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .eslintignore

This file was deleted.

42 changes: 0 additions & 42 deletions .eslintrc.yml

This file was deleted.

16 changes: 16 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# .gitignore:
node_modules/
dist/
cypress/videos
cypress/screenshots
.DS_Store
.env

#legacy
i18n-scripts/
#generated
package-lock.json
locales/
# YAML files
*.yaml
*.yml
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ src/
### Plugin extension registration

Each view has a `manifest.ts` that exports:

- `{View}ExposedModules` — maps module names to file paths for code splitting
- `{View}Extensions` — array of `EncodedExtension` objects defining console nav items, pages, and templates

Expand All @@ -52,6 +53,7 @@ Models live in `src/console-models/` as individual files exporting `K8sModel` ob
### Path aliases

Defined in `tsconfig.json`:

- `@images/*` → `images/*`
- `@utils/*` → `src/utils/*`
- `@models` → `src/console-models/index.ts`
Expand All @@ -61,6 +63,7 @@ Defined in `tsconfig.json`:
For coding standards (component rules, PatternFly usage, i18n, linting), see [CONTRIBUTING.md](CONTRIBUTING.md#coding-standards).

Key details for code generation:

- Translation keys use the `plugin__nmstate-console-plugin~` prefix
- In manifest files, use `%plugin__nmstate-console-plugin~Label%` syntax for nav item names
- Shared hooks (used in 2+ components) live in `src/utils/hooks/`; single-use hooks must be co-located with the component that uses them — do not add to shared utils
Expand All @@ -77,12 +80,14 @@ Key details for code generation:
For full coding standards, linting rules, testing, and PR process, see [CONTRIBUTING.md](CONTRIBUTING.md).

### File & directory naming

- React components: `PascalCase.tsx`
- Utilities, hooks, constants: `camelCase.ts`
- Directories for components use PascalCase, directories for hooks use camelCase, all other directories use kebab-case
- Each view directory has a `manifest.ts`, `constants.ts`, and optionally `utils.ts`

### Deployment

- Webpack builds the plugin as a dynamic console remote module
- Deployed via Helm chart (`deployment/nmstate-console-plugin/`) or OpenShift templates (`oc-manifest.yaml`)
- Container images published to `quay.io/nmstate/nmstate-console-plugin`
Expand Down
33 changes: 18 additions & 15 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ plugin-manifest.ts
```

Each manifest exports:

- **ExposedModules** — maps module names to file paths for webpack code splitting
- **Extensions** — typed `EncodedExtension` objects that register console navigation items, resource list/detail pages, YAML templates, and route pages

Expand All @@ -73,12 +74,12 @@ src/views/{view}/
└── hooks/ # View-specific React hooks
```

| View | K8s Resource | Extension Type | Perspectives |
|------|-------------|---------------|-------------|
| **policies** | `NodeNetworkConfigurationPolicy` | Resource list + details + YAML template | admin, virtualization |
| **states** | `NodeNetworkState` | Resource list | admin |
| **physical-networks** | (aggregated from enactments + states) | Href route page | admin, virtualization |
| **nodenetworkconfiguration** | (aggregated from nodes + states + enactments) | Href route page | admin, virtualization |
| View | K8s Resource | Extension Type | Perspectives |
| ---------------------------- | --------------------------------------------- | --------------------------------------- | --------------------- |
| **policies** | `NodeNetworkConfigurationPolicy` | Resource list + details + YAML template | admin, virtualization |
| **states** | `NodeNetworkState` | Resource list | admin |
| **physical-networks** | (aggregated from enactments + states) | Href route page | admin, virtualization |
| **nodenetworkconfiguration** | (aggregated from nodes + states + enactments) | Href route page | admin, virtualization |

**Resource-based views** (policies, states) use `console.page/resource/list` and `console.page/resource/details` extensions, which automatically bind to the K8s model and provide standard console features (filtering, pagination, YAML editor).

Expand All @@ -104,6 +105,7 @@ src/utils/
```

Resource utilities follow a consistent pattern:

- **selectors** (`selectors.ts`) — extract and transform fields from K8s resource objects
- **getters** (`getters.ts`) — older naming convention for the same purpose; some resource directories still use `getters.ts` (e.g., `nns/`, `interfaces/`) while others have migrated to `selectors.ts` (e.g., `enactments/`). New code should use the `selectors` naming
- **utils** (`utils.ts`) — transform or compute derived data
Expand All @@ -128,8 +130,9 @@ src/nmstate-types/ # NMState CRD schemas and custom types
```

Models serve two purposes:

1. **SDK integration** — `K8sModel` objects tell the console how to watch, list, and navigate to resources
2. **Type safety** — NMState API types from `@kubevirt-ui/kubevirt-api/nmstate` provide TypeScript interfaces for resource objects
2. **Type safety** — NMState API types from `@kubevirt-ui-ext/kubevirt-api/nmstate` provide TypeScript interfaces for resource objects

## Data Flow

Expand Down Expand Up @@ -195,11 +198,11 @@ Each release branch pins its SDK version to match (e.g., `release-4.22` uses `@o

## Key Dependencies

| Package | Purpose |
|---------|---------|
| `@openshift-console/dynamic-plugin-sdk` | Console integration: K8s watches, navigation, pages |
| `@openshift-console/dynamic-plugin-sdk-webpack` | Webpack plugin for building console remote modules |
| `@patternfly/react-core` | UI components (tables, forms, modals, alerts) |
| `@patternfly/react-topology` | Topology graph visualization |
| `@kubevirt-ui/kubevirt-api` | TypeScript types for NMState and K8s resources |
| `react-i18next` | Internationalization framework |
| Package | Purpose |
| ----------------------------------------------- | --------------------------------------------------- |
| `@openshift-console/dynamic-plugin-sdk` | Console integration: K8s watches, navigation, pages |
| `@openshift-console/dynamic-plugin-sdk-webpack` | Webpack plugin for building console remote modules |
| `@patternfly/react-core` | UI components (tables, forms, modals, alerts) |
| `@patternfly/react-topology` | Topology graph visualization |
| `@kubevirt-ui-ext/kubevirt-api` | TypeScript types for NMState and K8s resources |
| `react-i18next` | Internationalization framework |
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Read these files for full context before working on this codebase:
**Views:** `src/views/policies/`, `src/views/states/`, `src/views/physical-networks/`, `src/views/nodenetworkconfiguration/`

**Commands:**

- `npm run start-console` — run dev server
- `npm run lint` / `npm run lint:fix` — lint
- `npm test` — Jest unit tests
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ npm run lint:fix # Auto-fix lint errors
```

Key rules:

- **Prettier:** single quotes, trailing commas, 100-char print width
- **Import sorting:** enforced via `simple-import-sort` — React first, then external packages, then internal paths, then relative imports, then CSS
- **No unused variables:** `@typescript-eslint/no-unused-vars` is set to `error`
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ npm run dev -- --port 9443 \

#### Requirements

| Requirement | Description |
| --------------------------------------------------- | ------------------------------------------------ |
| [Node.js](https://nodejs.org/) | JavaScript runtime environment |
| [npm](https://www.npmjs.com/) | Package manager for Node.js (included with Node.js) |
| [Kubernetes](https://kubernetes.io/) | An [OpenShift](https://www.openshift.com/) or Kubernetes cluster |
| [kubectl](https://kubernetes.io/docs/tasks/tools/) | The Kubernetes command-line tool |
| Requirement | Description |
| -------------------------------------------------- | ---------------------------------------------------------------- |
| [Node.js](https://nodejs.org/) | JavaScript runtime environment |
| [npm](https://www.npmjs.com/) | Package manager for Node.js (included with Node.js) |
| [Kubernetes](https://kubernetes.io/) | An [OpenShift](https://www.openshift.com/) or Kubernetes cluster |
| [kubectl](https://kubernetes.io/docs/tasks/tools/) | The Kubernetes command-line tool |

#### Setting up a local cluster

Expand Down
18 changes: 9 additions & 9 deletions cypress/e2e/NewPolicy.spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const deletePolicyFromDetailsPage = (policyName: string) => {

cy.contains('button[disabled]', 'Delete');

cy.get('input#text-confirmation').clear().type(`${policyName}`);
cy.get('input#text-confirmation').clear();
cy.get('input#text-confirmation').type(`${policyName}`);

cy.contains('button', 'Delete').click();

Expand All @@ -26,17 +27,16 @@ describe('Create new policy with form', () => {
cy.contains('button', 'From Form').click();

// Network identity step
cy.wait(1000);
cy.get('input[name="physical-network-name"]').clear().type(`${testPolicyName}`);
cy.get('input[name="physical-network-name"]').should('be.visible').clear();
cy.get('input[name="physical-network-name"]').type(`${testPolicyName}`);
cy.contains('button', 'Next').click();

// Nodes configuration step
cy.get('input[name="policy-name"]')
.should('be.visible')
.click()
.type('{selectall}{backspace}')
.type(testPolicyName);
cy.get('input[name="policy-description"]').clear().type(`${testDescription}`);
cy.get('input[name="policy-name"]').should('be.visible').click();
cy.get('input[name="policy-name"]').type('{selectall}{backspace}');
cy.get('input[name="policy-name"]').type(testPolicyName);
cy.get('input[name="policy-description"]').clear();
cy.get('input[name="policy-description"]').type(`${testDescription}`);
cy.contains('button', 'Next').click();

// Uplink connection step - 'Default node network' selected by default
Expand Down
2 changes: 0 additions & 2 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-namespace */
/// <reference types="cypress" />
import { KUBEADMIN_IDP, KUBEADMIN_USERNAME } from './constants';
import { ConsoleWindowType } from './types';
Expand Down Expand Up @@ -46,7 +45,6 @@ const dismissWelcomeTourIfPresent = () => {
if (Date.now() >= deadline) {
return;
}

// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(TOUR_POLL_MS).then(tryDismiss);
});
Expand Down
121 changes: 121 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import prettierConfig from 'eslint-config-prettier';
import cypressPlugin from 'eslint-plugin-cypress';
import importPlugin from 'eslint-plugin-import';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import globals from 'globals';
import tseslint from 'typescript-eslint';

import js from '@eslint/js';

export default tseslint.config(
{
ignores: [
// .gitignore:
'**/node_modules/',
'**/dist/',
'cypress/videos',
'cypress/screenshots',
'**/.DS_Store',
'.env',
// legacy
'i18n-scripts/',
// generated
'package-lock.json',
'locales/',
],
},

js.configs.recommended,
...tseslint.configs.recommended,
reactPlugin.configs.flat.recommended,

{
languageOptions: {
globals: {
...globals.browser,
...globals.es2021,
},
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
plugins: {
import: importPlugin,
'react-hooks': reactHooksPlugin,
'simple-import-sort': simpleImportSort,
},
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-shadow': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^_' }],

'import/no-cycle': 'off',
'import/no-duplicates': 'error',

'no-console': 'error',
'no-nested-ternary': 'error',

'react-hooks/exhaustive-deps': 'warn',
'react-hooks/rules-of-hooks': 'error',
'react/display-name': 'off',
'react/prop-types': 'off',

'simple-import-sort/exports': 'error',
'simple-import-sort/imports': [
'warn',
{
groups: [
[
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)',
],
['^react', '^\\w'],
['^(@|config/)(/*|$)'],
['^\\u0000'],
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
['^.+\\.s?css$'],
],
},
],
},
settings: {
react: { version: 'detect' },
},
},

{
files: ['**/*.{js,cjs}', 'webpack.config.ts', 'jest.config.ts', 'cypress.config.ts'],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
},

{
files: ['cypress/**/*.{js,ts,jsx,tsx}'],
...cypressPlugin.configs.recommended,
languageOptions: {
globals: {
...globals.node,
...cypressPlugin.configs.globals.languageOptions.globals,
},
},
rules: {
...cypressPlugin.configs.recommended.rules,
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-require-imports': 'off',
'no-console': 'off',
'no-redeclare': 'off',
},
},

// last to disable conflicting rules
prettierConfig,
);
2 changes: 0 additions & 2 deletions i18next-parser.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef
const { CustomJSONLexer } = require('./i18n-scripts/lexers');

// eslint-disable-next-line no-undef
module.exports = {
createOldCatalogs: false,
defaultNamespace: 'plugin__nmstate-console-plugin',
Expand Down
Loading
Loading