@vention/machine-apps-components
v0.4.0
Published
Reusable components for machine applications.
Keywords
Readme
Machine Apps Components
Reusable components for machine applications.
Components
NavigationBar
A navigation bar component with support for:
- Multiple navigation items with icons
- Control Center button
- Support button
- Optional timer display
Props
interface NavigationItem {
label: string
path: string
icon: ReactNode
onClick?: () => void // Optional: Custom click handler (overrides default navigation)
}
interface NavigationBarProps {
navigationItems: NavigationItem[]
showTimer?: boolean // Default: true
}Example Usage
Basic Usage
import { NavigationBar } from "@vention/machine-apps-components"
import { VentionIcon } from "@ventionco/machine-ui"
const NAV_ITEMS = [
{
label: "Operation",
path: "/operation",
icon: <VentionIcon size={32} type="category-filled" color="white" />,
},
{
label: "Settings",
path: "/settings",
icon: <VentionIcon size={32} type="tool-1" color="white" />,
},
]
// With timer (default)
<NavigationBar navigationItems={NAV_ITEMS} />
// Without timer
<NavigationBar navigationItems={NAV_ITEMS} showTimer={false} />Custom Click Handlers
You can override the default navigation behavior by providing custom click handlers:
// Custom handler for navigation items
const NAV_ITEMS = [
{
label: "Settings",
path: "/settings",
icon: <VentionIcon size={32} type="tool-1" color="white" />,
onClick: () => {
// Show confirmation before navigating
if (confirm("You have unsaved changes. Continue?")) {
navigate("/settings")
}
},
},
{
label: "Logs",
path: "/logs",
icon: <VentionIcon size={32} type="alarm-bell-filled" color="white" />,
onClick: () => {
// Do something before navigating
localStorage.setItem("lastView", "operation")
navigate("/logs")
},
},
]Note: When a custom onClick handler is provided, it completely overrides the default navigation behavior. The handler is responsible for any navigation logic if needed.
StatusTopBar
A status bar component positioned at the top of the screen that displays:
- Status indicator with label and colored dot
- Configurable action buttons with visibility and disabled states
- Custom styling for buttons
Key Features:
- ✅ Declarative, config-based API
- ✅ Fully controlled component (no internal state)
- ✅ Type-safe button configurations
- ✅ Easy to test and reason about
Props
interface ButtonConfig {
id: string // Unique identifier for the button
label: string // Button text
onClick?: () => void // Click handler
visible?: boolean // Show/hide button (default: true)
disabled?: boolean // Enable/disable button (default: false)
backgroundColor?: string // Button background color (e.g., "#4CAF50")
backgroundColorHover?: string // Hover background color
borderColor?: string // Border color
textColor?: string // Text color
width?: number // Button width in pixels (default: 208)
height?: number // Button height in pixels (default: 80)
icon?: ReactNode // Icon to display when enabled
iconDisabled?: ReactNode // Icon to display when disabled
}
interface StatusTopBarProps {
status?: {
label: string // Status label text
color: string // Color of the status dot
}
buttonConfigs?: ButtonConfig[] // Array of button configurations
}Example Usage
Basic Usage
import { StatusTopBar, ButtonConfig } from "@vention/machine-apps-components"
import { VentionIcon } from "@ventionco/machine-ui"
function App() {
const buttonConfigs: ButtonConfig[] = [
{
id: "start",
label: "Start",
onClick: () => console.log("Start clicked"),
backgroundColor: "#2196F3",
textColor: "white",
icon: <VentionIcon size={32} type="player-play-filled" color="white" />,
visible: true,
disabled: false,
},
{
id: "stop",
label: "Stop",
onClick: () => console.log("Stop clicked"),
backgroundColor: "#F44336",
textColor: "white",
icon: <VentionIcon size={32} type="player-stop-filled" color="white" />,
visible: false, // Hidden by default
disabled: false,
},
]
return (
<StatusTopBar
status={{
label: "Machine state: Ready",
color: "#4CAF50",
}}
buttonConfigs={buttonConfigs}
/>
)
}Dynamic State Management
The component is fully controlled - all button states are derived from your application state:
import { StatusTopBar, ButtonConfig } from "@vention/machine-apps-components"
import { useMemo } from "react"
function MachineApp() {
const { machineState, isRunning, canStart } = useMachineState()
const buttonConfigs: ButtonConfig[] = useMemo(() => {
const configs: ButtonConfig[] = []
// Start button - visible when not running
if (!isRunning) {
configs.push({
id: "start",
label: "Start",
onClick: handleStart,
backgroundColor: "#2196F3",
textColor: "white",
visible: true,
disabled: !canStart, // Disabled if prerequisites not met
})
}
// Stop button - visible when running
if (isRunning) {
configs.push({
id: "stop",
label: "Stop",
onClick: handleStop,
backgroundColor: "#F44336",
textColor: "white",
visible: true,
disabled: false,
})
}
// Home button - always visible
configs.push({
id: "home",
label: "Home",
onClick: handleHome,
borderColor: "#E2E8F0",
textColor: "#1A202C",
visible: true,
disabled: isRunning, // Can't home while running
})
return configs
}, [isRunning, canStart])
return (
<StatusTopBar
status={{
label: `Machine state: ${machineState}`,
color: getStatusColor(machineState),
}}
buttonConfigs={buttonConfigs}
/>
)
}Conditional Buttons
Easily show/hide buttons based on application state:
const buttonConfigs: ButtonConfig[] = useMemo(() => {
const configs: ButtonConfig[] = []
// Operation page buttons
if (currentPage === "operation") {
configs.push({
id: "start",
label: isRunning ? "Stop" : "Start",
onClick: isRunning ? handleStop : handleStart,
backgroundColor: isRunning ? "#F44336" : "#2196F3",
textColor: "white",
visible: true,
disabled: false,
})
}
// Calibration page buttons
if (currentPage === "calibration") {
configs.push({
id: "freedrive",
label: isFreeDriveEnabled ? "Disable Free Drive" : "Enable Free Drive",
onClick: toggleFreeDrive,
backgroundColor: isFreeDriveEnabled ? "#CBD5E0" : undefined,
borderColor: !isFreeDriveEnabled ? "#E2E8F0" : undefined,
textColor: "black",
visible: true,
disabled: false,
})
}
return configs
}, [currentPage, isRunning, isFreeDriveEnabled])Migration from Imperative API
If you're migrating from the old compound component pattern:
Before (compound components):
<StatusTopBar statusLabel="Ready" dotColor="#4CAF50" visibleButtons={["start"]} disabledButtons={["stop"]}>
<StatusTopBar.Button id="start" label="Start" onClick={handleStart} backgroundColor="#2196F3" textColor="white" />
<StatusTopBar.Button id="stop" label="Stop" onClick={handleStop} backgroundColor="#F44336" textColor="white" />
</StatusTopBar>After (config-based):
<StatusTopBar
status={{ label: "Ready", color: "#4CAF50" }}
buttonConfigs={[
{ id: "start", label: "Start", onClick: handleStart, backgroundColor: "#2196F3", textColor: "white", visible: true },
{ id: "stop", label: "Stop", onClick: handleStop, backgroundColor: "#F44336", textColor: "white", visible: false },
]}
/>Benefits of the new API:
- Single source of truth for button state
- No need for refs or imperative methods
- Easier to test (just pass different configs)
- Better TypeScript support
- More predictable behavior
Logs
Reusable logs components for filtering, sorting, and displaying machine logs.
Exports
import { LogsPanel, LogsTable, LogFilterForm, LogsPagination } from "@vention/machine-apps-components"
import type {
LogEntry,
LogType,
LogFilterFormValues,
SortOrder,
LogsPanelHandle,
PaginationConfig,
PaginationMode,
FetchParams,
FetchResult,
} from "@vention/machine-apps-components"Types
type LogType = "error" | "warning" | "info"
interface LogEntry {
id: string
date: string // ISO or locale string parsable by Date
level: LogType
code: string
message: string
description: string
}
type SortOrder = "latest" | "oldest"
interface LogFilterFormValues {
fromDate?: string // YYYY-MM-DD
toDate?: string // YYYY-MM-DD
logType?: LogType
sortOrder: SortOrder
}
type PaginationMode = "pagination" | "infinite-scroll" | "none"
interface PaginationConfig {
mode: PaginationMode
pageSize?: number // Default: 10
initialPage?: number // Default: 1
}
// Parameters passed to the dataFetcher
interface FetchParams {
filters: LogFilterFormValues // Current filter state
page: number // Current page number
pageSize: number // Items per page
}
// Expected return type from dataFetcher
interface FetchResult {
logs: LogEntry[] // The log entries for this page
totalCount: number // Total number of logs (after filtering)
totalPages: number // Total number of pages
currentPage: number // Current page number
hasMore: boolean // Whether there are more pages available
}
// Type definition for data fetcher
type DataFetcher = (params: FetchParams) => Promise<FetchResult>
interface LogsPanelHandle {
refresh: () => Promise<void>
resetFilters: () => void
applyFilters: (filters: Partial<LogFilterFormValues>) => void
getCurrentFilters: () => LogFilterFormValues
exportLogs: () => LogEntry[]
}LogsPanel
A composite component that combines filter UI and table display with loading states and error handling. The component is a pure presentation component - it displays data and manages UI state, while delegating all data operations (filtering, sorting, pagination) to your dataFetcher function.
Features:
- 🔄 Automatic loading state with spinner when fetching data
- ❌ Built-in error display with VentionAlert
- 🔍 Filter UI for date range and log type
- ↕️ Sort UI for latest/oldest
- 📅 Date formatting (YYYY-MM-DD h:mm:ssa)
- 🎨 Type-based icons (error, warning, info)
- 📄 Pagination support (standard pagination or infinite scroll)
- 🎯 Imperative API for programmatic control
- 🔔 Event callbacks for integration
- 🎨 Customizable styling and empty states
- ⚡ Performance optimized with React.memo
How It Works
The component calls your dataFetcher function whenever filters, sort order, or page changes. You implement the filtering, sorting, and pagination logic, and return the results:
import { LogsPanel } from "@vention/machine-apps-components"
import type { FetchParams, FetchResult } from "@vention/machine-apps-components"
const fetchLogs = async (params: FetchParams): Promise<FetchResult> => {
// params contains:
// - params.filters.fromDate
// - params.filters.toDate
// - params.filters.logType
// - params.filters.sortOrder
// - params.page
// - params.pageSize
const response = await fetch(
`/api/logs?${new URLSearchParams({
fromDate: params.filters.fromDate || "",
toDate: params.filters.toDate || "",
logType: params.filters.logType || "",
sortOrder: params.filters.sortOrder,
page: params.page.toString(),
pageSize: params.pageSize.toString(),
})}`
)
const data = await response.json()
return {
logs: data.logs,
totalCount: data.total,
totalPages: Math.ceil(data.total / params.pageSize),
currentPage: params.page,
hasMore: params.page < Math.ceil(data.total / params.pageSize),
}
}
;<LogsPanel
dataFetcher={fetchLogs}
pagination={{
mode: "pagination",
pageSize: 20,
}}
/>Important: When using dataFetcher, you are responsible for:
- ✅ Filtering logs by date range and type
- ✅ Sorting logs by date (latest/oldest)
- ✅ Paginating the results
- ✅ Returning pagination metadata (totalCount, totalPages, hasMore)
The component handles:
- ✅ Rendering the filter UI
- ✅ Managing filter state
- ✅ Calling your
dataFetcherwhen filters/page changes - ✅ Loading and error states
- ✅ Displaying the results
Complete Client-Side Example
Here's a complete example showing how to implement filtering, sorting, and pagination on the client-side:
import { useCallback, useMemo } from "react"
import { LogsPanel } from "@vention/machine-apps-components"
import type { FetchParams, FetchResult, LogEntry } from "@vention/machine-apps-components"
import dayjs from "dayjs"
function LogsPage() {
// Your data source (could come from props, context, etc.)
const allLogs: LogEntry[] = useMemo(
() => [
{ id: "1", date: "2025-10-07T14:00:00Z", type: "error", code: "ERR_001", message: "Error", description: "..." },
{ id: "2", date: "2025-10-07T13:00:00Z", type: "warning", code: "WARN_001", message: "Warning", description: "..." },
{ id: "3", date: "2025-10-07T12:00:00Z", type: "info", code: "INFO_001", message: "Info", description: "..." },
// ... more logs
],
[]
)
const fetchLogs = useCallback(
async (params: FetchParams): Promise<FetchResult> => {
// Simulate network delay (optional)
await new Promise(resolve => setTimeout(resolve, 500))
// 1. Apply filtering
let filtered = [...allLogs]
// Filter by date range
if (params.filters.fromDate) {
const fromTimestamp = dayjs(params.filters.fromDate).valueOf()
filtered = filtered.filter(log => dayjs(log.date).valueOf() >= fromTimestamp)
}
if (params.filters.toDate) {
const toTimestamp = dayjs(params.filters.toDate).endOf("day").valueOf()
filtered = filtered.filter(log => dayjs(log.date).valueOf() <= toTimestamp)
}
// Filter by log type
if (params.filters.logType) {
filtered = filtered.filter(log => log.type === params.filters.logType)
}
// 2. Apply sorting
filtered.sort((a, b) => {
const aTime = dayjs(a.date).valueOf()
const bTime = dayjs(b.date).valueOf()
return params.filters.sortOrder === "latest" ? bTime - aTime : aTime - bTime
})
// 3. Apply pagination
const totalCount = filtered.length
const totalPages = Math.ceil(totalCount / params.pageSize)
const start = (params.page - 1) * params.pageSize
const end = start + params.pageSize
const paginated = filtered.slice(start, end)
// 4. Return result
return {
logs: paginated,
totalCount,
totalPages,
currentPage: params.page,
hasMore: end < totalCount,
}
},
[allLogs]
)
return (
<LogsPanel
dataFetcher={fetchLogs}
pagination={{
mode: "pagination",
pageSize: 10,
}}
/>
)
}Props
interface LogsPanelProps {
// Required - Data fetcher function
dataFetcher: (params: FetchParams) => Promise<FetchResult>
// Optional - Initial state
initialFilters?: Partial<LogFilterFormValues>
// Optional - Error handling
onError?: (error: unknown) => void
// Optional - Pagination
pagination?: PaginationConfig
// Optional - Event callbacks
onFilterChange?: (filters: LogFilterFormValues) => void
onLogClick?: (log: LogEntry) => void
// Optional - Customization
className?: string
tableHeight?: string | number
emptyStateMessage?: string
emptyStateIcon?: ReactNode
}States
- Loading: Shows VentionSpinner with "Loading logs..." message
- Error: Displays VentionAlert with error title and description
- Empty: Shows customizable empty state message
- Success: Renders filterable/sortable table with data
Initial Filters
<LogsPanel
data={logs}
initialFilters={{
fromDate: "2025-09-01",
toDate: "2025-09-30",
logType: "error",
sortOrder: "latest",
}}
/>Pagination
// Standard pagination (default: 10 items per page)
<LogsPanel
data={logs}
pagination={{
mode: "pagination",
pageSize: 20,
initialPage: 1,
}}
/>
// Infinite scroll (loads more as you scroll)
<LogsPanel
data={logs}
pagination={{
mode: "infinite-scroll",
pageSize: 10,
}}
/>
// No pagination (show all logs)
<LogsPanel
data={logs}
pagination={{ mode: "none" }}
/>
// Default behavior (no pagination prop = show all logs)
<LogsPanel data={logs} />Event Callbacks
Get notified when user interacts with the component:
import { LogsPanel } from "@vention/machine-apps-components"
import type { LogEntry, LogFilterFormValues } from "@vention/machine-apps-components"
;<LogsPanel
data={logs}
onFilterChange={(filters: LogFilterFormValues) => {
console.log("User changed filters:", filters)
// Track analytics, sync to URL params, etc.
}}
onLogClick={(log: LogEntry) => {
console.log("User clicked log:", log)
// Show details modal, navigate to details page, etc.
}}
/>Imperative API
Control the component programmatically using a ref:
import { useRef } from "react"
import { LogsPanel } from "@vention/machine-apps-components"
import type { LogsPanelHandle } from "@vention/machine-apps-components"
function MyComponent() {
const logsPanelRef = useRef<LogsPanelHandle>(null)
const handleRefresh = async () => {
// Manually refresh async data
await logsPanelRef.current?.refresh()
}
const handleResetFilters = () => {
// Reset all filters to default
logsPanelRef.current?.resetFilters()
}
const handleShowErrors = () => {
// Programmatically apply filters
logsPanelRef.current?.applyFilters({ logType: "error" })
}
const handleExport = () => {
// Get current filtered/sorted logs
const logs = logsPanelRef.current?.exportLogs()
if (logs) {
// Export to CSV, JSON, etc.
downloadAsCSV(logs)
}
}
const handleGetFilters = () => {
// Get current filter state
const filters = logsPanelRef.current?.getCurrentFilters()
console.log("Current filters:", filters)
}
return (
<>
<div>
<button onClick={handleRefresh}>Refresh</button>
<button onClick={handleResetFilters}>Reset Filters</button>
<button onClick={handleShowErrors}>Show Errors Only</button>
<button onClick={handleExport}>Export Logs</button>
</div>
<LogsPanel ref={logsPanelRef} data={fetchLogs} />
</>
)
}Available Methods:
refresh()- Manually re-fetch data with current filters/pageresetFilters()- Reset all filters to their default valuesapplyFilters(filters)- Programmatically set filtersgetCurrentFilters()- Get the current filter stateexportLogs()- Get currently displayed logs array
Customization
Customize the appearance and behavior:
import { LogsPanel } from "@vention/machine-apps-components"
import { VentionIcon } from "@ventionco/machine-ui"
;<LogsPanel
data={logs}
className="my-custom-logs-panel"
tableHeight="600px"
emptyStateMessage="No logs match your filters"
emptyStateIcon={<VentionIcon type="inbox" size={48} color="gray" />}
/>Customization Props:
className- Add custom CSS class to the root containertableHeight- Set custom table height (e.g., "500px", 600)emptyStateMessage- Custom message when no logs (default: "You have no logs")emptyStateIcon- Custom React node to display above empty message
LogsTable
Just the table component. Useful if you want to handle filtering/sorting yourself or compose your own custom layout with filters.
import { LogsTable } from "@vention/machine-apps-components"
import type { LogEntry } from "@vention/machine-apps-components"
<LogsTable
logs={logs}
onLogClick={log => showDetailsModal(log)}
tableHeight="500px"
emptyStateMessage="No logs available"
/>Props:
interface LogsTableProps {
logs?: LogEntry[]
isLoading?: boolean
error?: string | null
onLoadMoreLogs?: () => void // For infinite scroll
hasMoreLogs?: boolean // For infinite scroll
onLogClick?: (log: LogEntry) => void
tableHeight?: string | number
emptyStateMessage?: string
emptyStateIcon?: ReactNode
}LogFilterForm
Just the filter UI. Use this when you want full control over the filtering logic.
import { LogFilterForm } from "@vention/machine-apps-components"
import type { LogFilterFormValues } from "@vention/machine-apps-components"
const handleFilterChange = (filters: LogFilterFormValues) => {
// Apply filters to your own data source
const filtered = myLogs.filter(log => {
// Your custom filter logic
})
}
const handleReset = () => {
// Handle reset
}
;<LogFilterForm onFilterChange={handleFilterChange} onReset={handleReset} initialFilters={{ logType: "error" }} />LogsPagination
Just the pagination controls. Use this for custom pagination implementations.
import { LogsPagination } from "@vention/machine-apps-components"
;<LogsPagination currentPage={currentPage} totalPages={totalPages} onPageChange={page => setCurrentPage(page)} />Development
Running Tests
# Run tests
nx test machine-apps-components
# Run tests with coverage
nx test machine-apps-components --coverage
# Run tests in watch mode
cd projects/machine-code/libs/machine-apps-components
npx vitestTesting Setup
This library uses Vitest with jsdom environment for testing React components. Test utilities are provided in src/test-utils.tsx that wrap components with necessary providers:
ThemeProviderwithmachineUiThemeMemoryRouterfor routing
Example test:
import { renderWithProviders } from "../../test-utils"
import { NavigationBar } from "./navigation-bar"
it("should render correctly", () => {
renderWithProviders(<NavigationBar navigationItems={mockNavigationItems} />)
expect(screen.getByText("Operation")).toBeDefined()
})Building
nx build machine-apps-componentsLinting
nx lint machine-apps-components