Skip to content

Commit

Permalink
fead: Add Settings Page for Dynamic Settings (#1789)
Browse files Browse the repository at this point in the history
Introduced a new settings page for managing dynamic settings with
backend API routes and frontend components.

- Error handling in API routes.
- State management for editing in UI.
- Updated database indexing for efficient querying.


https://www.loom.com/share/ce7541a92538431f8c55b3ffcf712eea?sid=04cf9b72-6631-49be-9073-6ef8161432d4
  • Loading branch information
iskakaushik authored Jun 5, 2024
1 parent 2459629 commit 32a395e
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 15 deletions.
47 changes: 47 additions & 0 deletions ui/app/api/settings/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import prisma from '@/app/utils/prisma';
import { dynamic_settings } from '@prisma/client';

export async function GET() {
try {
const configs: dynamic_settings[] =
await prisma.dynamic_settings.findMany();
const serializedConfigs = configs.map((config) => ({
...config,
id: config.id,
}));
return new Response(JSON.stringify(serializedConfigs));
} catch (error) {
console.error('Error fetching dynamic settings:', error);
return new Response(
JSON.stringify({ error: 'Failed to fetch dynamic settings' }),
{ status: 500 }
);
}
}

export async function POST(request: Request) {
try {
const configReq: dynamic_settings = await request.json();
const updateRes = await prisma.dynamic_settings.update({
where: {
id: configReq.id,
},
data: {
config_value: configReq.config_value,
},
});

let updateStatus: 'success' | 'error' = 'error';
if (updateRes.id) {
updateStatus = 'success';
}

return new Response(updateStatus);
} catch (error) {
console.error('Error updating dynamic setting:', error);
return new Response(
JSON.stringify({ error: 'Failed to update dynamic setting' }),
{ status: 500 }
);
}
}
2 changes: 1 addition & 1 deletion ui/app/mirrors/status/qrep/[mirrorId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default async function QRepMirrorStatus({
startTime: run.start_time,
endTime: run.end_time,
pulledRows: run.rows_in_partition,
syncedRows: run.rows_synced,
syncedRows: Number(run.rows_synced),
};
return ret;
});
Expand Down
7 changes: 7 additions & 0 deletions ui/app/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import SidebarComponent from '@/components/SidebarComponent';
import { Layout } from '@/lib/Layout';
import { PropsWithChildren } from 'react';

export default function PageLayout({ children }: PropsWithChildren) {
return <Layout sidebar={<SidebarComponent />}>{children}</Layout>;
}
235 changes: 235 additions & 0 deletions ui/app/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
'use client';

import { DynconfApplyMode } from '@/grpc_generated/flow';
import { Button } from '@/lib/Button';
import { Icon } from '@/lib/Icon';
import { Label } from '@/lib/Label';
import { SearchField } from '@/lib/SearchField';
import { Table, TableCell, TableRow } from '@/lib/Table';
import { TextField } from '@/lib/TextField';
import { Tooltip } from '@/lib/Tooltip';
import { dynamic_settings } from '@prisma/client';
import { MaterialSymbol } from 'material-symbols';
import { useEffect, useMemo, useState } from 'react';

const ROWS_PER_PAGE = 7;

const ApplyModeIconWithTooltip = ({ applyMode }: { applyMode: number }) => {
let tooltipText = '';
let iconName: MaterialSymbol = 'help';

switch (applyMode) {
case DynconfApplyMode.APPLY_MODE_IMMEDIATE:
tooltipText = 'Changes to this configuration will apply immediately';
iconName = 'bolt';
break;
case DynconfApplyMode.APPLY_MODE_AFTER_RESUME:
tooltipText = 'Changes to this configuration will apply after resume';
iconName = 'cached';
break;
case DynconfApplyMode.APPLY_MODE_RESTART:
tooltipText =
'Changes to this configuration will apply after server restart.';
iconName = 'restart_alt';
break;
case DynconfApplyMode.APPLY_MODE_NEW_MIRROR:
tooltipText =
'Changes to this configuration will apply only to new mirrors';
iconName = 'new_window';
break;
default:
tooltipText = 'Unknown apply mode';
iconName = 'help';
}

return (
<div style={{ cursor: 'help' }}>
<Tooltip style={{ width: '100%' }} content={tooltipText}>
<Icon name={iconName} />
</Tooltip>
</div>
);
};

const DynamicSettingItem = ({
setting,
onSettingUpdate,
}: {
setting: dynamic_settings;
onSettingUpdate: () => void;
}) => {
const [editMode, setEditMode] = useState(false);
const [newValue, setNewValue] = useState(setting.config_value);

const handleEdit = () => {
setEditMode(true);
};

const handleSave = async () => {
const updatedSetting = { ...setting, config_value: newValue };
await fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedSetting),
});
setEditMode(false);
onSettingUpdate();
};

return (
<TableRow key={setting.id}>
<TableCell style={{ width: '35%' }}>
<Label>{setting.config_name}</Label>
</TableCell>
<TableCell style={{ width: '10%' }}>
{editMode ? (
<div style={{ display: 'flex', alignItems: 'center' }}>
<TextField
value={newValue || undefined}
onChange={(e) => setNewValue(e.target.value)}
variant='simple'
/>
<Button variant='normalBorderless' onClick={handleSave}>
<Icon name='save' />
</Button>
</div>
) : (
<div style={{ display: 'flex', alignItems: 'center' }}>
{setting.config_value || 'N/A'}
<Button variant='normalBorderless' onClick={handleEdit}>
<Icon name='edit' />
</Button>
</div>
)}
</TableCell>
<TableCell style={{ width: '10%' }}>
{setting.config_default_value || 'N/A'}
</TableCell>
<TableCell style={{ width: '35%' }}>
{setting.config_description || 'N/A'}
</TableCell>
<TableCell style={{ width: '10%' }}>
<ApplyModeIconWithTooltip applyMode={setting.config_apply_mode || 0} />
</TableCell>
</TableRow>
);
};

