webui: Add rooms table and slide-over

This commit is contained in:
Kevin Kandlbinder 2022-07-20 15:12:55 +02:00
parent 464591f77a
commit 42a5e527eb
15 changed files with 694 additions and 30 deletions

View file

@ -1804,9 +1804,9 @@
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@tsconfig/docusaurus@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@tsconfig/docusaurus/-/docusaurus-1.0.4.tgz#fc40f87a672568678d83533dd4031a09d75877ca"
integrity sha512-I6sziQAzLrrqj9r6S26c7aOAjfGVXIE7gWdNONPwnpDcHiMRMQut1s1YCi/APem3dOy23tAb2rvHfNtGCaWuUQ==
version "1.0.5"
resolved "https://registry.yarnpkg.com/@tsconfig/docusaurus/-/docusaurus-1.0.5.tgz#5298c5b0333c6263f06c3149b38ebccc9f169a4e"
integrity sha512-KM/TuJa9fugo67dTGx+ktIqf3fVc077J6jwHu845Hex4EQf7LABlNonP/mohDKT0cmncdtlYVHHF74xR/YpThg==
"@types/body-parser@*":
version "1.19.2"
@ -7254,9 +7254,9 @@ typedarray-to-buffer@^3.1.5:
is-typedarray "^1.0.0"
typescript@^4.5.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4"
integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==
version "4.6.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c"
integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==
ua-parser-js@^0.7.30:
version "0.7.31"

View file

@ -15,6 +15,7 @@
"@types/react-helmet": "^6.1.5",
"@types/react-redux": "^7.1.22",
"@types/react-relay": "^13.0.1",
"@types/react-table": "^7.7.10",
"@types/relay-runtime": "^13.0.2",
"axios": "^0.26.0",
"hamburger-react": "^2.4.1",
@ -31,6 +32,7 @@
"react-relay": "^13.2.0",
"react-router-dom": "6",
"react-scripts": "5.0.0",
"react-table": "^7.7.0",
"sass": "^1.49.9",
"typescript": "~4.1.5"
},

View file

