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
115 changes: 100 additions & 15 deletions packages/extension/src/ui/App.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
--color-success: #4caf50;
--color-error: #f44336;
--color-warning: #ffeb3b;
--color-warning-dark: #ffe600;

--spacing-xs: 4px;
--spacing-sm: 8px;
Expand Down Expand Up @@ -77,7 +78,8 @@ body > div {
transition: background-color var(--transition-speed);
}

select {
select,
.select {
min-width: 120px;
cursor: pointer;
appearance: none;
Expand All @@ -88,15 +90,6 @@ select {
padding-right: var(--spacing-xxl);
}

h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
}

/* Button variants */
.button {
white-space: nowrap;
Expand Down Expand Up @@ -134,7 +127,7 @@ h6 {
.buttonWarning {
composes: button;
background-color: var(--color-warning);
border: none;
border: 1px solid var(--color-warning-dark);
}

.buttonPrimary {
Expand All @@ -153,9 +146,9 @@ h6 {

.buttonGray {
composes: button;
background-color: var(--color-gray-300);
background-color: var(--color-gray-200);
border: 1px solid var(--color-gray-300);
color: var(--color-black);
border: none;
}

.button:hover:not(:disabled) {
Expand Down Expand Up @@ -363,7 +356,16 @@ div + .sent {
margin-bottom: var(--spacing-md);
}

.table table {
.noBorder {
border: none;
}
.table.noBorder {
border-radius: 0;
border-bottom: 1px solid var(--color-gray-300);
}

.table table,
table.table {
width: 100%;
border-collapse: collapse;
}
Expand All @@ -380,6 +382,10 @@ div + .sent {
border-top: 1px solid var(--color-gray-300);
}

.table td.long {
word-break: break-word;
}

.table tr:hover {
background: var(--color-gray-100);
}
Expand Down Expand Up @@ -433,7 +439,6 @@ div + .sent {
margin: var(--spacing-xxl) 0 var(--spacing-md);
}

.dbInspector table td,
.dbInspector table th {
height: var(--input-height);
}
Expand Down Expand Up @@ -500,6 +505,8 @@ div + .sent {
border-radius: var(--border-radius);
resize: vertical;
min-height: 200px;
width: 100%;
margin-bottom: var(--spacing-sm);
}

.configEditorButtons {
Expand All @@ -513,3 +520,81 @@ div + .sent {
gap: var(--spacing-xs);
justify-content: space-between;
}

.noMargin {
margin: 0;
}

/* Accordion styles */
.accordion {
margin-bottom: var(--spacing-md);
border: 1px solid var(--color-gray-300);
border-radius: var(--border-radius);
overflow: hidden;
}

.accordionHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md);
background-color: var(--color-gray-100);
cursor: pointer;
transition: background-color var(--transition-speed);
user-select: none;
font-weight: 600;
border-bottom: 1px solid transparent;
}

.accordion:hover .accordionHeader {
background-color: var(--color-gray-300);
}

.accordionTitle {
display: flex;
align-items: center;
}

.accordionIndicator {
font-size: 18px;
font-weight: bold;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-gray-600);
}

.accordionContent {
padding: var(--spacing-md);
background-color: var(--color-white);
}

.tableContainer {
margin-bottom: var(--spacing-xl);
}

.tableContainer h4 {
margin-top: var(--spacing-lg);
margin-bottom: var(--spacing-sm);
color: var(--color-gray-800);
font-weight: 600;
}

.vatInfo {
margin-bottom: var(--spacing-md);
color: var(--color-gray-600);
font-size: var(--font-size-sm);
}

.vatInfo p {
margin: var(--spacing-md) 0;
}

.vatDetailsHeader {
font-size: var(--font-size-sm);
color: var(--color-gray-600);
font-weight: 400;
margin-left: var(--spacing-xs);
}
2 changes: 1 addition & 1 deletion packages/extension/src/ui/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('App', () => {
} as unknown as StreamState);
const { App } = await import('./App.tsx');
render(<App />);
expect(screen.getByText('Kernel Vats')).toBeInTheDocument();
expect(screen.getByText('Kernel')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Launch Vat' }),
).toBeInTheDocument();
Expand Down
35 changes: 24 additions & 11 deletions packages/extension/src/ui/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import type { NonEmptyArray } from '@metamask/utils';
import { useState } from 'react';

import styles from './App.module.css';
import { ControlPanel } from './components/ControlPanel.tsx';
import { DatabaseInspector } from './components/DatabaseInspector.tsx';
import { MessagePanel } from './components/MessagePanel.tsx';
import { ObjectRegistry } from './components/ObjectRegistry.tsx';
import { Tabs } from './components/Tabs.tsx';
import { VatManager } from './components/VatManager.tsx';
import { PanelProvider } from './context/PanelContext.tsx';
import { useStream } from './hooks/useStream.ts';

const tabs: NonEmptyArray<{
label: string;
value: string;
component: React.ReactNode;
}> = [
{ label: 'Control Panel', value: 'control', component: <ControlPanel /> },
{
label: 'Object Registry',
value: 'registry',
component: <ObjectRegistry />,
},
{
label: 'Database Inspector',
value: 'database',
component: <DatabaseInspector />,
},
];

export const App: React.FC = () => {
const { callKernelMethod, error } = useStream();
const [activeTab, setActiveTab] = useState('vats');
const [activeTab, setActiveTab] = useState(tabs[0].value);

if (error) {
return (
Expand All @@ -34,15 +54,8 @@ export const App: React.FC = () => {
<PanelProvider callKernelMethod={callKernelMethod}>
<div className={styles.panel}>
<div className={styles.leftPanel}>
<Tabs
tabs={[
{ label: 'Vat Manager', value: 'vats' },
{ label: 'Database Inspector', value: 'database' },
]}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
{activeTab === 'vats' ? <VatManager /> : <DatabaseInspector />}
<Tabs tabs={tabs} activeTab={activeTab} onTabChange={setActiveTab} />
{tabs.find((tab) => tab.value === activeTab)?.component}
</div>
<div className={styles.rightPanel}>
<MessagePanel />
Expand Down
30 changes: 23 additions & 7 deletions packages/extension/src/ui/components/ConfigEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ vi.mock('../context/PanelContext.tsx', () => ({
// Mock the CSS module
vi.mock('../App.module.css', () => ({
default: {
configEditor: 'config-editor',
accordion: 'accordion',
accordionHeader: 'accordion-header',
accordionTitle: 'accordion-title',
accordionIndicator: 'accordion-indicator',
accordionContent: 'accordion-content',
configTextarea: 'config-textarea',
configControls: 'config-controls',
select: 'select',
configEditorButtons: 'config-editor-buttons',
buttonPrimary: 'button-primary',
buttonBlack: 'button-black',
Expand All @@ -68,6 +74,7 @@ describe('ConfigEditor Component', () => {
terminateAllVats: vi.fn(),
clearState: vi.fn(),
launchVat: vi.fn(),
collectGarbage: vi.fn(),
});
vi.mocked(usePanelContext).mockReturnValue(mockUsePanelContext);
});
Expand All @@ -81,10 +88,11 @@ describe('ConfigEditor Component', () => {
expect(container).toBeEmptyDOMElement();
});

it('renders the config editor with initial config', () => {
it('renders the config editor with initial config', async () => {
render(<ConfigEditor />);
expect(screen.getByText('Cluster Config')).toBeInTheDocument();
const textarea = screen.getByRole('textbox');
await userEvent.click(screen.getByTestId('config-title'));
const textarea = screen.getByTestId('config-textarea');
expect(textarea).toHaveValue(
JSON.stringify(mockStatus.clusterConfig, null, 2),
);
Expand All @@ -98,14 +106,16 @@ describe('ConfigEditor Component', () => {

it('updates textarea value when user types', async () => {
render(<ConfigEditor />);
const textarea = screen.getByRole('textbox');
await userEvent.click(screen.getByTestId('config-title'));
const textarea = screen.getByTestId('config-textarea');
fireEvent.change(textarea, { target: { value: 'test' } });
expect(textarea).toHaveValue('test');
});

it('updates config when "Update Config" is clicked', async () => {
mockUpdateClusterConfig.mockResolvedValue(undefined);
render(<ConfigEditor />);
await userEvent.click(screen.getByTestId('config-title'));
const updateButton = screen.getByTestId('update-config');
await userEvent.click(updateButton);
expect(mockUpdateClusterConfig).toHaveBeenCalledWith(
Expand All @@ -117,6 +127,7 @@ describe('ConfigEditor Component', () => {
it('updates config and reloads when "Update and Reload" is clicked', async () => {
mockUpdateClusterConfig.mockResolvedValue(undefined);
render(<ConfigEditor />);
await userEvent.click(screen.getByTestId('config-title'));
const updateButton = screen.getByTestId('update-and-restart');
await userEvent.click(updateButton);
expect(mockUpdateClusterConfig).toHaveBeenCalledWith(
Expand All @@ -128,7 +139,8 @@ describe('ConfigEditor Component', () => {
it('logs error when invalid JSON is submitted', async () => {
mockUpdateClusterConfig.mockRejectedValueOnce(new Error('Invalid JSON'));
render(<ConfigEditor />);
const textarea = screen.getByRole('textbox');
await userEvent.click(screen.getByTestId('config-title'));
const textarea = screen.getByTestId('config-textarea');
fireEvent.change(textarea, { target: { value: 'test' } });
const updateButton = screen.getByTestId('update-config');
await userEvent.click(updateButton);
Expand All @@ -143,6 +155,7 @@ describe('ConfigEditor Component', () => {
const error = new Error('Update failed');
mockUpdateClusterConfig.mockRejectedValueOnce(error);
render(<ConfigEditor />);
await userEvent.click(screen.getByTestId('config-title'));
await userEvent.click(screen.getByTestId('update-config'));

await waitFor(() => {
Expand All @@ -152,7 +165,8 @@ describe('ConfigEditor Component', () => {

it('updates textarea when status changes', async () => {
const { rerender } = render(<ConfigEditor />);
const textarea = screen.getByRole('textbox');
await userEvent.click(screen.getByTestId('config-title'));
const textarea = screen.getByTestId('config-textarea');
expect(textarea).toHaveValue(
JSON.stringify(mockStatus.clusterConfig, null, 2),
);
Expand All @@ -179,15 +193,17 @@ describe('ConfigEditor Component', () => {
});
});

it('renders the config template selector with default option selected', () => {
it('renders the config template selector with default option selected', async () => {
render(<ConfigEditor />);
await userEvent.click(screen.getByTestId('config-title'));
const selector = screen.getByTestId('config-select');
expect(selector).toBeInTheDocument();
expect(selector).toHaveValue('Default');
});

it('updates textarea when selecting a different template', async () => {
render(<ConfigEditor />);
await userEvent.click(screen.getByTestId('config-title'));
const selector = screen.getByTestId('config-select');
const textarea = screen.getByTestId('config-textarea');
expect(textarea).toHaveValue(JSON.stringify(defaultClusterConfig, null, 2));
Expand Down
Loading
Loading