rsdeps

Cargo.lock visualizer (mirror)
Log | Files | Refs | README | LICENSE

commit b35185b2cfa3ae30f20c24403ced0125d55e40b2
parent 32f699788705ef291ecfa1a79eac888a8e206e6a
Author: Andy Khramtsov <>
Date:   Wed, 27 May 2026 17:14:02 +0300

feat: add file upload

Diffstat:
Mjustfile | 2+-
Mpyproject.toml | 3---
Msrc/deps/__init__.py | 6------
Asrc/deps/app_state.py | 10++++++++++
Asrc/deps/assets/styles.css | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/deps/config.py | 2++
Asrc/deps/dash_app.py | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/deps/pages/__init__.py | 0
Asrc/deps/pages/home.py | 49+++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 431 insertions(+), 10 deletions(-)

diff --git a/justfile b/justfile @@ -24,7 +24,7 @@ jupyter: # Starts the app start: - uv run deps + uv run src/deps/dash_app.py --debug # Alias to start run: start diff --git a/pyproject.toml b/pyproject.toml @@ -14,9 +14,6 @@ dependencies = [ "polars>=1.40.1", ] -[project.scripts] -deps = "deps:main" - [build-system] requires = ["uv_build>=0.10.9,<0.11.0"] build-backend = "uv_build" diff --git a/src/deps/__init__.py b/src/deps/__init__.py @@ -1,6 +0,0 @@ -def hello() -> str: - return "Hello from deps!" - - -def main(): - print("Deps") diff --git a/src/deps/app_state.py b/src/deps/app_state.py @@ -0,0 +1,10 @@ +from deps.config import Config + + +class AppState: + """Immutable application state""" + + config: Config + + def __init__(self, config: Config) -> None: + self.config = config diff --git a/src/deps/assets/styles.css b/src/deps/assets/styles.css @@ -0,0 +1,298 @@ +/* This stylesheet uses BEM */ + +/* Reset layer */ + +*, *::before, *::after { + box-sizing: border-box; + margin: unset; + padding: unset; +} + +button, input, textarea, select { + font: inherit; +} + +img, picture, svg, canvas { + display: block; + max-inline-size: 100%; + height: auto; +} + +/* Theme layer */ + +:root { + --main-hue: 266; + --c-bg: hsl(var(--main-hue), 0%, 100%); + --c-bg-contrast: hsl(var(--main-hue), 0%, 95%); + --c-bg-contrast-hover: hsl(var(--main-hue), 0%, 97%); + --c-bg-contrast-pressed: hsl(var(--main-hue), 0%, 95%); + --c-font: hsl(var(--main-hue), 10%, 15%); + --c-border: hsl(var(--main-hue), 0%, 45%); + --c-hr: hsl(var(--main-hue), 10%, 40%); + --c-hr-faint: hsl(var(--main-hue), 10%, 80%); + --c-link: hsl(var(--main-hue), 40%, 30%); + --c-shadow: hsl(var(--main-hue), 0%, 20%, 35%); + --font-bold: 600; + --font-tiny: 0.75rem; + --font-small: 0.8rem; + --font-normal: 1rem; + --font-big: 1.5rem; + --row-gap-small: 0.3em; + --row-gap-normal: 0.65em; + --row-gap-big: 1.5em; + --font-family-sans: Arial, Helvetica, Verdana, Tahoma, sans-serif; + --font-family-monospace: "SF Mono", "Menlo", "Consolas", "Courier New", monospace; +} + +.dark, :root[data-theme='dark'] { + --c-bg: hsl(var(--main-hue), 0%, 20%); + --c-bg-contrast: hsl(var(--main-hue), 0%, 25%); + --c-bg-contrast-hover: hsl(var(--main-hue), 0%, 30%); + --c-bg-contrast-pressed: hsl(var(--main-hue), 0%, 25%); + --c-font: hsl(var(--main-hue), 10%, 85%); + --c-border: hsl(var(--main-hue), 0%, 55%); + --c-hr: hsl(var(--main-hue), 10%, 70%); + --c-hr-faint: hsl(var(--main-hue), 10%, 30%); + --c-link: hsl(var(--main-hue), 60%, 70%); + --c-shadow: hsl(var(--main-hue), 0%, 10%, 65%); +} + +/* Global layer */ + +body { + background-color: var(--c-bg); + font-family: var(--font-family-sans); + font-optical-sizing: auto; + font-style: normal; + font-size: 12pt; + font-weight: 400; + color: var(--c-font); + min-height: 100vh; + min-height: 100dvh; +} + +h1, h2, h3, h4, h5, h6 { + padding: 0.25em 0; +} + +p { + padding: 0.25em 0; +} + +/* BEM layer */ + +.padded-box { + padding: 0.5em; +} + +.border { + border: solid 1px var(--c-border); +} + +.shadow { + box-shadow: 0 0 0.5em var(--c-shadow); +} + +.shadow_small { + box-shadow: 0 0 0.25em var(--c-shadow); +} + +.shadow_large { + box-shadow: 0 0 1em var(--c-shadow); +} + +.vertical-content { + display: flex; + flex-direction: column; + gap: 0.5em; +} + +.vertical-content_small-gap { + gap: 0.25em; +} + +.vertical-content_medium-gap { + gap: 1em; +} + +.vertical-content_large-gap { + gap: 1.5em; +} + +.horizontal-content { + display: flex; + gap: 1em; + align-items: baseline; +} + +.horizontal-content_small-gap { + gap: 0.5em; +} + +.horizontal-content_large-gap { + gap: 2em; +} + +.horizontal-content_spread { + justify-content: space-between; +} + +.horizontal-content_center { + align-items: center; +} + +.link { + text-decoration: none; + color: var(--c-link); +} + +.list { + display: flex; + flex-direction: column; + gap: 0.25em; +} + +.list__item { + margin-left: 1.5em; +} + +.dcc-checklist__label { + color: var(--c-font); +} + +.dcc-input { + color: var(--c-font); + background-color: var(--c-bg); + border-color: var(--c-border); +} + +.dcc-tab { + color: var(--c-font); + background-color: var(--c-bg); + border-color: var(--c-border); +} + +.dcc-tab:hover { + background-color: var(--c-bg-contrast-hover); +} + +.dcc-tab:active { + background-color: var(--c-bg-contrast); +} + +.dcc-tab_selected { + color: var(--c-font); + background-color: var(--c-bg-contrast); + border-color: var(--c-border); +} + +.button { + color: var(--c-font); + background-color: var(--c-bg-contrast); + border-color: var(--c-border); +} + +.button:hover { + color: var(--c-font); + background-color: var(--c-bg-contrast-hover); +} + +.button:active { + color: var(--c-font); + background-color: var(--c-bg-contrast-pressed); +} + +.collapse-button { + display: flex; + gap: 0.5em; + align-items: center; + color: var(--c-font); + background-color: var(--c-bg-contrast); + border: unset; + border-radius: 0; +} + +.collapse-button:hover { + color: var(--c-font); + background-color: var(--c-bg-contrast-hover); +} + +.collapse-button:active { + color: var(--c-font); + background-color: var(--c-bg-contrast-pressed); +} + +.collapse-button__arrow-right { + border: 0.5em solid; + border-color: transparent transparent transparent var(--c-font); +} + +.collapse-button__arrow-down { + border: 0.5em solid; + margin-top: 0.5em; + border-color: var(--c-font) transparent transparent; +} + +.hr { + border: none; + border-top: solid 1px var(--c-hr); + width: 100%; +} + +.hr_faint { + border: none; + border-top: solid 1px var(--c-hr-faint); +} + +.help-icon { + background-color: unset; + text-decoration: unset; + cursor: help; + font-weight: bold; + border: solid 2px var(--c-border); + border-radius: 50%; + width: 1.25em; + height: 1.25em; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.help-icon:hover { + background-color: var(--c-bg-contrast-hover); +} + +.hidden { + visibility: hidden; + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + +/* Restylings (not BEM) */ + +.dash-dropdown, +.dash-dropdown-content, +.dash-dropdown-clear, +.dash-dropdown-value-count, +.dash-dropdown-value, +.dash-dropdown-trigger-icon, +.dash-dropdown-actions, +.dash-dropdown-action-button, +.dash-dropdown-options, +.dash-dropdown-option, +.dash-dropdown-search-container, +.dash-dropdown-search-icon, +.dash-dropdown-search +{ + color: var(--c-font); + background: var(--c-bg); + border-color: var(--c-border); +} diff --git a/src/deps/config.py b/src/deps/config.py @@ -0,0 +1,2 @@ +class Config: + pass diff --git a/src/deps/dash_app.py b/src/deps/dash_app.py @@ -0,0 +1,71 @@ +import argparse + +import dash +from dash import Dash, dcc, html + +from deps.app_state import AppState +from deps.config import Config + + +def new_app_state() -> AppState: + config = Config() + return AppState(config=config) + + +def main(): + state = new_app_state() + + app = Dash(use_pages=True) + app.server.config["APP_STATE"] = state + + app.layout = html.Div( + className="vertical-content vertical-content_large-gap", + children=[ + html.Div( + className=( + "padded-box horizontal-content horizontal-content_large-gap " + "horizontal-content_spread horizontal-content_center" + ), + children=[ + html.Div( + className="horizontal-content horizontal-content_large-gap", + children=[ + html.Div(children="Dash App"), + html.Div( + className="horizontal-content", + children=[ + html.Div( + dcc.Link( + f"{page['name']}", + className="link", + href=page["relative_path"], + ) + ) + for page in dash.page_registry.values() + ], + ), + ], + ), + ], + ), + dash.page_container, + ], + ) + + parser = argparse.ArgumentParser(description="Start Dash App") + parser.add_argument( + "--debug", + action="store_true", + help="Start Dash in debug mode", + ) + args = parser.parse_args() + + if args.debug: + app.run(debug=True) + else: + # todo: use gunicorn or wsgi + raise Exception("Production server is not yet implemented") + + +if __name__ == "__main__": + main() diff --git a/src/deps/pages/__init__.py b/src/deps/pages/__init__.py diff --git a/src/deps/pages/home.py b/src/deps/pages/home.py @@ -0,0 +1,49 @@ +import base64 + +import dash +from dash import Input, Output, State, callback, dcc, html +from dash.exceptions import PreventUpdate + +dash.register_page(__name__, path="/", order=1) + + +def layout(): + return html.Div( + className="padded-box vertical-content vertical-content_large-gap", + children=[ + html.H2("Cargo dependency visualisation"), + dcc.Textarea(id="text-content", placeholder="Cargo.lock contents", style={}), + dcc.Upload( + id="upload-file", + children=[ + dcc.Button("Upload Cargo.lock"), + html.Div("Uploaded file name:"), + html.Div(id="upload-file-filename"), + ], + ), + ], + ) + + +@callback( + dict( + textarea=Output("text-content", "value"), + filename=Output("upload-file-filename", "children"), + ), + dict( + upload_contents=Input("upload-file", "contents"), + ), + dict( + upload_filename=State("upload-file", "filename"), + ), +) +def upload_cargo_lock(inputs, state): + contents = inputs["upload_contents"] + if not contents: + raise PreventUpdate + content_type, content_string = contents.split(",") + decoded = base64.b64decode(content_string) + return dict( + textarea=decoded.decode("utf-8"), + filename=state["upload_filename"], + )