@ -14,6 +14,8 @@ import {
} from 'react-relay/hooks';
import Dashboard from "./components/panel/dashboard/Dashboard";
import DashboardQueryGraphql, {DashboardQuery} from "./components/panel/dashboard/__generated__/DashboardQuery.graphql";
import Rooms from "./components/panel/rooms/Rooms";
import RoomsQueryGraphql, {RoomsQuery} from "./components/panel/rooms/__generated__/RoomsQuery.graphql";
function App() {
const dispatch = useAppDispatch()
@ -25,6 +27,10 @@ function App() {
DashboardQueryGraphql
)
const [roomsInitialState, loadRoomsQuery, disposeRoomsQuery] = useQueryLoader<RoomsQuery>(
RoomsQueryGraphql
)
// This needs to be here to prevent a weird bug
useTranslation()
@ -37,10 +43,12 @@ function App() {
useEffect(() => {
if(auth.jwt !== null) {
loadQuery({})
loadRoomsQuery({})
return
}
disposeQuery()
disposeRoomsQuery()
environment.getStore().notify(undefined, true)
}, [auth])
@ -53,7 +61,7 @@ function App() {
</Route>
<Route path={"/"} element={<PanelLayout/>}>
<Route path={""} element={<RequireAuth>{dashboardInitialState && <Dashboard initialQueryRef={dashboardInitialState}/>}</RequireAuth>} />
<Route path={"rooms"} element={<RequireAuth><h1>rooms</h1></RequireAuth>}>
<Route path={"rooms"} element={<RequireAuth>{roomsInitialState && <Rooms initialQueryRef={roomsInitialState}/>}</RequireAuth>}>
<Route path={":id"} element={<h1>room detail</h1>} />
</Route>
<Route path={"hashing/lists"} element={<RequireAuth><h1>lists</h1></RequireAuth>}>

View file

@ -0,0 +1,21 @@
@mixin badges {
.badge {
padding: 2px var(--veles-layout-padding-slim);
background-color: var(--veles-color-surface);
border-radius: var(--veles-layout-border-radius);
margin-left: 10px;
border: thin solid var(--veles-color-border);
&.red {
border-color: var(--veles-color-red);
}
&.blue {
border-color: var(--veles-color-blue);
}
&.green {
border-color: var(--veles-color-green);
}
}
}

View file

@ -31,25 +31,7 @@
font-size: 1.2em;
}
.badge {
padding: 2px var(--veles-layout-padding-slim);
background-color: var(--veles-color-surface);
border-radius: var(--veles-layout-border-radius);
margin-left: 10px;
border: thin solid var(--veles-color-border);
&.red {
border-color: var(--veles-color-red);
}
&.blue {
border-color: var(--veles-color-blue);
}
&.green {
border-color: var(--veles-color-green);
}
}
@include badges;
}
.id {

View file

@ -47,9 +47,9 @@ const DashMyRooms = (props: Props) => {
return <Link className={styles.room} key={edge.node.id} to={"/rooms/"+edge.node.id}>
<div className={styles.nameRow}>
<span className={styles.name}>{edge.node.name}</span>
{edge.node.debug && <span className={styles.badge + " " + styles.blue}>Debug</span>}
{!edge.node.active && <span className={styles.badge + " " + styles.red}>Inactive</span>}
{edge.node.active && <span className={styles.badge + " " + styles.green}>Active</span>}
{edge.node.debug && <span className={styles.badge + " " + styles.blue}><Trans i18nKey={"rooms.debug"}>Debug</Trans></span>}
{!edge.node.active && <span className={styles.badge + " " + styles.red}><Trans i18nKey={"rooms.inactive"}>Inactive</Trans></span>}
{edge.node.active && <span className={styles.badge + " " + styles.green}><Trans i18nKey={"rooms.active"}>Active</Trans></span>}
</div>
<span className={styles.id}>{edge.node.roomId}</span>
</Link>

View file

@ -0,0 +1,77 @@
@import "../../../globals";
$slideOverBreakpoint: 1000px;
.roomsContainer {
display: flex;
height: calc(100% + 2*var(--veles-layout-padding));
margin: var(--veles-layout-padding-inverse);
width: calc(100% + 2*var(--veles-layout-padding));
overflow: hidden;
.roomsOverview {
flex-grow: 1;
flex-shrink: 1;
width: 100px;
padding: var(--veles-layout-padding);
transition: margin-right .25s;
&.leaveSpace {
margin-right: 400px;
@media(max-width: $slideOverBreakpoint) {
margin-right: 0;
}
}
}
.slideOver {
position: absolute;
top: 0;
right: -400px;
height: 100%;
width: 400px;
border-left: thin solid var(--veles-color-border);
transition: right .25s, border-left .25s, width .25s;
@media(max-width: $slideOverBreakpoint) {
width: 100%;
border-left: 0 solid var(--veles-color-border);
margin-right: 0;
right: -100%;
}
background-color: var(--veles-color-background);
&.active {
right: 0;
}
.slideOverContent {
padding: var(--veles-layout-padding);
}
.slideOverHeader {
display: flex;
border-bottom: thin solid var(--veles-color-border);
align-items: center;
>* {
padding: var(--veles-layout-padding-slim) var(--veles-layout-padding);
}
> span {
flex-grow: 1;
}
> button {
margin: 0;
background: transparent;
font: inherit;
color: inherit;
border: none;
cursor: pointer;
}
}
}
}

View file

@ -0,0 +1,49 @@
import React from "react";
import {useNavigate, useOutlet} from "react-router-dom";
import styles from "./Rooms.module.scss";
import {Trans} from "react-i18next";
import RoomsTable from "./RoomsTable";
import {PreloadedQuery, usePreloadedQuery} from "react-relay/hooks";
import {graphql} from "babel-plugin-relay/macro";
import {RoomsQuery} from "./__generated__/RoomsQuery.graphql";
import {X} from "lucide-react";
type Props = {
initialQueryRef: PreloadedQuery<RoomsQuery>,
}
const Rooms = ({initialQueryRef}: Props) => {
const outlet = useOutlet()
const navigate = useNavigate()
const data = usePreloadedQuery(
graphql`
query RoomsQuery($first: String, $count: Int) {
...RoomsTableFragment
}
`,
initialQueryRef
)
return <div className={styles.roomsContainer}>
<div className={styles.roomsOverview + (outlet ? " "+styles.leaveSpace : "")}>
<h1><Trans i18nKey={"rooms.title"}>My Rooms</Trans></h1>
<RoomsTable initialQueryRef={data}/>
</div>
<div className={styles.slideOver + (outlet ? " "+styles.active : "")}>
<div className={styles.slideOverHeader}>
<span><Trans i18nKey={"rooms.details"}>Details</Trans></span>
<button onClick={() => navigate("/rooms")}><X/></button>
</div>
<div className={styles.slideOverContent}>
{outlet}
</div>
</div>
</div>
}
export default Rooms

View file

@ -0,0 +1,35 @@
@import "../../../globals";
.roomsTableWrapper {
width: 100%;
overflow-y: scroll;
.roomsTable {
width: 100%;
text-align: left;
white-space: nowrap;
border-spacing: 0;
border-collapse: collapse;
th, td {
padding: 10px 5px;
}
thead tr {
border-bottom: thin solid var(--veles-color-border-highlight);
}
tbody tr {
cursor:pointer;
border-bottom: thin solid var(--veles-color-border);
@include badges;
&:hover {
background: var(--veles-color-surface);
}
}
}
}

View file

@ -0,0 +1,62 @@
import React from "react";
import {usePaginationFragment} from "react-relay/hooks";
import {graphql} from "babel-plugin-relay/macro";
import {RoomsTableFragment$key} from "./__generated__/RoomsTableFragment.graphql";
import {useTable} from "react-table";
import styles from "./RoomsTable.module.scss";
import {useNavigate} from "react-router-dom";
import {Trans} from "react-i18next";
type Props = {
initialQueryRef: RoomsTableFragment$key,
}
const RoomsTable = ({initialQueryRef}: Props) => {
const {data, refetch, loadNext, hasNext, isLoadingNext} = usePaginationFragment(graphql`
fragment RoomsTableFragment on Query @refetchable(queryName: "RoomsTableFragment") {
rooms(after: $first, first: $count, filter: {canEdit: true}) @connection(key: "RoomsTableFragment_rooms") {
edges {
node {
id
name
active
debug
roomId
}
}
}
}
`, initialQueryRef)
const navigate = useNavigate()
return <div className={styles.roomsTableWrapper}>
<table className={styles.roomsTable}>
<thead>
<tr>
<th></th>
<th><Trans i18nKey={"rooms.name"}>Name</Trans></th>
<th><Trans i18nKey={"rooms.id"}>Room ID</Trans></th>
</tr>
</thead>
<tbody>
{
data.rooms?.edges.map((edge) => {
return <tr onClick={() => {navigate("/rooms/"+edge.node.id)}}>
<td>
{edge.node.debug && <span className={styles.badge + " " + styles.blue}><Trans i18nKey={"rooms.debug"}>Debug</Trans></span>}
{!edge.node.active && <span className={styles.badge + " " + styles.red}><Trans i18nKey={"rooms.inactive"}>Inactive</Trans></span>}
{edge.node.active && <span className={styles.badge + " " + styles.green}><Trans i18nKey={"rooms.active"}>Active</Trans></span>}
</td>
<td>{edge.node.name}</td>
<td>{edge.node.roomId}</td>
</tr>;
})
}
</tbody>
</table>
</div>
}
export default RoomsTable

View file

@ -0,0 +1,216 @@
/**
* @generated SignedSource<<bab85af474b5800aeae642e02d92a8fa>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type RoomsQuery$variables = {
first?: string | null;
count?: number | null;
};
export type RoomsQuery$data = {
readonly " $fragmentSpreads": FragmentRefs<"RoomsTableFragment">;
};
export type RoomsQuery = {
variables: RoomsQuery$variables;
response: RoomsQuery$data;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "count"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "first"
},
v2 = [
{
"kind": "Variable",
"name": "after",
"variableName": "first"
},
{
"kind": "Literal",
"name": "filter",
"value": {
"canEdit": true
}
},
{
"kind": "Variable",
"name": "first",
"variableName": "count"
}
];
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "RoomsQuery",
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "RoomsTableFragment"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "RoomsQuery",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "RoomConnection",
"kind": "LinkedField",
"name": "rooms",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "RoomEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Room",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "active",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "debug",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "roomId",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": (v2/*: any*/),
"filters": [
"filter"
],
"handle": "connection",
"key": "RoomsTableFragment_rooms",
"kind": "LinkedHandle",
"name": "rooms"
}
]
},
"params": {
"cacheID": "c933de605f2929607b671c06737a6f53",
"id": null,
"metadata": {},
"name": "RoomsQuery",
"operationKind": "query",
"text": "query RoomsQuery(\n $first: String\n $count: Int\n) {\n ...RoomsTableFragment\n}\n\nfragment RoomsTableFragment on Query {\n rooms(after: $first, first: $count, filter: {canEdit: true}) {\n edges {\n node {\n id\n name\n active\n debug\n roomId\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
}
};
})();
(node as any).hash = "c86e1dca69190955b66fc4f94477a8fc";
export default node;

