webui: Add dashboard widgets

This commit is contained in:
Kevin Kandlbinder 2022-03-29 15:33:58 +02:00
parent 2d68e30ad2
commit 00f4208ad2
29 changed files with 1443 additions and 174 deletions

View file

@ -154,6 +154,7 @@ type ComplexityRoot struct {
Debug func(childComplexity int) int
HashCheckerConfig func(childComplexity int) int
ID func(childComplexity int) int
Name func(childComplexity int) int
RoomID func(childComplexity int) int
}
@ -814,6 +815,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Room.ID(childComplexity), true
case "Room.name":
if e.complexity.Room.Name == nil {
break
}
return e.complexity.Room.Name(childComplexity), true
case "Room.roomId":
if e.complexity.Room.RoomID == nil {
break
@ -1024,6 +1032,7 @@ type HashCheckerConfig {
type Room {
id: ID!
active: Boolean!
name: String!
roomId: String!
debug: Boolean!
adminPowerLevel: Int!
@ -4694,6 +4703,41 @@ func (ec *executionContext) _Room_active(ctx context.Context, field graphql.Coll
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Room_name(ctx context.Context, field graphql.CollectedField, obj *model.Room) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Room",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Name, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _Room_roomId(ctx context.Context, field graphql.CollectedField, obj *model.Room) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -8225,6 +8269,11 @@ func (ec *executionContext) _Room(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null {
invalids++
}
case "name":
out.Values[i] = ec._Room_name(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "roomId":
out.Values[i] = ec._Room_roomId(ctx, field, obj)
if out.Values[i] == graphql.Null {

View file

@ -5,6 +5,7 @@ import "github.com/Unkn0wnCat/matrix-veles/internal/config"
type Room struct {
ID string `json:"id"`
Active bool `json:"active"`
Name string `json:"name"`
RoomID string `json:"roomId"`
Debug bool `json:"debug"`
AdminPowerLevel int `json:"adminPowerLevel"`
@ -25,7 +26,8 @@ func MakeRoom(room *config.RoomConfig) *Room {
}
return &Room{
ID: room.ID.String(),
ID: room.ID.Hex(),
Name: room.Name,
Active: room.Active,
RoomID: room.RoomID,
Debug: room.Debug,

View file

@ -46,6 +46,7 @@ type HashCheckerConfig {
type Room {
id: ID!
active: Boolean!
name: String!
roomId: String!
debug: Boolean!
adminPowerLevel: Int!

View file

@ -100,13 +100,13 @@ func Run() {
// Set up async tasks
go startSync(matrixClient)
go doInitialUpdate(matrixClient)
go doAdminStateUpdate(matrixClient)
go doRoomStateUpdate(matrixClient)
go func() {
ticker := time.NewTicker(5 * time.Minute)
for {
go doAdminStateUpdate(matrixClient)
go doRoomStateUpdate(matrixClient)
<-ticker.C
}
}()
@ -188,8 +188,8 @@ func doInitialUpdate(matrixClient *mautrix.Client) {
config.RoomConfigInitialUpdate(resp.JoinedRooms, ctx)
}
func doAdminStateUpdate(matrixClient *mautrix.Client) {
ctx, span := tracer.Tracer.Start(tracer.Ctx, "admin_state_update")
func doRoomStateUpdate(matrixClient *mautrix.Client) {
ctx, span := tracer.Tracer.Start(tracer.Ctx, "room_state_update")
defer span.End()
_, requestSpan := tracer.Tracer.Start(ctx, "request_joined_rooms")
@ -222,8 +222,14 @@ func doAdminStateUpdate(matrixClient *mautrix.Client) {
}
}
state, _ := GetRoomNameState(matrixClient, roomId)
roomConfig = config.GetRoomConfig(roomId.String())
roomConfig.Admins = admins
roomConfig.Name = roomId.String()
if state != nil && state.Name != "" {
roomConfig.Name = state.Name
}
err = config.SaveRoomConfig(&roomConfig)
if err != nil {
processSpan.RecordError(err)

View file

@ -84,3 +84,59 @@ func GetRoomPowerLevelState(matrixClient *mautrix.Client, roomId id.RoomID) (*St
return &plEventContent, nil
}
type StateEventRoomName struct {
Type string `json:"type"`
Sender string `json:"sender"`
RoomID string `json:"room_id"`
EventID string `json:"event_id"`
OriginServerTS int64 `json:"origin_server_ts"`
Content StateEventRoomNameContent `json:"content"`
Unsigned struct {
Age int `json:"age"`
} `json:"unsigned"`
}
type StateEventRoomNameContent struct {
Name string
}
func GetRoomNameState(matrixClient *mautrix.Client, roomId id.RoomID) (*StateEventRoomNameContent, error) {
// https://matrix.example.com/_matrix/client/r0/rooms/<roomId.String()>/state
url := matrixClient.BuildURL("rooms", roomId.String(), "state")
res, err := matrixClient.MakeRequest("GET", url, nil, nil)
if err != nil {
return nil, fmt.Errorf("ERROR: Could request room state - %v", err)
}
// res contains an array of state events
var stateEvents []StateEventRoomName
err = json.Unmarshal(res, &stateEvents)
if err != nil {
return nil, fmt.Errorf("ERROR: Could parse room state - %v", err)
}
// plEventContent will hold the final event
var plEventContent StateEventRoomNameContent
found := false
for _, e2 := range stateEvents {
if e2.Type != event.StateRoomName.Type {
continue // If the current event is not of the room name, skip.
}
// This is what we're looking for!
found = true
plEventContent = e2.Content
break
}
if !found {
return nil, fmt.Errorf("ERROR: Could find room power level - %v", err)
}
return &plEventContent, nil
}

View file

@ -29,6 +29,9 @@ type RoomConfig struct {
// Active tells if the bot is active in this room (Set to false on leave/kick/ban)
Active bool `yaml:"active" bson:"active"`
// Name is fetched regularly from the room state
Name string `yaml:"name" bson:"name"`
// RoomID is the rooms ID
RoomID string `yaml:"roomID" bson:"room_id"`

View file

@ -46,6 +46,7 @@ type HashCheckerConfig {
type Room {
id: ID!
active: Boolean!
name: String!
roomId: String!
debug: Boolean!
adminPowerLevel: Int!
@ -316,6 +317,11 @@ input HashCheckerConfigUpdate {
hashCheckMode: HashCheckerMode
}
input ListSubscriptionUpdate {
roomId: ID!
listId: ID!
}
type Mutation {
login(input: Login!): String!
register(input: Register!): String! @hasRole(role: UNAUTHENTICATED)
@ -323,6 +329,8 @@ type Mutation {
removeMXID(input: RemoveMXID!): User! @loggedIn
reconfigureRoom(input: RoomConfigUpdate!): Room! @loggedIn
subscribeToList(input: ListSubscriptionUpdate!): Room! @loggedIn
unsubscribeFromList(input: ListSubscriptionUpdate!): Room! @loggedIn
createEntry(input: CreateEntry!): Entry! @loggedIn
commentEntry(input: CommentEntry!): Entry! @loggedIn

View file

@ -1,5 +1,17 @@
{
"list": {
"none": "Keine Einträge",
"loading": "Lade weitere Einträge...",
"more": "Mehr zeigen",
"end": "Keine weiteren Einträge"
},
"dashboard": {
"helloText": "Ayo {{name}}!"
"helloText": "Ayo {{name}}!",
"my_lists": {
"title": "Meine Listen"
},
"my_rooms": {
"title": "Meine Räume"
}
}
}

View file

@ -1,5 +1,17 @@
{
"list": {
"none": "No entries",
"loading": "Loading more entries...",
"more": "Show more",
"end": "No more entries"
},
"dashboard": {
"helloText": "Ayo {{name}}!"
"helloText": "Ayo {{name}}!",
"my_lists": {
"title": "My Lists"
},
"my_rooms": {
"title": "My Rooms"
}
}
}

View file

@ -12,8 +12,8 @@ import {useTranslation} from "react-i18next";
import {
useQueryLoader, useRelayEnvironment,
} from 'react-relay/hooks';
import Dashboard from "./components/dashboard/Dashboard";
import DashboardQueryGraphql, {DashboardQuery} from "./components/dashboard/__generated__/DashboardQuery.graphql";
import Dashboard from "./components/panel/dashboard/Dashboard";
import DashboardQueryGraphql, {DashboardQuery} from "./components/panel/dashboard/__generated__/DashboardQuery.graphql";
function App() {
const dispatch = useAppDispatch()
@ -52,15 +52,16 @@ function App() {
<Route path={"register"} element={<RegisterView/>} />
</Route>
<Route path={"/"} element={<PanelLayout/>}>
<Route path={""} element={<RequireAuth>{/*<h1><Trans i18nKey={"test"}>Test</Trans></h1> <button onClick={() => {
dispatch(logOut())
}
}>Log out</button> <p>{
JSON.stringify(data.self)
}</p>*/}{dashboardInitialState && <Dashboard initialQueryRef={dashboardInitialState}/>}</RequireAuth>} />
<Route path={"rooms"} element={<RequireAuth><h1>rooms</h1></RequireAuth>} />
<Route path={"hashing/lists"} element={<RequireAuth><h1>lists</h1></RequireAuth>} />
<Route path={"hashing/entries"} element={<RequireAuth><h1>entries</h1></RequireAuth>} />
<Route path={""} element={<RequireAuth>{dashboardInitialState && <Dashboard initialQueryRef={dashboardInitialState}/>}</RequireAuth>} />
<Route path={"rooms"} element={<RequireAuth><h1>rooms</h1></RequireAuth>}>
<Route path={":id"} element={<h1>room detail</h1>} />
</Route>
<Route path={"hashing/lists"} element={<RequireAuth><h1>lists</h1></RequireAuth>}>
<Route path={":id"} element={<h1>list detail</h1>} />
</Route>
<Route path={"hashing/entries"} element={<RequireAuth><h1>entries</h1></RequireAuth>}>
<Route path={":id"} element={<h1>entry detail</h1>} />
</Route>
</Route>
</Routes>
);

View file

@ -0,0 +1,56 @@
@import "../globals";
.list {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-y: auto;
> * {
margin-bottom: var(--veles-layout-padding);
}
.eol {
display: block;
text-align: center;
padding: var(--veles-layout-padding);
opacity: .5;
margin-bottom: 0;
}
.loadMore {
display: block;
text-align: center;
padding: var(--veles-layout-padding);
background-color: transparent;
border: none;
cursor: pointer;
font: inherit;
color: inherit;
margin-bottom: 0;
}
.loader {
margin: 0 auto;
padding: var(--veles-layout-padding);
display: flex;
justify-content: center;
align-items: center;
> svg {
animation-name: spinny-spin;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
}
}
@keyframes spinny-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View file

@ -0,0 +1,32 @@
import React, {useCallback} from "react";
import styles from "./List.module.scss";
import {Box, Loader} from "lucide-react";
import {Trans, useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
import {LoadMoreFn} from "react-relay/relay-hooks/useLoadMoreFunction";
type Props = {
children?: React.ReactNodeArray
isLoadingNext?: boolean
hasNext?: boolean
loadNext?: LoadMoreFn<any>|(() => any)
className?: string
}
const List = (props: Props) => {
const {t} = useTranslation()
return <div className={styles.list + (props.className ? " " + props.className :"")}>
{(!props.children || props.children.length == 0) && <span className={styles.eol}>
<Box width={50} height={50} strokeWidth={1} strokeDasharray={"2px 4px"} /><br/>
<Trans i18nKey={"list.none"}>No entries</Trans>
</span>}
{props.children}
{props.isLoadingNext && <div className={styles.loader} title={t("list.loading", "Loading more entries...")}><Loader/></div>}
{!props.isLoadingNext && props.children && props.children.length > 0 && (props.hasNext ? <button className={styles.loadMore} onClick={() => props.loadNext}><Trans i18nKey={"list.more"}>Show more</Trans></button> : <span className={styles.eol}><Trans i18nKey={"list.end"}>No more entries</Trans></span>)}
</div>
}
export default List

View file

@ -1,77 +0,0 @@
import React, {useCallback} from "react";
import {graphql} from "babel-plugin-relay/macro";
import {PreloadedQuery, usePaginationFragment, usePreloadedQuery} from "react-relay/hooks";
import {DashboardQuery} from "./__generated__/DashboardQuery.graphql";
import {Trans} from "react-i18next";
import {DashboardListsQuery} from "./__generated__/DashboardListsQuery.graphql";
import {DashboardQueryLists$data, DashboardQueryLists$key} from "./__generated__/DashboardQueryLists.graphql";
type Props = {
initialQueryRef: PreloadedQuery<DashboardQuery>,
}
const Dashboard = (props: Props) => {
const data = usePreloadedQuery<DashboardQuery>(
graphql`
query DashboardQuery($first: String, $count: Int) {
self {
username
id
admin
}
...DashboardQueryLists
}
`,
props.initialQueryRef
)
const {data: d2, hasNext, loadNext, refetch} = usePaginationFragment<DashboardListsQuery, DashboardQueryLists$key>(
graphql`
fragment DashboardQueryLists on Query @refetchable(queryName: "DashboardListsQuery") {
rooms(after: $first, first: $count, filter: {canEdit: true}) @connection(key: "DashboardQueryLists_rooms") {
edges {
node {
id
active
debug
hashCheckerConfig {
chatNotice
hashCheckMode
}
}
}
}
}
`,
// @ts-ignore
data
)
const refresh = useCallback(() => {
refetch({}, {fetchPolicy: "network-only"})
}, [])
const name = data.self?.username
return <>
<h1><Trans i18nKey={"dashboard.helloText"}>Ayo {{name}}!</Trans></h1>
{<button onClick={refresh}>Refresh</button>}
<pre>{
JSON.stringify(d2, null, 2)
}</pre>
{hasNext && <button
onClick={() => {
loadNext(2)
}}>
Load more Entries
</button>}
</>
}
export default Dashboard

View file

@ -0,0 +1,40 @@
@import "../../../globals";
.dashMyLists {
background-color: var(--veles-color-surface);
border: thin solid var(--veles-color-border);
padding: var(--veles-layout-padding);
padding-bottom: 0;
border-radius: var(--veles-layout-border-radius);
display: flex;
flex-direction: column;
> * {
flex-shrink: 0;
}
.list {
.listEntry {
background-color: var(--veles-color-surface);
padding: var(--veles-layout-padding);
border-radius: var(--veles-layout-border-radius);
display: flex;
flex-direction: column;
text-decoration: none;
.nameRow {
display: flex;
align-items: center;
.name {
font-weight: 600;
font-size: 1.2em;
}
}
.id {
opacity: .5;
}
}
}
}

View file

@ -0,0 +1,55 @@
import React from "react";
import {PreloadedQuery, usePaginationFragment} from "react-relay/hooks";
import {graphql} from "babel-plugin-relay/macro";
import {Trans, useTranslation} from "react-i18next";
import styles from "./DashMyLists.module.scss";
import {Link} from "react-router-dom";
import List from "../../List";
import {DashMyListsFragment$key} from "./__generated__/DashMyListsFragment.graphql";
import {ComponentDashMyLists} from "./__generated__/ComponentDashMyLists.graphql";
type Props = {
initialQueryRef: DashMyListsFragment$key,
className?: string,
}
const DashMyLists = (props: Props) => {
const {t} = useTranslation()
const {data, refetch, loadNext, hasNext, isLoadingNext} = usePaginationFragment<ComponentDashMyLists, DashMyListsFragment$key>(
graphql`
fragment DashMyListsFragment on Query @refetchable(queryName: "ComponentDashMyLists") {
lists(after: $first, first: $count) @connection(key: "ComponentDashMyLists_lists") {
edges {
node {
id
name
}
}
}
}
`,
props.initialQueryRef
)
return (
<div className={styles.dashMyLists + " " + (props.className || "")}>
<h2><Trans i18nKey={"dashboard.my_lists.title"}>My Lists</Trans></h2>
<List className={styles.list} hasNext={hasNext} isLoadingNext={isLoadingNext} loadNext={loadNext}>
{
data.lists?.edges.map((edge) => {
return <Link className={styles.listEntry} key={edge.node.id} to={"/hashing/lists/"+edge.node.id}>
<div className={styles.nameRow}>
<span className={styles.name}>{edge.node.name}</span>
</div>
<span className={styles.id}>{edge.node.id}</span>
</Link>
})
}
</List>
</div>
)
}
export default DashMyLists

View file

@ -0,0 +1,69 @@
@import "../../../globals";
.dashMyRooms {
background-color: var(--veles-color-surface);
border: thin solid var(--veles-color-border);
padding: var(--veles-layout-padding);
padding-bottom: 0;
border-radius: var(--veles-layout-border-radius);
display: flex;
flex-direction: column;
> * {
flex-shrink: 0;
}
.list {
.room {
background-color: var(--veles-color-surface);
padding: var(--veles-layout-padding);
border-radius: var(--veles-layout-border-radius);
display: flex;
flex-direction: column;
text-decoration: none;
.nameRow {
display: flex;
align-items: center;
.name {
font-weight: 600;
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);
}
}
}
.id {
opacity: .5;
}
}
}
}
@keyframes spinny-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View file

@ -0,0 +1,63 @@
import React from "react";
import {PreloadedQuery, usePaginationFragment} from "react-relay/hooks";
import {graphql} from "babel-plugin-relay/macro";
import {DashboardQuery} from "./__generated__/DashboardQuery.graphql";
import {DashMyRoomsFragment$key} from "./__generated__/DashMyRoomsFragment.graphql";
import {ComponentDashMyRooms} from "./__generated__/ComponentDashMyRooms.graphql";
import {Trans, useTranslation} from "react-i18next";
import styles from "./DashMyRooms.module.scss";
import {Link} from "react-router-dom";
import {Loader, Box} from "lucide-react";
import List from "../../List";
type Props = {
initialQueryRef: DashMyRoomsFragment$key,
className?: string,
}
const DashMyRooms = (props: Props) => {
const {t} = useTranslation()
const {data, refetch, loadNext, hasNext, isLoadingNext} = usePaginationFragment<ComponentDashMyRooms, DashMyRoomsFragment$key>(
graphql`
fragment DashMyRoomsFragment on Query @refetchable(queryName: "ComponentDashMyRooms") {
rooms(after: $first, first: $count, filter: {canEdit: true}) @connection(key: "ComponentDashMyRooms_rooms") {
edges {
node {
id
name
active
debug
roomId
}
}
}
}
`,
props.initialQueryRef
)
return (
<div className={styles.dashMyRooms + " " + (props.className || "")}>
<h2><Trans i18nKey={"dashboard.my_rooms.title"}>My Rooms</Trans></h2>
<List className={styles.list} hasNext={hasNext} isLoadingNext={isLoadingNext} loadNext={loadNext}>
{
data.rooms?.edges.map((edge) => {
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>}
</div>
<span className={styles.id}>{edge.node.roomId}</span>
</Link>
})
}
</List>
</div>
)
}
export default DashMyRooms

View file

@ -0,0 +1,19 @@
@import "../../../globals";
.dashboardGrid {
height: 90%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr;
gap: var(--veles-layout-padding);
@media (max-width: 1300px) {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
@media (max-width: 950px) {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr 1fr;
}
}

View file

@ -0,0 +1,48 @@
import React, {useCallback} from "react";
import {graphql} from "babel-plugin-relay/macro";
import {PreloadedQuery, usePaginationFragment, usePreloadedQuery} from "react-relay/hooks";
import {DashboardQuery} from "./__generated__/DashboardQuery.graphql";
import {Trans} from "react-i18next";
import {DashboardListsQuery} from "./__generated__/DashboardListsQuery.graphql";
import {DashboardQueryLists$data, DashboardQueryLists$key} from "./__generated__/DashboardQueryLists.graphql";
import DashMyRooms from "./DashMyRooms";
import styles from "./Dashboard.module.scss";
import DashMyLists from "./DashMyLists";
type Props = {
initialQueryRef: PreloadedQuery<DashboardQuery>,
}
const Dashboard = (props: Props) => {
const data = usePreloadedQuery<DashboardQuery>(
graphql`
query DashboardQuery($first: String, $count: Int) {
self {
username
id
admin
}
...DashMyRoomsFragment
...DashMyListsFragment
}
`,
props.initialQueryRef
)
const name = data.self?.username
return <>
<h1><Trans i18nKey={"dashboard.helloText"}>Ayo {{name}}!</Trans></h1>
<div className={styles.dashboardGrid}>
<DashMyRooms initialQueryRef={data}/>
<DashMyLists initialQueryRef={data}/>
</div>
</>
}
export default Dashboard

View file

@ -0,0 +1,182 @@
/**
* @generated SignedSource<<6d344122998b6cbd0c9e6d2b2f8d23bd>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type ComponentDashMyLists$variables = {
count?: number | null;
first?: string | null;
};
export type ComponentDashMyLists$data = {
readonly " $fragmentSpreads": FragmentRefs<"DashMyListsFragment">;
};
export type ComponentDashMyLists = {
variables: ComponentDashMyLists$variables;
response: ComponentDashMyLists$data;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "count"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "first"
}
],
v1 = [
{
"kind": "Variable",
"name": "after",
"variableName": "first"
},
{
"kind": "Variable",
"name": "first",
"variableName": "count"
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ComponentDashMyLists",
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "DashMyListsFragment"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ComponentDashMyLists",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "ListConnection",
"kind": "LinkedField",
"name": "lists",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "ListEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "List",
"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": "__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": (v1/*: any*/),
"filters": null,
"handle": "connection",
"key": "ComponentDashMyLists_lists",
"kind": "LinkedHandle",
"name": "lists"
}
]
},
"params": {
"cacheID": "56319a434d00fcd6406c4e9aa88de1fa",
"id": null,
"metadata": {},
"name": "ComponentDashMyLists",
"operationKind": "query",
"text": "query ComponentDashMyLists(\n $count: Int\n $first: String\n) {\n ...DashMyListsFragment\n}\n\nfragment DashMyListsFragment on Query {\n lists(after: $first, first: $count) {\n edges {\n node {\n id\n name\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
}
};
})();
(node as any).hash = "6f41b7c821add7cf4c8c37b343bd6d2d";
export default node;

View file

@ -0,0 +1,212 @@
/**
* @generated SignedSource<<a4ab3c123e20e4c610b878fd82d98eb5>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type ComponentDashMyRooms$variables = {
count?: number | null;
first?: string | null;
};
export type ComponentDashMyRooms$data = {
readonly " $fragmentSpreads": FragmentRefs<"DashMyRoomsFragment">;
};
export type ComponentDashMyRooms = {
variables: ComponentDashMyRooms$variables;
response: ComponentDashMyRooms$data;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "count"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "first"
}
],
v1 = [
{
"kind": "Variable",
"name": "after",
"variableName": "first"
},
{
"kind": "Literal",
"name": "filter",
"value": {
"canEdit": true
}
},
{
"kind": "Variable",
"name": "first",
"variableName": "count"
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ComponentDashMyRooms",
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "DashMyRoomsFragment"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ComponentDashMyRooms",
"selections": [
{
"alias": null,
"args": (v1/*: 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": (v1/*: any*/),
"filters": [
"filter"
],
"handle": "connection",
"key": "ComponentDashMyRooms_rooms",
"kind": "LinkedHandle",
"name": "rooms"
}
]
},
"params": {
"cacheID": "900fab96de8f0dd453020c4443727ed4",
"id": null,
"metadata": {},
"name": "ComponentDashMyRooms",
"operationKind": "query",
"text": "query ComponentDashMyRooms(\n $count: Int\n $first: String\n) {\n ...DashMyRoomsFragment\n}\n\nfragment DashMyRoomsFragment 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 = "fc18a3a1a32649d6d9fd694500167a4c";
export default node;

View file

@ -0,0 +1,163 @@
/**
* @generated SignedSource<<f487153d3e4df6d37d1c3f952fde6252>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment, RefetchableFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type DashMyListsFragment$data = {
readonly lists: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly name: string;
};
}>;
} | null;
readonly " $fragmentType": "DashMyListsFragment";
};
export type DashMyListsFragment$key = {
readonly " $data"?: DashMyListsFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"DashMyListsFragment">;
};
const node: ReaderFragment = (function(){
var v0 = [
"lists"
];
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('./ComponentDashMyLists.graphql')
}
},
"name": "DashMyListsFragment",
"selections": [
{
"alias": "lists",
"args": null,
"concreteType": "ListConnection",
"kind": "LinkedField",
"name": "__ComponentDashMyLists_lists_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "ListEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "List",
"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": "__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
}
],
"type": "Query",
"abstractKey": null
};
})();
(node as any).hash = "6f41b7c821add7cf4c8c37b343bd6d2d";
export default node;

View file

@ -0,0 +1,195 @@
/**
* @generated SignedSource<<4b90432feeb0d507f1f25105ee5c5bd5>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment, RefetchableFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type DashMyRoomsFragment$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": "DashMyRoomsFragment";
};
export type DashMyRoomsFragment$key = {
readonly " $data"?: DashMyRoomsFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"DashMyRoomsFragment">;
};
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('./ComponentDashMyRooms.graphql')
}
},
"name": "DashMyRoomsFragment",
"selections": [
{
"alias": "rooms",
"args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"canEdit": true
}
}
],
"concreteType": "RoomConnection",
"kind": "LinkedField",
"name": "__ComponentDashMyRooms_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": "__ComponentDashMyRooms_rooms_connection(filter:{\"canEdit\":true})"
}
],
"type": "Query",
"abstractKey": null
};
})();
(node as any).hash = "fc18a3a1a32649d6d9fd694500167a4c";
export default node;

View file

@ -1,5 +1,5 @@
/**
* @generated SignedSource<<3a1cacd9d1f9fc8ae498211b0141af63>>
* @generated SignedSource<<a34bf324ec46a1d657a8967d773745fe>>
* @lightSyntaxTransform
* @nogrep
*/
@ -20,7 +20,7 @@ export type DashboardQuery$data = {
readonly id: string;
readonly admin: boolean | null;
} | null;
readonly " $fragmentSpreads": FragmentRefs<"DashboardQueryLists">;
readonly " $fragmentSpreads": FragmentRefs<"DashMyRoomsFragment" | "DashMyListsFragment">;
};
export type DashboardQuery = {
variables: DashboardQuery$variables;
@ -71,12 +71,18 @@ v3 = {
],
"storageKey": null
},
v4 = [
{
"kind": "Variable",
"name": "after",
"variableName": "first"
},
v4 = {
"kind": "Variable",
"name": "after",
"variableName": "first"
},
v5 = {
"kind": "Variable",
"name": "first",
"variableName": "count"
},
v6 = [
(v4/*: any*/),
{
"kind": "Literal",
"name": "filter",
@ -84,11 +90,57 @@ v4 = [
"canEdit": true
}
},
{
"kind": "Variable",
"name": "first",
"variableName": "count"
}
(v5/*: any*/)
],
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v10 = {
"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
},
v11 = [
(v4/*: any*/),
(v5/*: any*/)
];
return {
"fragment": {
@ -104,7 +156,12 @@ return {
{
"args": null,
"kind": "FragmentSpread",
"name": "DashboardQueryLists"
"name": "DashMyRoomsFragment"
},
{
"args": null,
"kind": "FragmentSpread",
"name": "DashMyListsFragment"
}
],
"type": "Query",
@ -122,7 +179,7 @@ return {
(v3/*: any*/),
{
"alias": null,
"args": (v4/*: any*/),
"args": (v6/*: any*/),
"concreteType": "RoomConnection",
"kind": "LinkedField",
"name": "rooms",
@ -145,6 +202,7 @@ return {
"plural": false,
"selections": [
(v2/*: any*/),
(v7/*: any*/),
{
"alias": null,
"args": null,
@ -162,100 +220,93 @@ return {
{
"alias": null,
"args": null,
"concreteType": "HashCheckerConfig",
"kind": "LinkedField",
"name": "hashCheckerConfig",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "chatNotice",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hashCheckMode",
"storageKey": null
}
],
"kind": "ScalarField",
"name": "roomId",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
(v8/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
(v9/*: any*/)
],
"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
}
(v10/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": (v4/*: any*/),
"args": (v6/*: any*/),
"filters": [
"filter"
],
"handle": "connection",
"key": "DashboardQueryLists_rooms",
"key": "ComponentDashMyRooms_rooms",
"kind": "LinkedHandle",
"name": "rooms"
},
{
"alias": null,
"args": (v11/*: any*/),
"concreteType": "ListConnection",
"kind": "LinkedField",
"name": "lists",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "ListEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "List",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v7/*: any*/),
(v8/*: any*/)
],
"storageKey": null
},
(v9/*: any*/)
],
"storageKey": null
},
(v10/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": (v11/*: any*/),
"filters": null,
"handle": "connection",
"key": "ComponentDashMyLists_lists",
"kind": "LinkedHandle",
"name": "lists"
}
]
},
"params": {
"cacheID": "f41a91749c22f4e9026e9ae1d21e72a9",
"cacheID": "72b0fa7a61819018fd38de7564f1f3cb",
"id": null,
"metadata": {},
"name": "DashboardQuery",
"operationKind": "query",
"text": "query DashboardQuery(\n $first: String\n $count: Int\n) {\n self {\n username\n id\n admin\n }\n ...DashboardQueryLists\n}\n\nfragment DashboardQueryLists on Query {\n rooms(after: $first, first: $count, filter: {canEdit: true}) {\n edges {\n node {\n id\n active\n debug\n hashCheckerConfig {\n chatNotice\n hashCheckMode\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
"text": "query DashboardQuery(\n $first: String\n $count: Int\n) {\n self {\n username\n id\n admin\n }\n ...DashMyRoomsFragment\n ...DashMyListsFragment\n}\n\nfragment DashMyListsFragment on Query {\n lists(after: $first, first: $count) {\n edges {\n node {\n id\n name\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment DashMyRoomsFragment 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 = "dc06e10a331a27f1a5904147ae650a71";
(node as any).hash = "5a8ab0cabd608a79789053ba85880813";
export default node;

View file

@ -26,7 +26,12 @@ html, body, #root {
--veles-color-accent: #007300;
--veles-color-error: #e3373e;
--veles-color-red: #e3373e;
--veles-color-blue: #37a7e3;
--veles-color-green: #5ce337;
--veles-layout-padding: 20px;
--veles-layout-padding-inverse: -20px;
--veles-layout-padding-slim: 10px;
--veles-layout-padding-wide: 40px;
--veles-layout-border-radius: 10px;
@ -42,6 +47,12 @@ html, body, #root {
}
}
h1, h2, h3, h4, h5, h6 {
&:first-child {
margin-top: 0;
}
}
a {
color: inherit;
text-decoration: underline dotted currentColor;