Skip to content

Commit

Permalink
feat: can edit existing config variables (#3)
Browse files Browse the repository at this point in the history
* chore: updated readme with a sales pitch

* feat: can edit keys

* fixup! feat: can edit keys
  • Loading branch information
birme authored Oct 15, 2024
1 parent 0d4ef2f commit 15450ff
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 9 deletions.
10 changes: 2 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,21 @@
</h1>

<div align="center">
project name - quick salespitch why this is awesome.
Provide applications with configuration values in a performant way!
<br />
<br />
:book: <b><a href="https://eyevinn.github.io/{{repo-name}}/">Read the documentation (github pages)</a></b> :eyes:
<br />
</div>

<div align="center">
<br />

[![npm](https://img.shields.io/npm/v/@eyevinn/{{repo-name}}?style=flat-square)](https://www.npmjs.com/package/@eyevinn/{{repo-name}})
[![github release](https://img.shields.io/github/v/release/Eyevinn/{{repo-name}}?style=flat-square)](https://github.com/Eyevinn/{{repo-name}}/releases)
[![license](https://img.shields.io/github/license/eyevinn/{{repo-name}}.svg?style=flat-square)](LICENSE)

[![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg?style=flat-square)](https://github.com/eyevinn/{{repo-name}}/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
[![made with hearth by Eyevinn](https://img.shields.io/badge/made%20with%20%E2%99%A5%20by-Eyevinn-59cbe8.svg?style=flat-square)](https://github.com/eyevinn)
[![Slack](http://slack.streamingtech.se/badge.svg)](http://slack.streamingtech.se)

</div>

<!-- Add a description of the project here -->
Backed with a Redis (or Redis compatible) key/value store this service lets you manage application configuration variables and serve them to the clients. Cache control headers provided by default and ready to be placed behind a CDN for delivery to many user applications at the same time.

## Requirements

Expand Down
3 changes: 3 additions & 0 deletions src/app/config/layout.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.navbar > header {
max-width: none;
}
211 changes: 211 additions & 0 deletions src/app/config/variables/_components/ConfigObjectTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
'use client';

import { ConfigObject, ConfigObjectList } from '@/api_config';
import { ActionResponse } from '@/app/utils';
import { useApiUrl } from '@/hooks/useApiUrl';
import {
Button,
Input,
Pagination,
Spinner,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow
} from '@nextui-org/react';
import { IconCheck, IconPencil, IconTrash } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react';

export interface ConfigObjectTableProps {
pageSize?: number;
}

const DEFAULT_PAGE_SIZE = 20;

async function getConfigObjectList({
apiUrl,
offset,
limit
}: {
apiUrl: string;
offset: number;
limit: number;
}): ActionResponse<ConfigObjectList> {
const url = new URL(`${apiUrl}/config`);
url.searchParams.append('offset', offset.toString());
url.searchParams.append('limit', limit.toString());
const response = await fetch(url);
if (!response.ok) {
return [undefined, 'Failed to fetch config object list'];
}
return [await response.json()];
}

async function updateConfigObject({
apiUrl,
keyId,
value
}: {
apiUrl: string;
keyId: string;
value: string;
}): ActionResponse<ConfigObject> {
const url = new URL(`${apiUrl}/config`);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: keyId, value })
});
if (!response.ok) {
return [undefined, 'Failed to update config object'];
}
return [await response.json()];
}

export default function ConfigObjectTable({
pageSize = DEFAULT_PAGE_SIZE
}: ConfigObjectTableProps) {
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<ConfigObjectList | undefined>(undefined);
const [editKeyId, setEditKeyId] = useState<string | undefined>(undefined);
const [editValue, setEditValue] = useState<string | undefined>(undefined);

const apiUrl = useApiUrl();

const handleSaveEdit = (keyId: string) => {
if (editValue === undefined) {
return;
}

if (!apiUrl) {
return;
}

updateConfigObject({ apiUrl, keyId, value: editValue })
.then(([data, error]) => {
if (error) {
console.error(error);
return;
}
setEditValue(data?.value);
})
.finally(() => {
setEditKeyId(undefined);
updateTableContents();
});
};

const updateTableContents = () => {
if (!apiUrl) {
return;
}
setIsLoading(true);
getConfigObjectList({
apiUrl,
offset: (page - 1) * pageSize,
limit: pageSize
})
.then(([data, error]) => {
if (error) {
console.error(error);
return;
}
setData(data);
})
.finally(() => {
setIsLoading(false);
});
};

useEffect(() => {
updateTableContents();
}, [page, pageSize, apiUrl]);

const pages = useMemo(() => {
return data?.total ? Math.ceil(data.total / pageSize) : 0;
}, [data?.total, pageSize]);

const loadingState =
isLoading || data?.items.length === 0 ? 'loading' : 'idle';

return (
<Table
bottomContent={
pages > 0 ? (
<div className="flex w-full justify-center">
<Pagination
isCompact
showControls
showShadow
color="primary"
page={page}
total={pages}
onChange={(page) => setPage(page)}
/>
</div>
) : null
}
>
<TableHeader>
<TableColumn key="configKey">KEY</TableColumn>
<TableColumn key="configValue">VALUE</TableColumn>
<TableColumn key="configActions">ACTIONS</TableColumn>
</TableHeader>
<TableBody
items={
data?.items.map((obj) => {
return { keyId: obj.key, value: obj.value };
}) ?? []
}
loadingContent={<Spinner />}
loadingState={loadingState}
>
{(item) => (
<TableRow key={item.keyId}>
<TableCell>{item.keyId}</TableCell>
<TableCell>
{editKeyId === item.keyId ? (
<Input
value={editValue ?? item.value}
onValueChange={setEditValue}
/>
) : (
item.value
)}
</TableCell>
<TableCell>
{editKeyId !== item.keyId && (
<Button
isIconOnly
color="primary"
size="sm"
onPress={() => setEditKeyId(item.keyId)}
>
<IconPencil />
</Button>
)}
{editKeyId === item.keyId && (
<Button
isIconOnly
color="primary"
size="sm"
onPress={() => handleSaveEdit(item.keyId)}
>
<IconCheck />
</Button>
)}
<Button isIconOnly color="danger" size="sm">
<IconTrash />
</Button>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
}
8 changes: 7 additions & 1 deletion src/app/config/variables/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
'use client';

import ConfigObjectTable from './_components/ConfigObjectTable';

export default function Page() {
return <div></div>;
return (
<div>
<ConfigObjectTable />
</div>
);
}
2 changes: 2 additions & 0 deletions src/app/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type ErrorMessage = string;
export type ActionResponse<T> = Promise<[T] | [undefined, ErrorMessage]>;

0 comments on commit 15450ff

Please sign in to comment.