View file

@ -0,0 +1,195 @@
/**
* @generated SignedSource<<b588731e75572f0b23592574eb079dc4>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment, RefetchableFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type RoomsTableFragment$data = {
readonly rooms: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly name: string;
readonly active: boolean;
readonly debug: boolean;
readonly roomId: string;
};
}>;
} | null;
readonly " $fragmentType": "RoomsTableFragment";
};
export type RoomsTableFragment$key = {
readonly " $data"?: RoomsTableFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"RoomsTableFragment">;
};
const node: ReaderFragment = (function(){
var v0 = [
"rooms"
];
return {
"argumentDefinitions": [
{
"kind": "RootArgument",
"name": "count"
},
{
"kind": "RootArgument",
"name": "first"
}
],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": "count",
"cursor": "first",
"direction": "forward",
"path": (v0/*: any*/)
}
],
"refetch": {
"connection": {
"forward": {
"count": "count",
"cursor": "first"
},
"backward": null,
"path": (v0/*: any*/)
},
"fragmentPathInResult": [],
"operation": require('./RoomsTableFragment.graphql')
}
},
"name": "RoomsTableFragment",
"selections": [
{
"alias": "rooms",
"args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"canEdit": true
}
}
],
"concreteType": "RoomConnection",
"kind": "LinkedField",
"name": "__RoomsTableFragment_rooms_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "RoomEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Room",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "active",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "debug",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "roomId",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "__RoomsTableFragment_rooms_connection(filter:{\"canEdit\":true})"
}
],
"type": "Query",
"abstractKey": null
};
})();
(node as any).hash = "6e31abc6b1af82d05defa80dd7242e44";
export default node;

