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 .get_args import get_args
|
||||||
from .snake_case_to_text import snake_case_to_text
|
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 {
|
:root {
|
||||||
background-color: hotpink;
|
--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 good_ai.utilities.unique import unique
|
||||||
|
|
||||||
from ..context import get_context
|
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 ..views import SortBy
|
||||||
from .get_description import get_description
|
from .get_description import get_description
|
||||||
from .get_filter_from_datatable import get_filter_from_datatable
|
from .get_filter_from_datatable import get_filter_from_datatable
|
||||||
|
from .get_footer import get_footer
|
||||||
|
|
||||||
|
|
||||||
def create_dash_app(function_name: str) -> Flask:
|
def create_dash_app(function_name: str) -> Flask:
|
||||||
|
accent_color = text_to_hex_color(function_name)
|
||||||
|
|
||||||
flask_app = Flask(__name__)
|
flask_app = Flask(__name__)
|
||||||
app = Dash(
|
app = Dash(
|
||||||
function_name,
|
function_name,
|
||||||
|
|
@ -32,11 +35,8 @@ def create_dash_app(function_name: str) -> Flask:
|
||||||
documents = get_context().persistence.get_documents()
|
documents = get_context().persistence.get_documents()
|
||||||
df = pd.DataFrame(documents)
|
df = pd.DataFrame(documents)
|
||||||
|
|
||||||
app.layout = html.Div(
|
execution_time_histogram = dcc.Graph(config={"displaylogo": False})
|
||||||
[
|
table = dash_table.DataTable(
|
||||||
get_description(function_name),
|
|
||||||
html.Div(
|
|
||||||
table := dash_table.DataTable(
|
|
||||||
columns=[{"name": i, "id": i} for i in df.columns],
|
columns=[{"name": i, "id": i} for i in df.columns],
|
||||||
page_current=0,
|
page_current=0,
|
||||||
page_size=20,
|
page_size=20,
|
||||||
|
|
@ -45,11 +45,27 @@ def create_dash_app(function_name: str) -> Flask:
|
||||||
filter_query="",
|
filter_query="",
|
||||||
sort_action="custom",
|
sort_action="custom",
|
||||||
sort_mode="multi",
|
sort_mode="multi",
|
||||||
sort_by=[],
|
sort_by=[
|
||||||
|
{"column_id": "created", "direction": "desc"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.layout = html.Div(
|
||||||
|
[
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
get_description(
|
||||||
|
function_name=function_name, accent_color=accent_color
|
||||||
),
|
),
|
||||||
|
execution_time_histogram,
|
||||||
|
],
|
||||||
|
className="glance",
|
||||||
),
|
),
|
||||||
execution_time_histogram := dcc.Graph(),
|
html.Div([html.H2("Latest traces"), table], className="table-container"),
|
||||||
parallel_coords := dcc.Graph(),
|
parallel_coords := dcc.Graph(
|
||||||
|
className="parallel-coords", config={"displaylogo": False}
|
||||||
|
),
|
||||||
|
get_footer(),
|
||||||
interval := dcc.Interval(
|
interval := dcc.Interval(
|
||||||
interval=4 * 1000, # in milliseconds
|
interval=4 * 1000, # in milliseconds
|
||||||
n_intervals=0,
|
n_intervals=0,
|
||||||
|
|
@ -100,15 +116,23 @@ def create_dash_app(function_name: str) -> Flask:
|
||||||
)
|
)
|
||||||
df = pd.DataFrame(rows)
|
df = pd.DataFrame(rows)
|
||||||
|
|
||||||
return px.histogram(
|
fig = px.histogram(
|
||||||
df,
|
df,
|
||||||
x="execution_time_ms",
|
x="execution_time_ms",
|
||||||
labels={"execution_time_ms": "Execution time (ms)"},
|
labels={"execution_time_ms": "Execution time (ms)"},
|
||||||
nbins=20,
|
nbins=20,
|
||||||
title="Execution times",
|
height=400,
|
||||||
log_y=True,
|
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(
|
@app.callback(
|
||||||
Output(parallel_coords, "figure"),
|
Output(parallel_coords, "figure"),
|
||||||
Input(table, "filter_query"),
|
Input(table, "filter_query"),
|
||||||
|
|
@ -131,7 +155,8 @@ def create_dash_app(function_name: str) -> Flask:
|
||||||
get_dimension_descriptor(df, c)
|
get_dimension_descriptor(df, c)
|
||||||
for c in df.columns
|
for c in df.columns
|
||||||
if not c.startswith("arg:") and c not in {"id", "created"}
|
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
|
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"""
|
markdown_text = f"""
|
||||||
# {snake_case_to_text(function_name)} - metrics
|
View the live data of your deployments here.
|
||||||
> A human-friendly framework for robust end-to-end AI deployments
|
|
||||||
|
|
||||||
## Using the API
|
## Using the API
|
||||||
|
|
||||||
|
|
@ -14,7 +13,15 @@ def get_description(function_name: str) -> Markdown:
|
||||||
|
|
||||||
## Metrics
|
## 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] = []
|
sort: List[SortBy] = []
|
||||||
skip: int = 0
|
skip: int = 0
|
||||||
take: int = 100
|
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