feat: overhaul webui (#135)

feat: Added Inter Variable as the default font

feat: Added a pattern to the page background.

fix(react-multi-select-component): Made the multiselect components theme-agnostic, so it works properly with the light theme now.

fix(react-select): Made the components fix the default 30px height of the rest of the input components.

fix(useToggle): Fixed a bug where the toggle didn't work due to useCallback memoizing a callback with the old value.
refactor(Base):
- Added small theme-primary gradient to the beginning of the header.
- Made the splitter border theme-agnostic.
- Increased logo size a bit.
- Moved the links a bit closer to the logo.
- Updated the default link style to look more stylish.
- Added a link to the autobrr Docs section (currently defaults to the Indexers sections, but this should lead to an "Overview" page for configuring autobrr)
- Adapted the user dropdown to match the other header links' stylesheets and removed the annoying ring focus.
- Adapted the header for theme-agnostic mobile usage.
- Removed the negative padding/margin hacks.

refactor(StatsItem): Increased shadow size/strength and made the description text a bit lighter on the dark theme.

refactor(Dashboard): Increased the heading text sizes.

refactor(Logs, Releases, Settings, Login, filters/details, filters/list): Adapted to match the previous changes and improved theme compatibility with regards to text.

refactor(RegexPlayground): Fixed match highlight for JS regex.
This commit is contained in:
stacksmash76 2022-02-14 19:12:10 +01:00 committed by GitHub
parent c3687b8fa5
commit ac37bd4d9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 371 additions and 342 deletions

View file

@ -5,6 +5,7 @@
"proxy": "http://127.0.0.1:8989", "proxy": "http://127.0.0.1:8989",
"homepage": ".", "homepage": ".",
"dependencies": { "dependencies": {
"@fontsource/inter": "^4.5.4",
"@headlessui/react": "^1.2.0", "@headlessui/react": "^1.2.0",
"@heroicons/react": "^1.0.1", "@heroicons/react": "^1.0.1",
"date-fns": "^2.25.0", "date-fns": "^2.25.0",

View file

@ -28,6 +28,6 @@
</head> </head>
<body class="bg-gray-100 dark:bg-gray-900"> <body class="bg-gray-100 dark:bg-gray-900">
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root" class="pattern"></div>
</body> </body>
</html> </html>

View file

@ -38,5 +38,5 @@ export function App() {
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />
) : null} ) : null}
</QueryClientProvider> </QueryClientProvider>
) );
} }

View file

