Add rendered config viewer at index for improved debug operations later

This commit is contained in:
2023-11-15 20:49:26 -06:00
parent 50665fb444
commit 18d392fb1f
3 changed files with 339 additions and 142 deletions

View File

@@ -0,0 +1,146 @@
import { FunctionComponent } from 'react';
import { BellIcon, CalendarIcon, ClockIcon } from '@heroicons/react/20/solid';
import {
type Configuration,
type DayEnum,
numberAsDay,
ParsedTime
} from '@/timing';
import clsx from 'clsx';
export type ConfigurationListProps = {
configs: Configuration;
};
const shortDays: Record<DayEnum, string> = {
monday: 'Mon',
tuesday: 'Tue',
wednesday: 'Wed',
thursday: 'Thurs',
friday: 'Fri',
saturday: 'Sat',
sunday: 'Sun'
};
const stringifyParsedTime = (time: ParsedTime): string => {
let hour = time.hours;
let postMeridiem = hour >= 12;
if (postMeridiem) hour -= 12;
return `${hour.toString().padStart(2, ' ')}:${time.minutes
.toString()
.padStart(2, '0')} ${postMeridiem ? 'P' : 'A'}M`;
};
// Stringification function for producing dense but precise weekday information.
// Can produce things like "Mon, Wed, Sat", "Mon", "Sat", "Mon - Sat"
const stringifyDaySet = (days: Set<DayEnum>): string => {
if (days.size == 0) return 'No days';
if (days.size == 1) return shortDays[days.values().next().value as DayEnum];
// Build a sorted array of the day set so we can display it properly
const array = Array(...days);
array.sort((a, b) => numberAsDay[a] - numberAsDay[b]);
// Check if continuous from start to end
let isContinuous = true;
let previousDayIndex = numberAsDay[array[0]];
let current = 0;
while (current < array.length - 1) {
current += 1;
let currentDayIndex = numberAsDay[array[current]];
// If current day index is previous + 1, then it's still continuous
if (currentDayIndex !== previousDayIndex + 1) {
isContinuous = false;
break;
}
previousDayIndex = currentDayIndex;
}
if (isContinuous)
return `${shortDays[array[0]]} - ${shortDays[array[current]]}`;
return array.map((day) => shortDays[day]).join(', ');
};
const ConfigurationItem: FunctionComponent<{
title: string;
isCurrent: boolean;
days: Set<DayEnum>;
message: string;
timeString: string;
}> = ({ title, isCurrent, days, message, timeString }) => {
return (
<li>
<a href="#" className="block bg-zinc-900 hover:bg-zinc-800">
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<p className="truncate text-sm font-medium text-indigo-500">
{title}
</p>
<div className="ml-2 flex flex-shrink-0">
<p
className={clsx(
'inline-flex rounded-full px-2 text-xs font-semibold leading-5',
isCurrent
? 'bg-[#0B1910] text-[#4E9468]'
: 'text-yellow-900 bg-yellow-500/80'
)}
>
Not Current
</p>
</div>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="sm:flex">
<p className="mt-2 flex items-center text-sm text-zinc-500">
<ClockIcon
className="mr-1.5 h-5 w-5 flex-shrink-0 text-zinc-400"
aria-hidden="true"
/>
{timeString}
</p>
<p className="flex items-center text-sm text-zinc-500 sm:mt-0 sm:ml-6">
<BellIcon
className="mr-1.5 h-5 w-5 flex-shrink-0 text-zinc-400"
aria-hidden="true"
/>
<span className="truncate pr-2">{message}</span>
</p>
</div>
<div className="mt-2 flex items-center text-sm text-zinc-500 sm:mt-0">
<CalendarIcon
className="mr-1.5 h-5 w-5 flex-shrink-0 text-zinc-400"
aria-hidden="true"
/>
<p>{stringifyDaySet(days)}</p>
</div>
</div>
</div>
</a>
</li>
);
};
const ConfigurationList: FunctionComponent<ConfigurationListProps> = ({
configs
}) => {
return (
<div className="overflow-hidden max-w-screen-md shadow-lg sm:rounded-md">
<ul role="list" className="divide-y divide-zinc-800">
{configs.times.map((config, index) => (
<ConfigurationItem
key={index}
isCurrent={index % 2 == 0}
days={config.days}
title={config.name}
message={config.message}
timeString={stringifyParsedTime(config.time)}
/>
))}
</ul>
</div>
);
};
export default ConfigurationList;