View file

@ -22,6 +22,7 @@ html, body, #root {
--veles-color-background: #0d0d0d;
--veles-color-foreground: #fff;
--veles-color-border: #1c1c1c;
--veles-color-border-highlight: #464646;
--veles-color-surface: #ffffff08;
--veles-color-accent: #007300;
--veles-color-error: #e3373e;
@ -42,6 +43,7 @@ html, body, #root {
--veles-color-background: #f2f2f2;
--veles-color-foreground: #0d0d0d;
--veles-color-border: #cccccc;
--veles-color-border-highlight: #9b9b9b;
--veles-color-surface: #00000008;
--veles-color-accent: #007300;
}

View file

@ -152,6 +152,7 @@ $navBreakpoint: 650px;
>* {
flex-shrink: 0;
flex-basis: 0;
}
.dropdown {
@ -200,8 +201,10 @@ $navBreakpoint: 650px;
> main {
padding: var(--veles-layout-padding);
height: calc(100vh - 66px);
overflow: auto;
overflow-y: auto;
overflow-x: hidden;
flex-grow: 1;
position: relative;
}
}
}

View file

@ -1973,6 +1973,13 @@
"@types/react" "*"
"@types/relay-runtime" "*"
"@types/react-table@^7.7.10":
version "7.7.10"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.10.tgz#ca8bb5420bfeae964ff61682f31f1cadfcfee726"
integrity sha512-yt7FHv/2cFsucStSWLBOB3OmsRZF08DvVHzz8Zg41B4tzRL6pQ+5VYvmhaR1dKS//tDG4UOJ1RQJPEINHYoRtg==
dependencies:
"@types/react" "*"
"@types/react@*":
version "17.0.43"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55"
@ -8051,6 +8058,11 @@ react-side-effect@^2.1.0:
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3"
integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==
react-table@^7.7.0:
version "7.7.0"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.7.0.tgz#e2ce14d7fe3a559f7444e9ecfe8231ea8373f912"
integrity sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==
react@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"