@ -1,9 +1,11 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { MultiSelect as RMSC} from "react-multi-select-component";
import { Transition, Listbox } from "@headlessui/react";
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';
import { classNames, COL_WIDTHS } from "../../utils";
import { Field } from "formik"; import { Field } from "formik";
import { Transition, Listbox } from "@headlessui/react";
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
import { MultiSelect as RMSC } from "react-multi-select-component";
import { classNames, COL_WIDTHS } from "../../utils";
import { SettingsContext } from "../../utils/Context";
interface MultiSelectProps { interface MultiSelectProps {
label?: string; label?: string;
@ -17,80 +19,87 @@ export const MultiSelect = ({
label, label,
options, options,
columns, columns,
}: MultiSelectProps) => ( }: MultiSelectProps) => {
<div const settingsContext = SettingsContext.useValue();
className={classNames( return (
columns ? `col-span-${columns}` : "col-span-12" <div
)} className={classNames(
> columns ? `col-span-${columns}` : "col-span-12"
<label
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
htmlFor={label}
>
{label}
</label>
<Field name={name} type="select" multiple={true}>
{({
field,
form: { setFieldValue },
}: any) => (
<RMSC
{...field}
type="select"
options={options}
labelledBy={name}
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value === item))}
onChange={(values: any) => {
const am = values && values.map((i: any) => i.value);
setFieldValue(field.name, am);
}}
className="dark:bg-gray-700 dark"
/>
)} )}
</Field> >
</div> <label
); className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
htmlFor={label}
>
{label}
</label>
<Field name={name} type="select" multiple={true}>
{({
field,
form: { setFieldValue },
}: any) => (
<RMSC
{...field}
type="select"
options={options}
labelledBy={name}
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value === item))}
onChange={(values: any) => {
const am = values && values.map((i: any) => i.value);
setFieldValue(field.name, am);
}}
className={settingsContext.darkTheme ? "dark" : ""}
/>
)}
</Field>
</div>
);
}
export const IndexerMultiSelect = ({ export const IndexerMultiSelect = ({
name, name,
label, label,
options, options,
columns, columns,
}: MultiSelectProps) => ( }: MultiSelectProps) => {
<div const settingsContext = SettingsContext.useValue();
className={classNames( return (
columns ? `col-span-${columns}` : "col-span-12" <div
)} className={classNames(
> columns ? `col-span-${columns}` : "col-span-12"
<label )}
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200" >
htmlFor={label} <label
> className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
{label} htmlFor={label}
</label> >
{label}
</label>
<Field name={name} type="select" multiple={true}> <Field name={name} type="select" multiple={true}>
{({ {({
field, field,
form: { setFieldValue }, form: { setFieldValue },
}: any) => ( }: any) => (
<RMSC <RMSC
{...field} {...field}
type="select" type="select"
options={options} options={options}
labelledBy={name} labelledBy={name}
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value?.id === item.id))} value={field.value && field.value.map((item: any) => options.find((o: any) => o.value?.id === item.id))}
onChange={(values: any) => { onChange={(values: any) => {
const am = values && values.map((i: any) => i.value); const am = values && values.map((i: any) => i.value);
setFieldValue(field.name, am); setFieldValue(field.name, am);
}} }}
className="dark:bg-gray-700 dark" className={settingsContext.darkTheme ? "dark" : ""}
/> itemHeight={50}
)} />
</Field> )}
</div> </Field>
); </div>
);
}
interface DownloadClientSelectProps { interface DownloadClientSelectProps {
name: string; name: string;

View file

@ -254,6 +254,14 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
color: "unset" color: "unset"
}) })
}} }}
theme={(theme) => ({
...theme,
spacing: {
...theme.spacing,
controlHeight: 30,
baseUnit: 2,
}
})}
value={field?.value && field.value.value} value={field?.value && field.value.value}
onChange={(option: any) => { onChange={(option: any) => {
setFieldValue("name", option?.label ?? "") setFieldValue("name", option?.label ?? "")

View file

@ -1,8 +1,8 @@
import { useState, useCallback } from "react"; import { useState } from "react";
export function useToggle(initialValue = false): [boolean, () => void] { export function useToggle(initialValue = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []); const toggle = () => setValue(v => !v);
return [value, toggle]; return [value, toggle];
} }

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,7 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import "@fontsource/inter/variable.css";
import "./index.css"; import "./index.css";
import { App } from "./App"; import { App } from "./App";

View file

@ -1,6 +1,8 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { NavLink, Link, Route, Switch } from "react-router-dom"; import { NavLink, Link, Route, Switch } from "react-router-dom";
import type { match } from "react-router-dom";
import { Disclosure, Menu, Transition } from "@headlessui/react"; import { Disclosure, Menu, Transition } from "@headlessui/react";
import { ExternalLinkIcon } from "@heroicons/react/solid";
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline"; import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
import Logs from "./Logs"; import Logs from "./Logs";
@ -13,13 +15,35 @@ import { AuthContext } from '../utils/Context';
import logo from '../logo.png'; import logo from '../logo.png';
interface NavItem {
name: string;
path: string;
}
function classNames(...classes: string[]) { function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ') return classes.filter(Boolean).join(' ')
} }
const isActiveMatcher = (
match: match<any> | null,
location: { pathname: string },
item: NavItem
) => {
if (!match)
return false;
if (match?.url === "/" && item.path === "/" && location.pathname === "/")
return true
if (match.url === "/")
return false;
return true;
}
export default function Base() { export default function Base() {
const authContext = AuthContext.useValue(); const authContext = AuthContext.useValue();
const nav = [ const nav: Array<NavItem> = [
{ name: 'Dashboard', path: "/" }, { name: 'Dashboard', path: "/" },
{ name: 'Filters', path: "/filters" }, { name: 'Filters', path: "/filters" },
{ name: 'Releases', path: "/releases" }, { name: 'Releases', path: "/releases" },
@ -28,56 +52,58 @@ export default function Base() {
]; ];
return ( return (
<div> <div className="min-h-screen">
<Disclosure as="nav" className="bg-gray-900 pb-48"> <Disclosure
as="nav"
className="bg-gradient-to-b from-gray-100 dark:from-[#141414]"
>
{({ open }) => ( {({ open }) => (
<> <>
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="border-b border-gray-700"> <div className="border-b border-gray-300 dark:border-gray-700">
<div className="flex items-center justify-between h-16 px-4 sm:px-0"> <div className="flex items-center justify-between h-16 px-4 sm:px-0">
<div className="flex items-center"> <div className="flex items-center">
<div className="flex-shrink-0 flex items-center"> <div className="flex-shrink-0 flex items-center">
<img <img
className="block lg:hidden h-8 w-auto" className="block lg:hidden h-10 w-auto"
src={logo} src={logo}
alt="Logo" alt="Logo"
/> />
<img <img
className="hidden lg:block h-8 w-auto" className="hidden lg:block h-10 w-auto"
src={logo} src={logo}
alt="Logo" alt="Logo"
/> />
</div> </div>
<div className="sm:ml-6 hidden sm:block"> <div className="sm:ml-3 hidden sm:block">
<div className="flex items-baseline space-x-4"> <div className="flex items-baseline space-x-4">
{nav.map((item, itemIdx) => {nav.map((item, itemIdx) =>
<NavLink <NavLink
key={item.name + itemIdx} key={item.name + itemIdx}
to={item.path} to={item.path}
strict={true} strict
isActive={(match, location) => {
if (match?.url === "/" && item.path === "/" && location.pathname === "/") {
return true
}
if (!match) {
return false;
}
if (match.url === "/") {
return false;
}
return true;
}}
activeClassName="bg-gray-900 dark:bg-gray-700 text-white "
className={classNames( className={classNames(
"text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium" "text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
"transition-colors duration-200"
)} )}
activeClassName="text-black dark:text-gray-50 font-bold"
isActive={(match, location) => isActiveMatcher(match, location, item)}
> >
{item.name} {item.name}
</NavLink> </NavLink>
)} )}
<a
rel="noopener noreferrer"
target="_blank"
href="https://autobrr.com/docs/configuration/indexers"
className={classNames(
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
"transition-colors duration-200 flex items-center justify-center"
)}
>
Docs
<ExternalLinkIcon className="inline ml-1 h-5 w-5" aria-hidden="true" />
</a>
</div> </div>
</div> </div>
</div> </div>
@ -86,19 +112,21 @@ export default function Base() {
<Menu as="div" className="ml-3 relative"> <Menu as="div" className="ml-3 relative">
{({ open }) => ( {({ open }) => (
<> <>
<div> <Menu.Button
<Menu.Button className={classNames(
className="max-w-xs rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"> open ? "bg-gray-200 dark:bg-gray-800" : "",
<span "text-gray-800 dark:text-gray-300 max-w-xs rounded-full flex items-center text-sm px-3 py-2 hover:bg-gray-200 dark:hover:bg-gray-800"
className="hidden text-gray-300 text-sm font-medium sm:block"> )}
<span className="sr-only">Open user menu for </span>{authContext.username} >
</span> <span className="hidden text-sm font-medium sm:block">
<ChevronDownIcon <span className="sr-only">Open user menu for </span>
className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-400 sm:block" {authContext.username}
aria-hidden="true" </span>
/> <ChevronDownIcon
</Menu.Button> className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-800 dark:text-gray-300 sm:block"
</div> aria-hidden="true"
/>
</Menu.Button>
<Transition <Transition
show={open} show={open}
as={Fragment} as={Fragment}
@ -149,7 +177,7 @@ export default function Base() {
<div className="-mr-2 flex sm:hidden"> <div className="-mr-2 flex sm:hidden">
{/* Mobile menu button */} {/* Mobile menu button */}
<Disclosure.Button <Disclosure.Button
className="bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"> className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
<span className="sr-only">Open main menu</span> <span className="sr-only">Open main menu</span>
{open ? ( {open ? (
<XIcon className="block h-6 w-6" aria-hidden="true" /> <XIcon className="block h-6 w-6" aria-hidden="true" />
@ -162,48 +190,28 @@ export default function Base() {
</div> </div>
</div> </div>
<Disclosure.Panel className="border-b border-gray-700 md:hidden"> <Disclosure.Panel className="border-b border-gray-300 dark:border-gray-700 md:hidden">
<div className="px-2 py-3 space-y-1 sm:px-3"> <div className="px-2 py-3 space-y-1 sm:px-3">
{nav.map((item, itemIdx) => {nav.map((item) =>
itemIdx === 0 ? ( <NavLink
<Fragment key={item.path}> key={item.path}
<Link to={item.path} to={item.path}
className="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium"> strict
{item.name} className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
</Link> activeClassName="font-bold bg-gray-300 text-black"
</Fragment> isActive={(match, location) => isActiveMatcher(match, location, item)}
) : ( >
<Link {item.name}
key={item.path} </NavLink>
to={item.path}
className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
>
{item.name}
</Link>
)
)} )}
<Link
to="/logout"
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
>
Logout
</Link>
</div> </div>
<div className="pt-4 pb-3 border-t border-gray-700">
<div className="flex items-center px-5">
<div>
<div className="text-base font-medium leading-none text-white">User</div>
</div>
</div>
<div className="mt-3 px-2 space-y-1">
<Link
to="/settings"
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700"
>
Settings
</Link>
<Link
to="/logout"
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700"
>
Logout
</Link>
</div>
</div>
</Disclosure.Panel> </Disclosure.Panel>
</> </>
)} )}
@ -236,4 +244,4 @@ export default function Base() {
</Switch> </Switch>
</div> </div>
) )
} }

View file

@ -15,7 +15,7 @@ import { ReleaseStatusCell } from "./Releases";
export function Dashboard() { export function Dashboard() {
return ( return (
<main className="py-10 -mt-48"> <main className="py-10">
<div className="px-4 pb-8 mx-auto max-w-7xl sm:px-6 lg:px-8"> <div className="px-4 pb-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
<Stats /> <Stats />
<DataTable /> <DataTable />
@ -26,15 +26,15 @@ export function Dashboard() {
const StatsItem = ({ name, stat }: any) => ( const StatsItem = ({ name, stat }: any) => (
<div <div
className="relative px-4 pt-5 pb-2 overflow-hidden bg-white rounded-lg shadow dark:bg-gray-800 sm:pt-6 sm:px-6" className="relative px-4 pt-5 pb-2 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800 sm:pt-6 sm:px-6"
title="All time" title="All time"
> >
<dt> <dt>
<p className="pb-1 text-sm font-medium text-gray-500 truncate dark:text-gray-600">{name}</p> <p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p>
</dt> </dt>
<dd className="flex items-baseline pb-6 sm:pb-7"> <dd className="flex items-baseline pb-6 sm:pb-7">
<p className="text-2xl font-semibold text-gray-900 dark:text-gray-300">{stat}</p> <p className="text-2xl font-semibold text-gray-900 dark:text-gray-200">{stat}</p>
</dd> </dd>
</div> </div>
) )
@ -51,7 +51,9 @@ function Stats() {
return ( return (
<div> <div>
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-600">Stats</h3> <h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
Stats
</h3>
<dl className="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3"> <dl className="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3">
<StatsItem name="Filtered Releases" stat={data?.filtered_count} /> <StatsItem name="Filtered Releases" stat={data?.filtered_count} />
@ -311,7 +313,7 @@ function DataTable() {
return ( return (
<div className="flex flex-col mt-12"> <div className="flex flex-col mt-12">
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-600"> <h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
Recent activity Recent activity
</h3> </h3>

View file

@ -30,15 +30,15 @@ export default function Logs() {
}, [setLogs]); }, [setLogs]);
return ( return (
<main className="-mt-48"> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-white capitalize">Logs</h1> <h1 className="text-3xl font-bold text-black dark:text-white capitalize">Logs</h1>
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-2 sm:px-4 lg:px-8"> <div className="max-w-7xl mx-auto pb-12 px-2 sm:px-4 lg:px-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow px-2 sm:px-4 py-3 sm:py-4"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg px-2 sm:px-4 py-3 sm:py-4">
<div className=" overflow-y-auto p-2 rounded-lg h-96 bg-gray-900"> <div className=" overflow-y-auto p-2 rounded-lg h-96 bg-gray-100 dark:bg-gray-900">
{logs.map((a, idx) => ( {logs.map((a, idx) => (
<p key={idx}> <p key={idx}>
<span className="font-mono text-gray-600 mr-2">{a.time}</span> <span className="font-mono text-gray-600 mr-2">{a.time}</span>

View file

@ -21,11 +21,10 @@ import { classNames, simplifyDate } from "../utils";
export function Releases() { export function Releases() {
return ( return (
<main className="-mt-48"> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
<h1 className="text-3xl font-bold text-white capitalize">Releases</h1> <h1 className="text-3xl font-bold text-black dark:text-white capitalize">Releases</h1>
</div> </div>
</header> </header>
<div className="px-4 pb-8 mx-auto max-w-7xl sm:px-6 lg:px-8"> <div className="px-4 pb-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
@ -305,156 +304,138 @@ function Table() {
return ( return (
<> <>
{isSuccess && data ? ( {isSuccess && data ? (
<div className="flex flex-col mt-4"> <div className="flex flex-col">
{/* <GlobalFilter <div className="overflow-hidden bg-white shadow-lg dark:bg-gray-800 sm:rounded-lg">
preGlobalFilteredRows={preGlobalFilteredRows} <table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
globalFilter={state.globalFilter} <thead className="bg-gray-50 dark:bg-gray-800">
setGlobalFilter={setGlobalFilter} {headerGroups.map((headerGroup) => {
/> */} const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
{/* {headerGroups.map((headerGroup: { headers: any[] }) => return (
headerGroup.headers.map((column) => <tr key={rowKey} {...rowRest}>
column.Filter ? ( {headerGroup.headers.map((column) => {
<div className="mt-2 sm:mt-0" key={column.id}> const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
{column.render("Filter")} return (
</div> // Add the sorting props to control sorting. For this example
) : null // we can add them into the header props
) <th
)} */} key={`${rowKey}-${columnKey}`}
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> scope="col"
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
<div className="overflow-hidden bg-white shadow dark:bg-gray-800 sm:rounded-lg"> {...columnRest}
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> >
<thead className="bg-gray-50 dark:bg-gray-800"> <div className="flex items-center justify-between">
{headerGroups.map((headerGroup) => { {column.render('Header')}
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps(); {/* Add a sort direction indicator */}
return ( <span>
<tr key={rowKey} {...rowRest}> {column.isSorted ? (
{headerGroup.headers.map((column) => { column.isSortedDesc ? (
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps()); <SortDownIcon className="w-4 h-4 text-gray-400" />
return ( ) : (
// Add the sorting props to control sorting. For this example <SortUpIcon className="w-4 h-4 text-gray-400" />
// we can add them into the header props )
<th ) : (
key={`${rowKey}-${columnKey}`} <SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" />
scope="col" )}
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group" </span>
{...columnRest} </div>
> </th>
<div className="flex items-center justify-between"> );
{column.render('Header')} })}
{/* Add a sort direction indicator */} </tr>
<span> );
{column.isSorted ? ( })}
column.isSortedDesc ? ( </thead>
<SortDownIcon className="w-4 h-4 text-gray-400" /> <tbody
) : ( {...getTableBodyProps()}
<SortUpIcon className="w-4 h-4 text-gray-400" /> className="divide-y divide-gray-200 dark:divide-gray-700"
) >
) : ( {page.map((row: any) => { // new
<SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" /> prepareRow(row)
)} const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
</span> return (
</div> <tr key={bodyRowKey} {...bodyRowRest}>
</th> {row.cells.map((cell: any) => {
); const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
})} return (
</tr> <td
); key={cellRowKey}
})} className="px-6 py-4 whitespace-nowrap"
</thead> role="cell"
<tbody {...cellRowRest}
{...getTableBodyProps()} >
className="divide-y divide-gray-200 dark:divide-gray-700" {cell.column.Cell.name === "defaultRenderer"
> ? <div className="text-sm text-gray-500">{cell.render('Cell')}</div>
{page.map((row: any) => { // new : cell.render('Cell')
prepareRow(row) }
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps(); </td>
return ( );
<tr key={bodyRowKey} {...bodyRowRest}> })}
{row.cells.map((cell: any) => { </tr>
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps(); );
return ( })}
<td </tbody>
key={cellRowKey} </table>
className="px-6 py-4 whitespace-nowrap"
role="cell"
{...cellRowRest}
>
{cell.column.Cell.name === "defaultRenderer"
? <div className="text-sm text-gray-500">{cell.render('Cell')}</div>
: cell.render('Cell')
}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
{/* Pagination */} {/* Pagination */}
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700"> <div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
<div className="flex justify-between flex-1 sm:hidden"> <div className="flex justify-between flex-1 sm:hidden">
<Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</Button> <Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</Button>
<Button onClick={() => nextPage()} disabled={!canNextPage}>Next</Button> <Button onClick={() => nextPage()} disabled={!canNextPage}>Next</Button>
</div> </div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"> <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div className="flex items-baseline gap-x-2"> <div className="flex items-baseline gap-x-2">
<span className="text-sm text-gray-700"> <span className="text-sm text-gray-700">
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span> Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
</span> </span>
<label> <label>
<span className="sr-only">Items Per Page</span> <span className="sr-only">Items Per Page</span>
<select <select
className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50" className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
value={pageSize} value={pageSize}
onChange={e => { onChange={e => {
setPageSize(Number(e.target.value)) setPageSize(Number(e.target.value))
}} }}
> >
{[5, 10, 20, 50].map(pageSize => ( {[5, 10, 20, 50].map(pageSize => (
<option key={pageSize} value={pageSize}> <option key={pageSize} value={pageSize}>
Show {pageSize} Show {pageSize}
</option> </option>
))} ))}
</select> </select>
</label> </label>
</div> </div>
<div> <div>
<nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination"> <nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
<PageButton <PageButton
className="rounded-l-md" className="rounded-l-md"
onClick={() => gotoPage(0)} onClick={() => gotoPage(0)}
disabled={!canPreviousPage} disabled={!canPreviousPage}
> >
<span className="sr-only">First</span> <span className="sr-only">First</span>
<ChevronDoubleLeftIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> <ChevronDoubleLeftIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
</PageButton> </PageButton>
<PageButton <PageButton
onClick={() => previousPage()} onClick={() => previousPage()}
disabled={!canPreviousPage} disabled={!canPreviousPage}
> >
<span className="sr-only">Previous</span> <span className="sr-only">Previous</span>
<ChevronLeftIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> <ChevronLeftIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
</PageButton> </PageButton>
<PageButton <PageButton
onClick={() => nextPage()} onClick={() => nextPage()}
disabled={!canNextPage}> disabled={!canNextPage}>
<span className="sr-only">Next</span> <span className="sr-only">Next</span>
<ChevronRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> <ChevronRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
</PageButton> </PageButton>
<PageButton <PageButton
className="rounded-r-md" className="rounded-r-md"
onClick={() => gotoPage(pageCount - 1)} onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage} disabled={!canNextPage}
> >
<span className="sr-only">Last</span> <span className="sr-only">Last</span>
<ChevronDoubleRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> <ChevronDoubleRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
</PageButton> </PageButton>
</nav> </nav>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -62,15 +62,15 @@ function SidebarNav({subNavigation, url}: any) {
export default function Settings() { export default function Settings() {
const { url } = useRouteMatch(); const { url } = useRouteMatch();
return ( return (
<main className="relative -mt-48"> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-white capitalize">Settings</h1> <h1 className="text-3xl font-bold text-black dark:text-white capitalize">Settings</h1>
</div> </div>
</header> </header>
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8"> <div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x"> <div className="divide-y divide-gray-200 dark:divide-gray-700 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<SidebarNav url={url} subNavigation={subNavigation}/> <SidebarNav url={url} subNavigation={subNavigation}/>

View file

@ -33,7 +33,7 @@ function Login() {
const handleSubmit = (data: any) => mutation.mutate(data); const handleSubmit = (data: any) => mutation.mutate(data);
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8"> <div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md mb-6"> <div className="sm:mx-auto sm:w-full sm:max-w-md mb-6">
<img <img
className="mx-auto h-12 w-auto" className="mx-auto h-12 w-auto"
@ -41,8 +41,8 @@ function Login() {
alt="logo" alt="logo"
/> />
</div> </div>
<div className="sm:mx-auto sm:w-full sm:max-w-md"> <div className="sm:mx-auto sm:w-full sm:max-w-md shadow-lg">
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10"> <div className="bg-white dark:bg-gray-800 py-8 px-4 sm:rounded-lg sm:px-10">
<Formik <Formik
initialValues={{ username: "", password: "" }} initialValues={{ username: "", password: "" }}
@ -71,4 +71,4 @@ function Login() {
) )
} }
export default Login; export default Login;

View file

@ -186,16 +186,16 @@ export default function FilterDetails() {
} }
return ( return (
<main className="-mt-48 "> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center">
<h1 className="text-3xl font-bold text-white capitalize"> <h1 className="text-3xl font-bold text-black dark:text-white capitalize">
<NavLink to="/filters" exact={true}> <NavLink to="/filters" exact={true}>
Filters Filters
</NavLink> </NavLink>
</h1> </h1>
<ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />
<h1 className="text-3xl font-bold text-white capitalize">{filter.name}</h1> <h1 className="text-3xl font-bold text-black dark:text-white capitalize">{filter.name}</h1>
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">

View file

@ -29,12 +29,12 @@ export default function Filters() {
return (<p>An error has occurred: </p>); return (<p>An error has occurred: </p>);
return ( return (
<main className="-mt-48 "> <main>
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} /> <FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
<header className="py-10"> <header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
<h1 className="text-3xl font-bold text-white capitalize">Filters</h1> <h1 className="text-3xl font-bold text-black dark:text-white capitalize">Filters</h1>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<button <button
@ -49,7 +49,7 @@ export default function Filters() {
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
<div className="bg-white dark:bg-gray-800 light:rounded-lg light:shadow"> <div className="bg-white dark:bg-gray-800 light:rounded-lg shadow-lg">
<div className="relative inset-0 light:py-3 light:px-3 light:sm:px-3 light:lg:px-3 h-full"> <div className="relative inset-0 light:py-3 light:px-3 light:sm:px-3 light:lg:px-3 h-full">
{data && data.length > 0 ? <FilterList filters={data} /> : {data && data.length > 0 ? <FilterList filters={data} /> :
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />} <EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />}

View file

@ -20,16 +20,23 @@ export const RegexPlayground = () => {
if (match.index === undefined) if (match.index === undefined)
continue; continue;
if (!match.length)
continue;
const start = match.index; const start = match.index;
let length = 0;
match.forEach((group) => length += group.length);
results.push( results.push(
<span key={`match=${start}`}> <span key={`match-${start}`}>
{line.substring(lastIndex, start)} {line.substring(lastIndex, start)}
<span className="bg-green-200 text-black font-bold"> <span className="bg-blue-300 text-black font-bold">
{line.substring(start, start + match.length)} {line.substring(start, start + length)}
</span> </span>
</span> </span>
); );
lastIndex = start + match.length; lastIndex = start + length;
} }
if (lastIndex < line.length) { if (lastIndex < line.length) {
@ -60,7 +67,7 @@ export const RegexPlayground = () => {
<div className="px-6 py-4"> <div className="px-6 py-4">
<label <label
htmlFor="input-regex" htmlFor="input-regex"
className="block text-sm font-medium text-gray-300" className="block text-sm font-medium text-gray-600 dark:text-gray-300"
> >
RegExp filter RegExp filter
</label> </label>
@ -73,7 +80,7 @@ export const RegexPlayground = () => {
/> />
<label <label
htmlFor="input-lines" htmlFor="input-lines"
className="block text-sm font-medium text-gray-300" className="block text-sm font-medium text-gray-600 dark:text-gray-300"
> >
Lines to match Lines to match
</label> </label>