Timeline UI
This commit is contained in:
parent
0ba2956562
commit
78ad0b7762
11 changed files with 144 additions and 23 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
from peewee import JOIN, fn
|
from peewee import JOIN, fn
|
||||||
|
|
||||||
from util import sqlite_time
|
from util import sqlite_time
|
||||||
|
|
@ -171,5 +172,13 @@ def timeline_event_put(type, text, user, orders_pool, order_status=None, extra=N
|
||||||
user=user,
|
user=user,
|
||||||
orders_pool=orders_pool,
|
orders_pool=orders_pool,
|
||||||
order_status=order_status,
|
order_status=order_status,
|
||||||
extra=extra
|
extra=json.dumps(extra) if extra is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
|
def timeline_event_recent(user_id):
|
||||||
|
return (TimelineEvent
|
||||||
|
.select()
|
||||||
|
.where(TimelineEvent.user_id == user_id)
|
||||||
|
.order_by(TimelineEvent.updated_at.desc())
|
||||||
|
.limit(10)
|
||||||
)
|
)
|
||||||
2
main.py
2
main.py
|
|
@ -24,7 +24,7 @@ async def do_order_issue(order_pool_id):
|
||||||
logger.info(f'Issued order id {order_status.id}')
|
logger.info(f'Issued order id {order_status.id}')
|
||||||
timeline_event_put(
|
timeline_event_put(
|
||||||
TIMELINE_ORDER_ISSUED,
|
TIMELINE_ORDER_ISSUED,
|
||||||
f"Order issued from {orders_pool.name} - {order_status.text.split("\n")[0]}",
|
order_status.text.split("\n")[0],
|
||||||
user=orders_pool.user,
|
user=orders_pool.user,
|
||||||
orders_pool=orders_pool,
|
orders_pool=orders_pool,
|
||||||
order_status=order_status,
|
order_status=order_status,
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ async def order_check(order_status_id):
|
||||||
logger.info('Confirmed order %s' % (order_status.id))
|
logger.info('Confirmed order %s' % (order_status.id))
|
||||||
timeline_event_put(
|
timeline_event_put(
|
||||||
TIMELINE_ORDER_CONFIRMED,
|
TIMELINE_ORDER_CONFIRMED,
|
||||||
f'Order confirmed - {order_status.text.split("\n")[0]}',
|
order_status.text.split("\n")[0],
|
||||||
order_status.user,
|
order_status.user,
|
||||||
order_status.pool,
|
order_status.pool,
|
||||||
order_status,
|
order_status,
|
||||||
|
|
@ -222,7 +222,7 @@ async def order_check(order_status_id):
|
||||||
punishment_status = issue_result['order_status']
|
punishment_status = issue_result['order_status']
|
||||||
timeline_event_put(
|
timeline_event_put(
|
||||||
TIMELINE_ORDER_PUNISHED,
|
TIMELINE_ORDER_PUNISHED,
|
||||||
f'Order punished - {punishment_status.text.split("\n")[0]}',
|
punishment_status.text.split("\n")[0],
|
||||||
punishment_status.user,
|
punishment_status.user,
|
||||||
punishment_status.pool,
|
punishment_status.pool,
|
||||||
punishment_status,
|
punishment_status,
|
||||||
|
|
@ -233,7 +233,7 @@ async def order_check(order_status_id):
|
||||||
elif 'reason' in issue_result:
|
elif 'reason' in issue_result:
|
||||||
timeline_event_put(
|
timeline_event_put(
|
||||||
TIMELINE_ORDER_NOT_PUNISHED,
|
TIMELINE_ORDER_NOT_PUNISHED,
|
||||||
f'Order not punished - {issue_result['reason']}',
|
issue_result['reason'],
|
||||||
order_status.user,
|
order_status.user,
|
||||||
order_status.pool,
|
order_status.pool,
|
||||||
order_status
|
order_status
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ class OrderScheduler():
|
||||||
)
|
)
|
||||||
timeline_event_put(
|
timeline_event_put(
|
||||||
TIMELINE_ORDER_ISSUED,
|
TIMELINE_ORDER_ISSUED,
|
||||||
f"Order issued - {order_status.text.split("\n")[0]}",
|
order_status.text.split("\n")[0],
|
||||||
user=orders_pool.user,
|
user=orders_pool.user,
|
||||||
orders_pool=orders_pool,
|
orders_pool=orders_pool,
|
||||||
order_status=order_status,
|
order_status=order_status,
|
||||||
|
|
|
||||||
3
util.py
3
util.py
|
|
@ -13,6 +13,9 @@ def timezone():
|
||||||
def sqlite_time(dt):
|
def sqlite_time(dt):
|
||||||
return dt.astimezone(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')
|
return dt.astimezone(datetime.UTC).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
def time_sqlite(dt):
|
||||||
|
return dt.replace(tzinfo=datetime.UTC)
|
||||||
|
|
||||||
def order_time(str):
|
def order_time(str):
|
||||||
order_time_arr = list(map(int, str.split(':')))
|
order_time_arr = list(map(int, str.split(':')))
|
||||||
return datetime.time(
|
return datetime.time(
|
||||||
|
|
|
||||||
32
web/api.py
32
web/api.py
|
|
@ -1,11 +1,13 @@
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
|
import json
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask import Blueprint, jsonify, abort, request
|
from flask import Blueprint, jsonify, abort, request
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer
|
from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer
|
||||||
from db.queries import user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_mastodon_server_set, user_preferences_set
|
from db.queries import timeline_event_recent, user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_mastodon_server_set, user_preferences_set
|
||||||
from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE
|
from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE
|
||||||
|
from util import time_sqlite
|
||||||
|
|
||||||
RE_MASTODON_ATTN_LIST = re.compile(r'^(@(\w+)(@([\w\.]+))? )*(@\w+)(@[\w\.]+)?$')
|
RE_MASTODON_ATTN_LIST = re.compile(r'^(@(\w+)(@([\w\.]+))? )*(@\w+)(@[\w\.]+)?$')
|
||||||
|
|
||||||
|
|
@ -44,6 +46,31 @@ def profile():
|
||||||
|
|
||||||
return ('', 204)
|
return ('', 204)
|
||||||
|
|
||||||
|
def maybe_json(str):
|
||||||
|
try:
|
||||||
|
return json.loads(str)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@api.route('/timeline')
|
||||||
|
@login_required
|
||||||
|
def timeline():
|
||||||
|
return jsonify([
|
||||||
|
{
|
||||||
|
"id": t.id,
|
||||||
|
"updated_at": time_sqlite(t.updated_at).isoformat(),
|
||||||
|
"type": t.type,
|
||||||
|
"text": t.text,
|
||||||
|
"extra": maybe_json(t.extra),
|
||||||
|
"username": t.user.telegram_username,
|
||||||
|
"orders_pool": {
|
||||||
|
"id": t.orders_pool.id,
|
||||||
|
"name": t.orders_pool.name,
|
||||||
|
},
|
||||||
|
"order_status": t.order_status.text if t.order_status is not None else None,
|
||||||
|
} for t in timeline_event_recent(current_user.db_user.id)
|
||||||
|
])
|
||||||
|
|
||||||
@api.route('/mastodon_oauth')
|
@api.route('/mastodon_oauth')
|
||||||
@login_required
|
@login_required
|
||||||
def mastodon_oauth():
|
def mastodon_oauth():
|
||||||
|
|
@ -287,7 +314,8 @@ def sub_order_set(username, set_id, sub):
|
||||||
)
|
)
|
||||||
|
|
||||||
update_add_ons(order_to_update, updated_order['add_ons'])
|
update_add_ons(order_to_update, updated_order['add_ons'])
|
||||||
except:
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
transaction.rollback()
|
transaction.rollback()
|
||||||
abort(500)
|
abort(500)
|
||||||
elif request.method == 'DELETE':
|
elif request.method == 'DELETE':
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"@mantine/form": "^8.3.13",
|
"@mantine/form": "^8.3.13",
|
||||||
"@mantine/notifications": "^8.3.13",
|
"@mantine/notifications": "^8.3.13",
|
||||||
"@tabler/icons-react": "^3.36.1",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Container, Text, Title, Flex, Card, Image, Box } from "@mantine/core";
|
import { Text, Title, Flex, Card, Image } from "@mantine/core";
|
||||||
import { useLoaderData } from "react-router";
|
import { useLoaderData } from "react-router";
|
||||||
import { IconPencil } from "@tabler/icons-react";
|
import { IconPencil } from "@tabler/icons-react";
|
||||||
import { NavigateButton } from "./NavigateButton";
|
import { NavigateButton } from "./NavigateButton";
|
||||||
import { OrderSetProps, OrderSets } from "./OrderSets";
|
import { OrderSetProps, OrderSets } from "./OrderSets";
|
||||||
import { useUserContext } from "./UserContext";
|
import { useUserContext } from "./UserContext";
|
||||||
|
import { TimelineList } from "./TimelineList";
|
||||||
|
|
||||||
export const subsListLoader = () =>
|
export const subsListLoader = () =>
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch("/api/orders/").then((response) => response.json()),
|
fetch("/api/orders/").then((response) => response.json()),
|
||||||
fetch("/api/subs").then((response) => response.json()),
|
fetch("/api/subs").then((response) => response.json()),
|
||||||
|
fetch("/api/timeline").then((response) => response.json()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
interface SubsListProps {
|
interface SubsListProps {
|
||||||
|
|
@ -58,14 +60,17 @@ const SubsList: React.FC<SubsListProps> = ({ subs }) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Dashboard: React.FC = () => {
|
export const Dashboard: React.FC = () => {
|
||||||
const [orderSets, subs] =
|
const [orderSets, subs, timeline] =
|
||||||
useLoaderData<[OrderSetProps["orderSets"], SubsListProps["subs"]]>();
|
useLoaderData<
|
||||||
|
[OrderSetProps["orderSets"], SubsListProps["subs"], TimelineEvent[]]
|
||||||
|
>();
|
||||||
const { username } = useUserContext();
|
const { username } = useUserContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OrderSets orderSets={orderSets} username={username} />
|
<OrderSets orderSets={orderSets} username={username} />
|
||||||
{subs.length > 0 ? <SubsList subs={subs} /> : null}
|
{subs.length > 0 ? <SubsList subs={subs} /> : null}
|
||||||
|
{timeline.length > 0 ? <TimelineList timeline={timeline} /> : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,5 @@
|
||||||
import { Container, Title, Card, Text, Box, Flex, Badge } from "@mantine/core";
|
|
||||||
import { TimeValue } from "@mantine/dates";
|
|
||||||
import { IconPencil, IconPlus, IconTrash } from "@tabler/icons-react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import { Params, useLoaderData, useParams, Link } from "react-router";
|
||||||
Params,
|
|
||||||
useLoaderData,
|
|
||||||
useParams,
|
|
||||||
useFetcher,
|
|
||||||
Link,
|
|
||||||
} from "react-router";
|
|
||||||
import { ConfirmDialogButton } from "./ConfirmDialogButton";
|
|
||||||
import { NavigateButton } from "./NavigateButton";
|
|
||||||
import { OrderSetProps, OrderSets } from "./OrderSets";
|
import { OrderSetProps, OrderSets } from "./OrderSets";
|
||||||
|
|
||||||
export const subOrderSetsLoader = async ({
|
export const subOrderSetsLoader = async ({
|
||||||
|
|
|
||||||
75
web/vite/src/TimelineList.tsx
Normal file
75
web/vite/src/TimelineList.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { Timeline, Text, Title, Box, Flex } from "@mantine/core";
|
||||||
|
import React from "react";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import {
|
||||||
|
IconAlertCircle,
|
||||||
|
IconArrowBadgeRightFilled,
|
||||||
|
IconCheck,
|
||||||
|
IconExternalLink,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
const TIMELINE_TYPE = {
|
||||||
|
ORDER_NOT_ISSUED: {
|
||||||
|
title: "Order not issued",
|
||||||
|
color: "gray.6",
|
||||||
|
bullet: <IconX />,
|
||||||
|
},
|
||||||
|
ORDER_ISSUED: {
|
||||||
|
title: "Order issued",
|
||||||
|
color: "yellow.6",
|
||||||
|
bullet: <IconArrowBadgeRightFilled />,
|
||||||
|
},
|
||||||
|
ORDER_CONFIRMED: {
|
||||||
|
title: "Order confirmed",
|
||||||
|
color: "green.6",
|
||||||
|
bullet: <IconCheck />,
|
||||||
|
},
|
||||||
|
ORDER_NOT_PUNISHED: {
|
||||||
|
title: "Order not punished",
|
||||||
|
color: "gray.6",
|
||||||
|
bullet: null,
|
||||||
|
},
|
||||||
|
ORDER_PUNISHED: {
|
||||||
|
title: "Order punished",
|
||||||
|
color: "red.6",
|
||||||
|
bullet: <IconAlertCircle />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TimelineList: React.FC<{
|
||||||
|
timeline: TimelineEvent[];
|
||||||
|
}> = ({ timeline }) => (
|
||||||
|
<Box>
|
||||||
|
<Title order={1} mb="lg">
|
||||||
|
Timeline
|
||||||
|
</Title>
|
||||||
|
<Timeline bulletSize={24} lineWidth={2}>
|
||||||
|
{timeline.map(
|
||||||
|
({ id, updated_at, type, text, username, orders_pool, extra }) => (
|
||||||
|
<Timeline.Item key={id} active {...(TIMELINE_TYPE[type] ?? {})}>
|
||||||
|
<Text size="sm">{text}</Text>
|
||||||
|
<Flex mt={4} gap="xs">
|
||||||
|
{extra?.mastodon_status_url ? (
|
||||||
|
<Text size="xs">
|
||||||
|
<a href={extra.mastodon_status_url} target="_blank">
|
||||||
|
Mastodon Post <IconExternalLink size="0.8rem" />
|
||||||
|
</a>
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{orders_pool ? (
|
||||||
|
<Text size="xs">
|
||||||
|
<Link to={`/orders/${username}/${orders_pool.id}`}>
|
||||||
|
{orders_pool.name}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Text size="xs">{moment(updated_at).fromNow()}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Timeline.Item>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Timeline>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
11
web/vite/src/index.d.ts
vendored
11
web/vite/src/index.d.ts
vendored
|
|
@ -24,3 +24,14 @@ type OrderSet = {
|
||||||
confirm_delay: string;
|
confirm_delay: string;
|
||||||
punishment_pool_id?: number;
|
punishment_pool_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TimelineEvent = {
|
||||||
|
id: number;
|
||||||
|
updated_at: string;
|
||||||
|
type: string;
|
||||||
|
text: string;
|
||||||
|
extra: Record<string, string>;
|
||||||
|
username: string;
|
||||||
|
orders_pool: Pick<OrderSet, "id" | "name">;
|
||||||
|
order_status: string;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue