Meshery UI is the primary user interface for Meshery, built with Next.js and React. It provides visual design capabilities, collaborative GitOps workflows, and infrastructure management through an intuitive web interface.
Architecture Overview
Meshery UI is composed of several key technologies:
Next.js – React framework with server-side rendering and routing
React – Component-based UI library
Material UI (MUI) – Component library for design consistency
Sistent – Meshery’s design system (@sistent/sistent)
Redux Toolkit – Global state management
Relay – GraphQL client for real-time subscriptions
Axios – HTTP client for REST APIs
Playwright – End-to-end testing framework
┌─────────────────────────────────────────────────────────────────────┐
│ Meshery UI (Next.js) │
│ ┌───────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ Material UI │ │ Redux Toolkit│ │ Relay (GraphQL Client) │ │
│ │ Components │ │ State Mgmt │ │ + REST (Axios) │ │
│ └───────────────┘ └──────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Repository Structure
The UI code is located in two directories:
/ui – Main Meshery UI:
ui/
├── components/ # React components
├── pages/ # Next.js pages and routes
├── store/ # Redux store and slices
├── public/ # Static assets
├── styles/ # Global styles
├── utils/ # Utility functions
├── .eslintrc.js # ESLint configuration
└── package.json # Dependencies
/provider-ui – Provider-specific UI extensions:
provider-ui/
├── components/
├── pages/
└── package.json
Development Workflow
Installing Dependencies
Install all UI dependencies:
This installs dependencies for both ui/ and provider-ui/.
Clean install (CI environment):
Running the Development Server
Start Meshery UI (port 3000):
The UI will be available at http://localhost:3000.
The UI expects Meshery Server to be running at http://localhost:9081. Run make server in a separate terminal before starting the UI.
Start Provider UI:
Building for Production
Build all UIs:
This runs linting, builds the UI, and exports static files.
Build only Meshery UI:
Build only Provider UI:
Linting
Lint Meshery UI:
Lint Provider UI:
Fix linting issues automatically:
Code Style and Best Practices
ESLint Configuration
Meshery UI uses ESLint with Prettier for code quality and formatting.
Configuration: ui/.eslintrc.js
module . exports = {
extends: [
'eslint:recommended' ,
'plugin:react/recommended' ,
'next' ,
'plugin:prettier/recommended' ,
],
rules: {
'no-trailing-spaces' : 'error' ,
'brace-style' : [ 'error' , '1tbs' ],
'no-unused-vars' : [ 'error' , { argsIgnorePattern: '^_' }],
'unused-imports/no-unused-imports' : 'error' ,
'prettier/prettier' : [ 'error' , { endOfLine: 'lf' }],
},
};
Component Style
Use functional components with hooks:
import { useState , useEffect } from 'react' ;
function WorkspaceFilter ({ workspaces , onFilterChange }) {
const [ selectedWorkspace , setSelectedWorkspace ] = useState ( '' );
useEffect (() => {
if ( selectedWorkspace ) {
onFilterChange ( selectedWorkspace );
}
}, [ selectedWorkspace , onFilterChange ]);
return (
< select
value = { selectedWorkspace }
onChange = { ( e ) => setSelectedWorkspace ( e . target . value ) }
>
< option value = "" > All Workspaces </ option >
{ workspaces . map (( ws ) => (
< option key = { ws . id } value = { ws . id } >
{ ws . name }
</ option >
)) }
</ select >
);
}
export default WorkspaceFilter ;
Avoid class components:
// ❌ Don't use class components
class WorkspaceFilter extends React . Component {
// ...
}
// ✅ Use functional components
function WorkspaceFilter () {
// ...
}
Styling with Sistent
Prefer Sistent design system components:
import { Button , TextField , Card } from '@sistent/sistent' ;
function CreateWorkspaceForm () {
return (
< Card >
< TextField label = "Workspace Name" variant = "outlined" />
< TextField label = "Description" multiline rows = { 4 } />
< Button variant = "contained" color = "primary" >
Create Workspace
</ Button >
</ Card >
);
}
Fall back to Material UI when Sistent components are unavailable:
import { Tooltip } from '@material-ui/core' ;
import { Button } from '@sistent/sistent' ;
function ActionButton () {
return (
< Tooltip title = "Save changes" >
< Button variant = "contained" > Save </ Button >
</ Tooltip >
);
}
State Management
Use Redux Toolkit for global state:
// store/workspaceSlice.js
import { createSlice } from '@reduxjs/toolkit' ;
const workspaceSlice = createSlice ({
name: 'workspace' ,
initialState: {
selected: null ,
list: [],
},
reducers: {
setWorkspace : ( state , action ) => {
state . selected = action . payload ;
},
setWorkspaceList : ( state , action ) => {
state . list = action . payload ;
},
},
});
export const { setWorkspace , setWorkspaceList } = workspaceSlice . actions ;
export default workspaceSlice . reducer ;
Use hooks to access Redux state:
import { useSelector , useDispatch } from 'react-redux' ;
import { setWorkspace } from '../store/workspaceSlice' ;
function WorkspaceSelector () {
const dispatch = useDispatch ();
const workspaces = useSelector (( state ) => state . workspace . list );
const selected = useSelector (( state ) => state . workspace . selected );
const handleChange = ( workspaceId ) => {
dispatch ( setWorkspace ( workspaceId ));
};
return (
< select value = { selected ?. id } onChange = { ( e ) => handleChange ( e . target . value ) } >
{ workspaces . map (( ws ) => (
< option key = { ws . id } value = { ws . id } > { ws . name } </ option >
)) }
</ select >
);
}
Use local state for component-specific data:
function FormDialog () {
const [ open , setOpen ] = useState ( false );
const [ inputValue , setInputValue ] = useState ( '' );
return (
<>
< Button onClick = { () => setOpen ( true ) } > Open </ Button >
< Dialog open = { open } onClose = { () => setOpen ( false ) } >
{ /* Dialog content */ }
</ Dialog >
</>
);
}
API Integration
GraphQL with Relay:
import { useLazyLoadQuery } from 'react-relay' ;
import graphql from 'babel-plugin-relay/macro' ;
function WorkspaceList () {
const data = useLazyLoadQuery (
graphql `
query WorkspaceListQuery {
workspaces {
id
name
description
owner {
id
name
}
}
}
` ,
{}
);
return (
< ul >
{ data . workspaces . map (( ws ) => (
< li key = { ws . id } >
< strong > { ws . name } </ strong > - { ws . description }
< br />
Owner: { ws . owner . name }
</ li >
)) }
</ ul >
);
}
GraphQL mutations:
import { useMutation } from 'react-relay' ;
import graphql from 'babel-plugin-relay/macro' ;
function CreateWorkspaceButton () {
const [ commit , isInFlight ] = useMutation ( graphql `
mutation CreateWorkspaceMutation($name: String!, $description: String) {
createWorkspace(name: $name, description: $description) {
id
name
}
}
` );
const handleCreate = () => {
commit ({
variables: { name: 'New Workspace' , description: 'Description' },
onCompleted : ( response ) => {
console . log ( 'Workspace created:' , response . createWorkspace );
},
});
};
return (
< Button onClick = { handleCreate } disabled = { isInFlight } >
Create Workspace
</ Button >
);
}
REST API with Axios:
import axios from 'axios' ;
import { useState , useEffect } from 'react' ;
function WorkspaceList () {
const [ workspaces , setWorkspaces ] = useState ([]);
const [ loading , setLoading ] = useState ( true );
useEffect (() => {
const fetchWorkspaces = async () => {
try {
const response = await axios . get ( '/api/workspaces' );
setWorkspaces ( response . data );
} catch ( error ) {
console . error ( 'Failed to fetch workspaces:' , error );
} finally {
setLoading ( false );
}
};
fetchWorkspaces ();
}, []);
if ( loading ) return < div > Loading... </ div > ;
return (
< ul >
{ workspaces . map (( ws ) => (
< li key = { ws . id } > { ws . name } </ li >
)) }
</ ul >
);
}
Routing with Next.js
File-based routing:
pages/
├── index.js # Route: /
├── workspaces/
│ ├── index.js # Route: /workspaces
│ └── [id].js # Route: /workspaces/:id
└── designs.js # Route: /designs
Link between pages:
import Link from 'next/link' ;
function Navigation () {
return (
< nav >
< Link href = "/" > Home </ Link >
< Link href = "/concepts/workspaces" > Workspaces </ Link >
< Link href = "/concepts/designs" > Designs </ Link >
</ nav >
);
}
Programmatic navigation:
import { useRouter } from 'next/router' ;
function WorkspaceCard ({ workspace }) {
const router = useRouter ();
const handleClick = () => {
router . push ( `/workspaces/ ${ workspace . id } ` );
};
return (
< Card onClick = { handleClick } >
< h3 > { workspace . name } </ h3 >
</ Card >
);
}
Access route parameters:
import { useRouter } from 'next/router' ;
function WorkspaceDetail () {
const router = useRouter ();
const { id } = router . query ;
return < div > Workspace ID: { id } </ div > ;
}
Testing
End-to-End Tests with Playwright
Meshery UI uses Playwright for end-to-end testing.
Install Playwright:
This installs Playwright browsers with system dependencies.
Run E2E tests:
make ui-integration-tests
Or directly with npm:
Run tests in CI mode (non-interactive):
Write a Playwright test:
// ui/tests/workspaces.spec.js
import { test , expect } from '@playwright/test' ;
test ( 'create workspace' , async ({ page }) => {
await page . goto ( 'http://localhost:3000/workspaces' );
await page . click ( 'button:has-text("Create Workspace")' );
await page . fill ( 'input[name="name"]' , 'Test Workspace' );
await page . fill ( 'textarea[name="description"]' , 'Test Description' );
await page . click ( 'button:has-text("Save")' );
await expect ( page . locator ( 'text=Test Workspace' )). toBeVisible ();
});
Unit Tests
Unit testing infrastructure for JavaScript is still being expanded. Playwright E2E tests are currently the primary testing method.
Common Tasks
Adding a New Page
Create page file in ui/pages/:
// ui/pages/settings.js
import { useState } from 'react' ;
import { Button , TextField } from '@sistent/sistent' ;
function SettingsPage () {
const [ apiKey , setApiKey ] = useState ( '' );
return (
< div >
< h1 > Settings </ h1 >
< TextField
label = "API Key"
value = { apiKey }
onChange = { ( e ) => setApiKey ( e . target . value ) }
/>
< Button variant = "contained" > Save </ Button >
</ div >
);
}
export default SettingsPage ;
Add navigation link:
< Link href = "/settings" > Settings </ Link >
Adding a Redux Slice
Create slice in ui/store/:
// ui/store/settingsSlice.js
import { createSlice } from '@reduxjs/toolkit' ;
const settingsSlice = createSlice ({
name: 'settings' ,
initialState: {
apiKey: '' ,
theme: 'light' ,
},
reducers: {
setApiKey : ( state , action ) => {
state . apiKey = action . payload ;
},
setTheme : ( state , action ) => {
state . theme = action . payload ;
},
},
});
export const { setApiKey , setTheme } = settingsSlice . actions ;
export default settingsSlice . reducer ;
Add to store configuration:
import { configureStore } from '@reduxjs/toolkit' ;
import settingsReducer from './settingsSlice' ;
const store = configureStore ({
reducer: {
settings: settingsReducer ,
},
});
Adding a Reusable Component
Create component in ui/components/:
// ui/components/WorkspaceCard.js
import { Card , CardContent , Typography } from '@sistent/sistent' ;
function WorkspaceCard ({ workspace , onClick }) {
return (
< Card onClick = { onClick } style = { { cursor: 'pointer' } } >
< CardContent >
< Typography variant = "h5" > { workspace . name } </ Typography >
< Typography variant = "body2" > { workspace . description } </ Typography >
< Typography variant = "caption" > Owner: { workspace . owner } </ Typography >
</ CardContent >
</ Card >
);
}
export default WorkspaceCard ;
Use in pages:
import WorkspaceCard from '../components/WorkspaceCard' ;
function WorkspacesPage ({ workspaces }) {
const handleClick = ( workspace ) => {
console . log ( 'Clicked:' , workspace );
};
return (
< div >
{ workspaces . map (( ws ) => (
< WorkspaceCard
key = { ws . id }
workspace = { ws }
onClick = { () => handleClick ( ws ) }
/>
)) }
</ div >
);
}
Debugging
Use React DevTools and Redux DevTools extensions:
React DevTools – Inspect component tree and props
Redux DevTools – Inspect state and action history
Console Logging
useEffect (() => {
console . log ( 'Workspaces updated:' , workspaces );
}, [ workspaces ]);
Network Inspection
Use browser DevTools Network tab to inspect API requests:
Open DevTools (F12)
Go to Network tab
Filter by XHR to see API calls
Code Splitting
Next.js automatically code-splits pages. For components:
import dynamic from 'next/dynamic' ;
const HeavyComponent = dynamic (() => import ( '../components/HeavyComponent' ), {
loading : () => < div > Loading... </ div > ,
});
Memoization
import { useMemo } from 'react' ;
function WorkspaceList ({ workspaces , filter }) {
const filteredWorkspaces = useMemo (() => {
return workspaces . filter (( ws ) => ws . name . includes ( filter ));
}, [ workspaces , filter ]);
return (
< ul >
{ filteredWorkspaces . map (( ws ) => (
< li key = { ws . id } > { ws . name } </ li >
)) }
</ ul >
);
}
Next Steps
Testing Guide Learn how to test UI changes
Code Style Review JavaScript code style
Server Development Contribute to the backend
CLI Development Contribute to mesheryctl