UX improvements
Signed-off-by: András Schmelczer <andras@schmelczer.dev>
This commit is contained in:
parent
5b92d22f75
commit
c8c2010bf2
8 changed files with 217 additions and 26 deletions
|
|
@ -1,2 +1,3 @@
|
|||
from .get_args import get_args
|
||||
from .snake_case_to_text import snake_case_to_text
|
||||
from .text_to_hex_color import text_to_hex_color
|
||||
|
|
|
|||
13
good_ai/src/good_ai/good_ai/helper/text_to_hex_color.py
Normal file
13
good_ai/src/good_ai/good_ai/helper/text_to_hex_color.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import colorsys
|
||||
from hashlib import md5
|
||||
|
||||
|
||||
def text_to_hex_color(text: str) -> str:
|
||||
ascii_bytes = text.encode("ascii")
|
||||
digest = md5(
|
||||
ascii_bytes
|
||||
).hexdigest() # the built-in hash function is salted differently in each process
|
||||
integer = int(digest, 16)
|
||||
hue = integer % 6311 / 6311.0
|
||||
rgb = colorsys.hsv_to_rgb(hue, 0.75, 0.6)
|
||||
return "#" + "".join("%02X" % round(i * 255) for i in rgb)
|
||||
BIN
good_ai/src/good_ai/good_ai/metrics/assets/github.png
Normal file
BIN
good_ai/src/good_ai/good_ai/metrics/assets/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -1,3 +1,112 @@
|
|||
body {
|
||||
background-color: hotpink;
|
||||
:root {
|
||||
--accent-color: #47c2d0;
|
||||
--error-color: #a30808;
|
||||
--background-color: #edf5f6;
|
||||
--small-padding: 10px;
|
||||
--medium-padding: 20px;
|
||||
--large-padding: 40px;
|
||||
--border-radius: 10px;
|
||||
--shadow: 0 4px 6px -1px rgb(0 0 0 / 10%), 0 2px 4px -1px rgb(0 0 0 / 6%);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: var(--medium-padding) 0 var(--small-padding) 0;
|
||||
}
|
||||
|
||||
.glance,
|
||||
.table-container,
|
||||
.parallel-coords {
|
||||
padding: var(--large-padding);
|
||||
margin: var(--large-padding);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glance {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.glance .description {
|
||||
width: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.glance .dash-graph {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table-container h2 {
|
||||
padding: var(--small-padding);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-container h2,
|
||||
footer.watermark {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.dash-spreadsheet {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.parallel-coords {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer.watermark {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--large-padding);
|
||||
background-color: #ddd;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
footer.watermark div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 3rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
cursor: pointer;
|
||||
transition: transform 300ms;
|
||||
}
|
||||
|
||||
a img:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
|
@ -10,13 +10,16 @@ from flask import Flask
|
|||
from good_ai.utilities.unique import unique
|
||||
|
||||
from ..context import get_context
|
||||
from ..helper import snake_case_to_text
|
||||
from ..helper import snake_case_to_text, text_to_hex_color
|
||||
from ..views import SortBy
|
||||
from .get_description import get_description
|
||||
from .get_filter_from_datatable import get_filter_from_datatable
|
||||
from .get_footer import get_footer
|
||||
|
||||
|
||||
def create_dash_app(function_name: str) -> Flask:
|
||||
accent_color = text_to_hex_color(function_name)
|
||||
|
||||
flask_app = Flask(__name__)
|
||||
app = Dash(
|
||||
function_name,
|
||||
|
|
@ -32,24 +35,37 @@ def create_dash_app(function_name: str) -> Flask:
|
|||
documents = get_context().persistence.get_documents()
|
||||
df = pd.DataFrame(documents)
|
||||
|
||||
execution_time_histogram = dcc.Graph(config={"displaylogo": False})
|
||||
table = dash_table.DataTable(
|
||||
columns=[{"name": i, "id": i} for i in df.columns],
|
||||
page_current=0,
|
||||
page_size=20,
|
||||
page_action="custom",
|
||||
filter_action="custom",
|
||||
filter_query="",
|
||||
sort_action="custom",
|
||||
sort_mode="multi",
|
||||
sort_by=[
|
||||
{"column_id": "created", "direction": "desc"},
|
||||
],
|
||||
)
|
||||
|
||||
app.layout = html.Div(
|
||||
[
|
||||
get_description(function_name),
|
||||
html.Div(
|
||||
table := dash_table.DataTable(
|
||||
columns=[{"name": i, "id": i} for i in df.columns],
|
||||
page_current=0,
|
||||
page_size=20,
|
||||
page_action="custom",
|
||||
filter_action="custom",
|
||||
filter_query="",
|
||||
sort_action="custom",
|
||||
sort_mode="multi",
|
||||
sort_by=[],
|
||||
),
|
||||
[
|
||||
get_description(
|
||||
function_name=function_name, accent_color=accent_color
|
||||
),
|
||||
execution_time_histogram,
|
||||
],
|
||||
className="glance",
|
||||
),
|
||||
execution_time_histogram := dcc.Graph(),
|
||||
parallel_coords := dcc.Graph(),
|
||||
html.Div([html.H2("Latest traces"), table], className="table-container"),
|
||||
parallel_coords := dcc.Graph(
|
||||
className="parallel-coords", config={"displaylogo": False}
|
||||
),
|
||||
get_footer(),
|
||||
interval := dcc.Interval(
|
||||
interval=4 * 1000, # in milliseconds
|
||||
n_intervals=0,
|
||||
|
|
@ -100,15 +116,23 @@ def create_dash_app(function_name: str) -> Flask:
|
|||
)
|
||||
df = pd.DataFrame(rows)
|
||||
|
||||
return px.histogram(
|
||||
fig = px.histogram(
|
||||
df,
|
||||
x="execution_time_ms",
|
||||
labels={"execution_time_ms": "Execution time (ms)"},
|
||||
nbins=20,
|
||||
title="Execution times",
|
||||
height=400,
|
||||
log_y=True,
|
||||
color_discrete_sequence=[accent_color],
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
autosize=True,
|
||||
margin=dict(l=0, r=0, b=0, t=0, pad=0),
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
@app.callback(
|
||||
Output(parallel_coords, "figure"),
|
||||
Input(table, "filter_query"),
|
||||
|
|
@ -131,7 +155,8 @@ def create_dash_app(function_name: str) -> Flask:
|
|||
get_dimension_descriptor(df, c)
|
||||
for c in df.columns
|
||||
if not c.startswith("arg:") and c not in {"id", "created"}
|
||||
]
|
||||
],
|
||||
line_color=accent_color,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
from dash.dcc import Markdown
|
||||
from dash import dcc, html
|
||||
|
||||
from ..helper import snake_case_to_text
|
||||
|
||||
|
||||
def get_description(function_name: str) -> Markdown:
|
||||
def get_description(function_name: str, accent_color: str) -> html.Div:
|
||||
markdown_text = f"""
|
||||
# {snake_case_to_text(function_name)} - metrics
|
||||
> A human-friendly framework for robust end-to-end AI deployments
|
||||
View the live data of your deployments here.
|
||||
|
||||
## Using the API
|
||||
|
||||
|
|
@ -14,7 +13,15 @@ def get_description(function_name: str) -> Markdown:
|
|||
|
||||
## Metrics
|
||||
|
||||
The recent traces and aggregated metrics are presented below.
|
||||
Recent traces and aggregated metrics are presented below. Try filtering the table.
|
||||
"""
|
||||
|
||||
return Markdown(markdown_text)
|
||||
return html.Div(
|
||||
[
|
||||
html.H1(
|
||||
f"{snake_case_to_text(function_name)} - metrics",
|
||||
style={"color": accent_color},
|
||||
),
|
||||
dcc.Markdown(markdown_text, className="description"),
|
||||
]
|
||||
)
|
||||
|
|
|
|||
22
good_ai/src/good_ai/good_ai/metrics/get_footer.py
Normal file
22
good_ai/src/good_ai/good_ai/metrics/get_footer.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from dash import html
|
||||
|
||||
|
||||
def get_footer() -> html.Footer:
|
||||
return html.Footer(
|
||||
[
|
||||
html.Div(
|
||||
[
|
||||
html.H6("GoodAI"),
|
||||
html.P(
|
||||
"A human-friendly framework for robust end-to-end AI deployments."
|
||||
),
|
||||
]
|
||||
),
|
||||
html.A(
|
||||
html.Img(src="/assets/github.png"),
|
||||
href="https://github.com/ScoutinScience/good-ai",
|
||||
target="_blank",
|
||||
),
|
||||
],
|
||||
className="watermark",
|
||||
)
|
||||
|
|
@ -11,3 +11,17 @@ class Query(BaseModel):
|
|||
sort: List[SortBy] = []
|
||||
skip: int = 0
|
||||
take: int = 100
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"filter": [
|
||||
{"property": "execution_time_ms", "operator": ">", "value": 100}
|
||||
],
|
||||
"sort": [
|
||||
{"column_id": "execution_time_ms", "direction": "asc"},
|
||||
{"column_id": "id", "direction": "desc"},
|
||||
],
|
||||
"take": 10,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue