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 json
|
||||
from peewee import JOIN, fn
|
||||
|
||||
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,
|
||||
orders_pool=orders_pool,
|
||||
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}')
|
||||
timeline_event_put(
|
||||
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,
|
||||
orders_pool=orders_pool,
|
||||
order_status=order_status,
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ async def order_check(order_status_id):
|
|||
logger.info('Confirmed order %s' % (order_status.id))
|
||||
timeline_event_put(
|
||||
TIMELINE_ORDER_CONFIRMED,
|
||||
f'Order confirmed - {order_status.text.split("\n")[0]}',
|
||||
order_status.text.split("\n")[0],
|
||||
order_status.user,
|
||||
order_status.pool,
|
||||
order_status,
|
||||
|
|
@ -222,7 +222,7 @@ async def order_check(order_status_id):
|
|||
punishment_status = issue_result['order_status']
|
||||
timeline_event_put(
|
||||
TIMELINE_ORDER_PUNISHED,
|
||||
f'Order punished - {punishment_status.text.split("\n")[0]}',
|
||||
punishment_status.text.split("\n")[0],
|
||||
punishment_status.user,
|
||||
punishment_status.pool,
|
||||
punishment_status,
|
||||
|
|
@ -233,7 +233,7 @@ async def order_check(order_status_id):
|
|||
elif 'reason' in issue_result:
|
||||
timeline_event_put(
|
||||
TIMELINE_ORDER_NOT_PUNISHED,
|
||||
f'Order not punished - {issue_result['reason']}',
|
||||
issue_result['reason'],
|
||||
order_status.user,
|
||||
order_status.pool,
|
||||
order_status
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class OrderScheduler():
|
|||
)
|
||||
timeline_event_put(
|
||||
TIMELINE_ORDER_ISSUED,
|
||||
f"Order issued - {order_status.text.split("\n")[0]}",
|
||||
order_status.text.split("\n")[0],
|
||||
user=orders_pool.user,
|
||||
orders_pool=orders_pool,
|
||||
order_status=order_status,
|
||||
|
|
|
|||
3
util.py
3
util.py
|
|
@ -13,6 +13,9 @@ def timezone():
|
|||
def sqlite_time(dt):
|
||||
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):
|
||||
order_time_arr = list(map(int, str.split(':')))
|
||||
return datetime.time(
|
||||
|
|
|
|||
32
web/api.py
32
web/api.py
|
|
@ -1,11 +1,13 @@
|
|||
import re
|
||||
import requests
|
||||
import json
|
||||
from functools import wraps
|
||||
from flask import Blueprint, jsonify, abort, request
|
||||
from flask_login import login_required, current_user
|
||||
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 util import time_sqlite
|
||||
|
||||
RE_MASTODON_ATTN_LIST = re.compile(r'^(@(\w+)(@([\w\.]+))? )*(@\w+)(@[\w\.]+)?$')
|
||||
|
||||
|
|
@ -44,6 +46,31 @@ def profile():
|
|||
|
||||
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')
|
||||
@login_required
|
||||
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'])
|
||||
except:
|
||||
except Exception as e:
|
||||
print(e)
|
||||
transaction.rollback()
|
||||
abort(500)
|
||||
elif request.method == 'DELETE':
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"@mantine/form": "^8.3.13",
|
||||
"@mantine/notifications": "^8.3.13",
|
||||
"@tabler/icons-react": "^3.36.1",
|
||||
"moment": "^2.30.1",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
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 { IconPencil } from "@tabler/icons-react";
|
||||
import { NavigateButton } from "./NavigateButton";
|
||||
import { OrderSetProps, OrderSets } from "./OrderSets";
|
||||
import { useUserContext } from "./UserContext";
|
||||
import { TimelineList } from "./TimelineList";
|
||||
|
||||
export const subsListLoader = () =>
|
||||
Promise.all([
|
||||
fetch("/api/orders/").then((response) => response.json()),
|
||||
fetch("/api/subs").then((response) => response.json()),
|
||||
fetch("/api/timeline").then((response) => response.json()),
|
||||
]);
|
||||
|
||||
interface SubsListProps {
|
||||
|
|
@ -58,14 +60,17 @@ const SubsList: React.FC<SubsListProps> = ({ subs }) => (
|
|||
);
|
||||
|
||||
export const Dashboard: React.FC = () => {
|
||||
const [orderSets, subs] =
|
||||
useLoaderData<[OrderSetProps["orderSets"], SubsListProps["subs"]]>();
|
||||
const [orderSets, subs, timeline] =
|
||||
useLoaderData<
|
||||
[OrderSetProps["orderSets"], SubsListProps["subs"], TimelineEvent[]]
|
||||
>();
|
||||
const { username } = useUserContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<OrderSets orderSets={orderSets} username={username} />
|
||||
{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 {
|
||||
Params,
|
||||
useLoaderData,
|
||||
useParams,
|
||||
useFetcher,
|
||||
Link,
|
||||
} from "react-router";
|
||||
import { ConfirmDialogButton } from "./ConfirmDialogButton";
|
||||
import { NavigateButton } from "./NavigateButton";
|
||||
import { Params, useLoaderData, useParams, Link } from "react-router";
|
||||
import { OrderSetProps, OrderSets } from "./OrderSets";
|
||||
|
||||
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;
|
||||
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