159
src/pages/config.tsx Normal file
View File

@@ -0,0 +1,159 @@
import {
GetServerSidePropsContext,
GetServerSidePropsResult,
NextPage
} from 'next';
import { z } from 'zod';
import { env } from '@/env/server.mjs';
import Editor from 'react-simple-code-editor';
import { ReactNode, useState } from 'react';
import { highlight, languages } from 'prismjs';
import 'prismjs/components/prism-json';
import { fetchConfiguration } from '@/db';
import { useForm } from 'react-hook-form';
import { ConfigurationSchema } from '@/timing';
import { useRouter } from 'next/router';
import Layout from '@/components/Layout';
type Props = {
config: string;
};
export async function getServerSideProps({
query
}: GetServerSidePropsContext): Promise<GetServerSidePropsResult<Props>> {
const parsedKey = z.string().safeParse(query?.key);
if (parsedKey.success && env.API_KEY === parsedKey.data) {
return {
props: {
config: JSON.stringify(
await fetchConfiguration({ times: [] }, false),
null,
4
)
}
};
}
return {
redirect: {
destination: '/login',
permanent: false
}
};
}
const exampleConfiguration = {
times: [
{
time: '03:13',
maxLate: '00:10',
message: 'The bus is leaving soon.',
days: [
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday'
],
name: 'B'
},
{
name: 'A',
message: 'The bus is leaving soon.',
time: '23:26',
maxLate: '00:10',
days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']
}
]
};
const ConfigurationPage: NextPage<Props> = ({ config }) => {
const [code, setCode] = useState(config);
const router = useRouter();
const [validationElement, setValidationElement] = useState<ReactNode | null>(
null
);
const [parseError, setParseError] = useState<string | any>(null);
const { register, handleSubmit } = useForm();
async function onSubmit() {
const parsedConfig = await ConfigurationSchema.safeParseAsync(
JSON.parse(code)
);
if (!parsedConfig.success) {
console.log(parsedConfig.error);
setParseError(parsedConfig.error);
}
setValidationElement(
parsedConfig.success ? 'Valid Configuration' : 'Invalid Configuration'
);
if (parsedConfig.success) {
const response = await fetch(`/api/config?key=${router.query?.key}`, {
method: 'POST',
body: code
});
console.log(response);
}
}
return (
<Layout className="max-h-screen">
<div className="px-5 sm:mx-auto sm:w-full sm:max-w-4xl">
<div className="bg-black/ py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form onSubmit={handleSubmit(onSubmit)}>
<label
htmlFor="comment"
className="block text-sm font-medium text-gray-400"
>
Modify Configuration
</label>
<div
className="mt-1 min-h-[1rem] overflow-auto"
style={{ maxHeight: 'calc(100vh - 13rem)' }}
>
<Editor
value={code}
onValueChange={(code) => setCode(code)}
highlight={(code) => highlight(code, languages.json, 'json')}
padding={20}
preClassName="language-json overflow-y-scroll"
textareaClassName="border-zinc-700/70 overflow-y-scroll"
className="text-white w-full rounded-md bg-zinc-800/50 border-zinc-700/70 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
style={{
fontFamily: '"Fira code", "Fira Mono", monospace'
}}
/>
</div>
<div className="flex justify-between pt-2">
<div className="flex-shrink-0">{validationElement}</div>
<div className="flex-shrink-0 space-x-4">
<button
onClick={(e) => {
e.preventDefault();
setCode(JSON.stringify(exampleConfiguration, null, 4));
}}
className="inline-flex items-center rounded-md border border-transparent bg-zinc-700 hover:bg-zinc-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Load Example
</button>
<button
type="submit"
className="inline-flex items-center rounded-md border border-transparent bg-indigo-700 hover:bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Update
</button>
</div>
</div>
</form>
</div>
</div>
</Layout>
);
};
export default ConfigurationPage;

View File

@@ -1,159 +1,51 @@
import {
GetServerSidePropsContext,
GetServerSidePropsResult,
NextPage
GetServerSidePropsContext,
GetServerSidePropsResult,
NextPage
} from 'next';
import { z } from 'zod';
import { env } from '@/env/server.mjs';
import Editor from 'react-simple-code-editor';
import { ReactNode, useState } from 'react';
import { highlight, languages } from 'prismjs';
import 'prismjs/components/prism-json';
import { fetchConfiguration } from '@/db';
import { useForm } from 'react-hook-form';
import { ConfigurationSchema } from '@/timing';
import { useRouter } from 'next/router';
import Layout from '@/components/Layout';
import ConfigurationList from '@/components/ConfigurationList';
import superjson from 'superjson';
import type { Configuration } from '@/timing';
type Props = {
config: string;
type IndexPageProps = {
json: string;
};
export async function getServerSideProps({
query
}: GetServerSidePropsContext): Promise<GetServerSidePropsResult<Props>> {
const parsedKey = z.string().safeParse(query?.key);
query
}: GetServerSidePropsContext): Promise<
GetServerSidePropsResult<IndexPageProps>
> {
const parsedKey = z.string().safeParse(query?.key);
if (parsedKey.success && env.API_KEY === parsedKey.data) {
return {
props: {
config: JSON.stringify(
await fetchConfiguration({ times: [] }, false),
null,
4
)
}
};
}
if (parsedKey.success && env.API_KEY === parsedKey.data) {
const config = await fetchConfiguration({ times: [] }, true);
return {
props: {
json: superjson.stringify(config)
}
};
}
return {
redirect: {
destination: '/login',
permanent: false
}
};
return {
redirect: {
destination: '/login',
permanent: false
}
};
}
const exampleConfiguration = {
times: [
{
time: '03:13',
maxLate: '00:10',
message: 'The bus is leaving soon.',
days: [
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
'sunday'
],
name: 'B'
},
{
name: 'A',
message: 'The bus is leaving soon.',
time: '23:26',
maxLate: '00:10',
days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']
}
]
};
const IndexPage: NextPage<Props> = ({ config }) => {
const [code, setCode] = useState(config);
const router = useRouter();
const [validationElement, setValidationElement] = useState<ReactNode | null>(
null
);
const [parseError, setParseError] = useState<string | any>(null);
const { register, handleSubmit } = useForm();
async function onSubmit() {
const parsedConfig = await ConfigurationSchema.safeParseAsync(
JSON.parse(code)
);
if (!parsedConfig.success) {
console.log(parsedConfig.error);
setParseError(parsedConfig.error);
}
setValidationElement(
parsedConfig.success ? 'Valid Configuration' : 'Invalid Configuration'
);
if (parsedConfig.success) {
const response = await fetch(`/api/config?key=${router.query?.key}`, {
method: 'POST',
body: code
});
console.log(response);
}
}
return (
<Layout className="max-h-screen">
<div className="px-5 sm:mx-auto sm:w-full sm:max-w-4xl">
<div className="bg-black/ py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form onSubmit={handleSubmit(onSubmit)}>
<label
htmlFor="comment"
className="block text-sm font-medium text-gray-400"
>
Modify Configuration
</label>
<div
className="mt-1 min-h-[1rem] overflow-auto"
style={{ maxHeight: 'calc(100vh - 13rem)' }}
>
<Editor
value={code}
onValueChange={(code) => setCode(code)}
highlight={(code) => highlight(code, languages.json, 'json')}
padding={20}
preClassName="language-json overflow-y-scroll"
textareaClassName="border-zinc-700/70 overflow-y-scroll"
className="text-white w-full rounded-md bg-zinc-800/50 border-zinc-700/70 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
style={{
fontFamily: '"Fira code", "Fira Mono", monospace'
}}
/>
</div>
<div className="flex justify-between pt-2">
<div className="flex-shrink-0">{validationElement}</div>
<div className="flex-shrink-0 space-x-4">
<button
onClick={(e) => {
e.preventDefault();
setCode(JSON.stringify(exampleConfiguration, null, 4));
}}
className="inline-flex items-center rounded-md border border-transparent bg-zinc-700 hover:bg-zinc-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Load Example
</button>
<button
type="submit"
className="inline-flex items-center rounded-md border border-transparent bg-indigo-700 hover:bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Update
</button>
</div>
</div>
</form>
</div>
</div>
</Layout>
);
const IndexPage: NextPage<IndexPageProps> = ({ json }) => {
const config = superjson.parse<Configuration>(json);
return (
<Layout className="max-h-screen flex flex-col items-center">
<ConfigurationList configs={config} />
</Layout>
);
};
export default IndexPage;