const SettingsPage = () => {
const [settings, setSettings] = useState<dynamic_settings[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState('');
const [sortDir, setSortDir] = useState<'asc' | 'dsc'>('asc');
const sortField = 'config_name';

const fetchSettings = async () => {
const response = await fetch('/api/settings');
const data = await response.json();
setSettings(data);
};

useEffect(() => {
fetchSettings();
}, []);

const totalPages = Math.ceil(settings.length / ROWS_PER_PAGE);

const displayedSettings = useMemo(() => {
const filteredSettings = settings.filter((setting) =>
setting.config_name.toLowerCase().includes(searchQuery.toLowerCase())
);
filteredSettings.sort((a, b) => {
const aValue = a[sortField];
const bValue = b[sortField];
if (aValue === null || bValue === null) return 0;
if (aValue < bValue) return sortDir === 'dsc' ? 1 : -1;
if (aValue > bValue) return sortDir === 'dsc' ? -1 : 1;
return 0;
});

const startRow = (currentPage - 1) * ROWS_PER_PAGE;
const endRow = startRow + ROWS_PER_PAGE;
return filteredSettings.slice(startRow, endRow);
}, [settings, currentPage, searchQuery, sortField, sortDir]);

const handlePrevPage = () => {
if (currentPage > 1) setCurrentPage(currentPage - 1);
};

const handleNextPage = () => {
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
};

return (
<div>
<Table
title={<Label variant='headline'>Settings List</Label>}
toolbar={{
left: (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Button variant='normalBorderless' onClick={handlePrevPage}>
<Icon name='chevron_left' />
</Button>
<Button variant='normalBorderless' onClick={handleNextPage}>
<Icon name='chevron_right' />
</Button>
<Label>{`${currentPage} of ${totalPages}`}</Label>
<Button variant='normalBorderless' onClick={fetchSettings}>
<Icon name='refresh' />
</Button>
<button
className='IconButton'
onClick={() => setSortDir('asc')}
aria-label='sort up'
style={{ color: sortDir == 'asc' ? 'green' : 'gray' }}
>
<Icon name='arrow_upward' />
</button>
<button
className='IconButton'
onClick={() => setSortDir('dsc')}
aria-label='sort down'
style={{ color: sortDir == 'dsc' ? 'green' : 'gray' }}
>
<Icon name='arrow_downward' />
</button>
</div>
),
right: (
<SearchField
placeholder='Search by config name'
onChange={(e) => setSearchQuery(e.target.value)}
/>
),
}}
header={
<TableRow>
{[
{ header: 'Configuration Name', width: '35%' },
{ header: 'Current Value', width: '10%' },
{ header: 'Default Value', width: '10%' },
{ header: 'Description', width: '35%' },
{ header: 'Apply Mode', width: '10%' },
].map(({ header, width }) => (
<TableCell key={header} as='th' style={{ width }}>
{header}
</TableCell>
))}
</TableRow>
}
>
{displayedSettings.map((setting) => (
<DynamicSettingItem
key={setting.id}
setting={setting}
onSettingUpdate={fetchSettings}
/>
))}
</Table>
</div>
);
};

export default SettingsPage;
7 changes: 7 additions & 0 deletions ui/components/SidebarComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ export default function SidebarComponent() {
>
{sidebarState === 'open' && 'Logs'}
</SidebarItem>
<SidebarItem
as={Link}
href={'/settings'}
leadingIcon={<Icon name='settings' />}
>
{sidebarState === 'open' && 'Settings'}
</SidebarItem>
</Sidebar>
</SessionProvider>
);
Expand Down
35 changes: 21 additions & 14 deletions ui/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ model qrep_partitions {
restart_count Int
metadata Json?
id Int @id @default(autoincrement())
rows_synced Int?
rows_synced BigInt?
qrep_runs qrep_runs @relation(fields: [flow_name, run_uuid], references: [flow_name, run_uuid], onDelete: Cascade, onUpdate: NoAction, map: "fk_qrep_partitions_run")
@@unique([run_uuid, partition_uuid])
Expand Down Expand Up @@ -213,20 +213,14 @@ model schema_deltas_audit_log {
@@schema("peerdb_stats")
}

model alerting_settings {
id Int @id(map: "alerting_settings_pkey1") @default(autoincrement())
config_name String
config_value String
@@schema("public")
}

model dynamic_settings {
id Int @id(map: "alerting_settings_pkey") @default(autoincrement())
config_name String @unique(map: "idx_alerting_settings_config_name")
config_value String
setting_description String?
needs_restart Boolean?
id Int @id(map: "alerting_settings_pkey") @default(autoincrement())
config_name String @unique(map: "idx_alerting_settings_config_name")
config_value String?
config_default_value String?
config_value_type Int?
config_description String?
config_apply_mode Int?
@@schema("public")
}
Expand Down Expand Up @@ -261,6 +255,19 @@ model scripts {
@@schema("public")
}

model ch_s3_stage {
id Int @id @default(autoincrement())
flow_job_name String
sync_batch_id BigInt
avro_file Json
created_at DateTime @default(now()) @db.Timestamptz(6)
@@unique([flow_job_name, sync_batch_id])
@@index([flow_job_name])
@@index([sync_batch_id])
@@schema("public")
}

enum script_lang {
lua
Expand Down

0 comments on commit 32a395e

Please sign in to comment.