Django app for building dashboards using raw SQL queries

Overview

django-sql-dashboard

PyPI Changelog License

Django app for building dashboards using raw SQL queries

Brings a useful subset of Datasette to Django.

Currently only works with PostgreSQL.

This is very early alpha. You should not yet trust this code, especially with regards to security. Do not run this in production (yet)!

Installation

Install this library using pip:

$ pip install django-sql-dashboard

Usage

Add "django_sql_dashboard" to your INSTALLED_APPS.

Add the following to your urls.py:

from django.urls import path
from django_sql_dashboard.views import dashboard, dashboard_index

urlpatterns = [
    path("dashboard/", dashboard_index, name="django_sql_dashboard-index"),
    path("dashboard/<slug>/", dashboard),
    # ...
]

Now visit /dashboard as a staff user to start trying out the dashboard.

Screenshot

Django_SQL_Dashboard screenshot

Development

To contribute to this library, first checkout the code. Then create a new virtual environment:

cd django-sql-dashboard
python -mvenv venv
source venv/bin/activate

Or if you are using pipenv:

pipenv shell

Now install the dependencies and tests:

pip install -e '.[test]'

To run the tests:

pytest
Comments
  • Run tests against multiple PostgreSQL versions

    Run tests against multiple PostgreSQL versions

    It would be great to have the GitHub Actions workflow run tests against multiple PostgreSQL versions to catch this kind of thing in the future https://github.com/simonw/django-sql-dashboard/blob/main/.github/workflows/test.yml

    Originally posted by @simonw in https://github.com/simonw/django-sql-dashboard/issues/138#issuecomment-873649061

    ci 
    opened by simonw 15
  • If a column is called

    If a column is called "group" (or other reserved words) the example link to the table doesn't work

    Clicking this link:

    SQL__select_count____from_availability_tag_____select_id__name__notes__disabled__previous_names__slug__group_from_availability_tag

    Links to this SQL which throws an error:

    select id, name, notes, disabled, previous_names, slug, group from availability_tag
    

    Error:

    syntax error at or near "group" LINE 1: ... id, name, notes, disabled, previous_names, slug, group from... ^

    bug 
    opened by simonw 11
  • Add a Docker Compose setup for development.

    Add a Docker Compose setup for development.

    This attempts to add support for contributing to the project using Docker Compose, as mentioned in https://github.com/simonw/django-sql-dashboard/issues/120#issuecomment-859152991. My hope is that this will make it easier for contributors (or at least, ones familiar with Docker Compose) to get up and running.

    Specifically, it does the following:

    • Runs test_project interactively on port 8000
    • Runs the Sphinx auto-reloading server on port 8001
    • Runs a Postgres database that test_project uses

    Instructions

    Note: These instructions are now out of date; see this PR's contributing.md for up-to-date instructions.

    Setting up the Docker Compose environment can be accomplished with:

    docker-compose build
    
    # Wait a few seconds after running this, to give Postgres time
    # to create the initial account and database.
    docker-compose up -d db
    
    docker-compose run app python manage.py migrate
    docker-compose run app python manage.py createsuperuser
    

    Once you've done that, you can run:

    docker-compose up
    

    This will start up both the test project and the Sphinx documentation server. If you only want to start one of them, you can use docker-compose up app to start up only the test project, or docker-compose up docs to start up only the documentation server.

    You will probably want to visit http://localhost:8000/admin/ to log in as your newly-created superuser, and then visit http://localhost:8000/dashboard/ to tinker with the dashboard UI.

    If you want to run the test suite, you can run:

    docker-compose run app pytest
    

    To do

    • [x] Figure out if this is actually something @simonw wants merged into the codebase (it's fine if not; since this PR solely adds files to the repo and doesn't change existing files, I can always use this stuff separately for my personal use)
    • [x] Add documentation about all this to contributing.md.
    • [x] See if we can automate the initial manage.py migrate.
    • [x] See if we can dynamically use the UID of the current user rather than hard-coding it to 1000--or at least, provide a way to specify a custom value for UID/GID.
    opened by toolness 11
  • Export ALL results for a query as CSV/TSV

    Export ALL results for a query as CSV/TSV

    For any given SQL query the ability to export the entire resultset as CSV would be incredibly useful.

    It could be expensive, so we would want to restrict it to only specific trusted users.

    enhancement 
    opened by simonw 11
  • Run tests against a read-only

    Run tests against a read-only "dashboard" database connection

    Related to #16. I want to encourage using a separate "dashboard" database alias which is configured something like this:

    DATABASES = {
        "default": {
            "ENGINE": "django.db.backends.postgresql_psycopg2",
            "NAME": "mydb",
        },
        "dashboard": {
            "ENGINE": "django.db.backends.postgresql_psycopg2",
            "NAME": "mydb",
            "OPTIONS": {"options": "-c default_transaction_read_only=on -c statement_timeout=100"},
        },
    }
    

    I want to write the tests against this - but I'm running into some trouble because the test framework isn't designed to handle read-only database connections like this. I'm seeing errors like this:

    Got an error creating the test database: cannot execute CREATE DATABASE in a read-only transaction

    bug tests 
    opened by simonw 9
  • Use ?sql=xxx:signature instead of signed values

    Use ?sql=xxx:signature instead of signed values

    URLs to SQL queries currently look like this:

    https://simonwillison.net/dashboard/?sql=InNlbGVjdCAlKG5hbWUpcyBhcyBuYW1lLCB0b19jaGFyKGRhdGVfdHJ1bmMoJ21vbnRoJywgY3JlYXRlZCksICdZWVlZLU1NJykgYXMgYmFyX2xhYmVsLFxyXG5jb3VudCgqKSBhcyBiYXJfcXVhbnRpdHkgZnJvbSBibG9nX2VudHJ5IGdyb3VwIGJ5IGJhcl9sYWJlbCBvcmRlciBieSBjb3VudCgqKSBkZXNjIg%3A1lLfRD%3AvFP_m0s3BxRS2qyiWtlMlE1KRa2qoKItofP1vvK7hdY&sql=InNlbGVjdCBib2R5IGFzIGh0bWwgZnJvbSBibG9nX2VudHJ5IGxpbWl0IDEi%3A1lLfRD%3AEK0KOXcGgYdgD4Yzglbodf806GnbmrdtPridp8m0hlY

    The problem here is that if the Django secret is reset these become broken links - there's no easy way to recover the SQL.

    Instead, if the signatures do not match, how about populating the forms but NOT executing the SQL queries, and showing a warning message at the top of the page?

    The ?sql= parameters could then become ?sql=SELECT ...::oKItofP1vvK7hdY where oKItofP1vvK7hdY is a signature but the rest of the query is in plain text.

    enhancement security small 
    opened by simonw 8
  • Error: Object of type datetime is not JSON serializable

    Error: Object of type datetime is not JSON serializable

    Hello!

    This project is great. I've run into an issue though. I was trying to run this (unfinished) query:

    select m.id, m.pub_name, m.pub_id, m.contents_restricted, 
    	array_agg(s.updated_at) as sub_dates
    from main_manuscript m
    left join main_submission s on m.id=s.manuscript_id
    group by m.id, m.pub_name, m.pub_id, m.contents_restricted
    order by m.id
    

    The problem is that django-sql-dashboard errors out because it doesn't know how to serialize the s.updated_at DateTime field for working with array_agg.

    As far as I could tell the only way to fix this would be to fork the codebase? Am I missing something? Thanks!

    bug 
    opened by matthew-a-dunlap 6
  • Handle SQL queries that are too large for a GET request

    Handle SQL queries that are too large for a GET request

    Sometimes it's useful to run GIANT queries - queries with a huge copy-pasted list of IDs in them for example.

    Right now the POST works but the GET redirect may cause an error.

    If this happens, you can instead create a saved dashboard and the query will execute fine. Some kind of utility mechanism for spotting this and automatically handling it might be nice.

    enhancement 
    opened by simonw 6
  • Turn [count] into a cog action menu item, add more

    Turn [count] into a cog action menu item, add more

    Borrow the cog action menu design from Datasette. Actions can include:

    • Count values (the existing "count" link)
    • Sum / Avg / etc (for columns that are numbers)
    • Show not-null values
    enhancement 
    opened by simonw 6
  • Interface for creating and editing dashboards

    Interface for creating and editing dashboards

    This is currently only possible in the Django Admin. This will also implement edit permissions, taking over from #27.

    Still todo:

    • [x] Form to save a dashboard as a saved dashboard
    • [x] If dashboard reloads with form errors, scroll down to the form
    • [x] "Edit dashboard" link that links to the Django admin
    • [x] Custom Django admin code to respect edit policies
    • [x] Saved dashboard pages should show their visibility and edit policies
    enhancement 
    opened by simonw 6
  • Support markdown and/or HTML in dashboard descriptions

    Support markdown and/or HTML in dashboard descriptions

    In particular, I'm linking out to external sites in my dashboard descriptions, so it would be great to have those hyperlinked (currently the viewer has to manually copy/paste the URL, which is shown as plain text). Since the package already has both markdown and bleach as dependencies, it doesn't seem like this should be technically complex, although perhaps making it backwards-compatible might be non-trivial...

    Anyways, happy to discuss this further and start a PR if it's deemed a good idea.

    enhancement 
    opened by toolness 5
  • Error during

    Error during "docker-compose up"

    Hi, thank you for this project!

    While starting to run the dashboard interactively using the command docker-compose up I got an error Step 3/11 : COPY setup.py README.md . When using COPY with more than one source file, the destination must be a directory and end with a / The problem was fixed by adding a slash after the final dot in the Dockerfile, just as the error message tells, like so: COPY setup.py README.md ./

    I'm running Ubuntu 22.04.1 LTS. Docker version 20.10.18, build b40c2f6 docker-compose version 1.29.2, build unknown

    opened by reflektoin 0
  • Ability to customize individual cell rendering within tables - like widgets but at the table cell level

    Ability to customize individual cell rendering within tables - like widgets but at the table cell level

    Not sure if this pushes things a bit too far, but I thought it would be nice to be able to have a table on one dashboard, where each row displays a link that would take the user to a different dashboard that displays details about that row (e.g. by passing the id as a parameter for the other dashboard). This could be done e.g. by allowing to render individual cells as markdown or html. Essentially s.th. like this screenshot Not sure how difficult that would be though.

    enhancement 
    opened by jimmybutton 6
  • Figure out a way to display a title for individual queries/charts

    Figure out a way to display a title for individual queries/charts

    Many thanks for this great package!

    When creating a dashboard with multiple queries and charts, I'd like to be able to give all or some of them (especially the charts) a title so others can understand what the graph is showing without having to look at the SQL query. So what I ended up doing is having additional queries in between charts such as select '## Users per day' as markdown. It kinda does the job, but it ends up looking a bit weird as the title is then quite far away from the chart itself.

    A possible solution could be having an optional title field on the DashboardQuery object. What do you think about that?

    enhancement 
    opened by jimmybutton 1
  • Error on initial install: 'relation

    Error on initial install: 'relation "django_sql_dashboard_dashboard" does not exist'

    I'm trying to explore the functionality of the project for exploring a Postgres database, however I'm encountering the following exception immediately after installation. Maybe I am missing a configuration step? Any help is appreciated.

    Internal Server Error: /dashboard/
    Traceback (most recent call last):
      File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 89, in _execute
        return self.cursor.execute(sql, params)
    psycopg2.errors.UndefinedTable: relation "django_sql_dashboard_dashboard" does not exist
    LINE 1: ...id", "auth_group"."name", T6."id", T6."name" FROM "django_sq...
                                                                 ^
    
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "/usr/local/lib/python3.8/dist-packages/django/core/handlers/exception.py", line 55, in inner
        response = get_response(request)
      File "/usr/local/lib/python3.8/dist-packages/django/core/handlers/base.py", line 197, in _get_response
        response = wrapped_callback(request, *callback_args, **callback_kwargs)
      File "/usr/local/lib/python3.8/dist-packages/django/contrib/auth/decorators.py", line 23, in _wrapped_view
        return view_func(request, *args, **kwargs)
      File "/usr/local/lib/python3.8/dist-packages/django_sql_dashboard/views.py", line 121, in dashboard_index
        return _dashboard_index(
      File "/usr/local/lib/python3.8/dist-packages/django_sql_dashboard/views.py", line 325, in _dashboard_index
        saved_dashboards = [
      File "/usr/local/lib/python3.8/dist-packages/django/db/models/query.py", line 320, in __iter__
        self._fetch_all()
      File "/usr/local/lib/python3.8/dist-packages/django/db/models/query.py", line 1507, in _fetch_all
        self._result_cache = list(self._iterable_class(self))
      File "/usr/local/lib/python3.8/dist-packages/django/db/models/query.py", line 57, in __iter__
        results = compiler.execute_sql(
      File "/usr/local/lib/python3.8/dist-packages/django/db/models/sql/compiler.py", line 1361, in execute_sql
        cursor.execute(sql, params)
      File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 103, in execute
        return super().execute(sql, params)
      File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 67, in execute
        return self._execute_with_wrappers(
      File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
        return executor(sql, params, many, context)
      File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 89, in _execute
        return self.cursor.execute(sql, params)
      File "/usr/local/lib/python3.8/dist-packages/django/db/utils.py", line 91, in __exit__
        raise dj_exc_value.with_traceback(traceback) from exc_value
      File "/usr/local/lib/python3.8/dist-packages/django/db/backends/utils.py", line 89, in _execute
        return self.cursor.execute(sql, params)
    django.db.utils.ProgrammingError: relation "django_sql_dashboard_dashboard" does not exist
    LINE 1: ...id", "auth_group"."name", T6."id", T6."name" FROM "django_sq...
                                                                 ^
    
    question 
    opened by loganwilliams 1
  • Support non-string parameters and default values

    Support non-string parameters and default values

    Passing non-string parameters and default values can be achieved today using something like:

    select *
    where grade >= cast(%(grade)s as integer)
    and is_allowed = cast(coalesce(nullif(%(is_allowed)s,''), 'true') as boolean)
    

    But this is not very readable. Would be nice to support some other patterns, for example:

    select *
    where grade >= %(grade)d
    and is_allowed = %(is_allowed:true)b
    

    I know that psycopg2 supports only string parameters, but it does not seem too difficult to manually handle simple cases (like numbers and booleans) while still caring about SQL injections.

    For now, I've proposed pull request #148 to refactor the named-parameter feature within the library, so that developpers can easily extend it according to their needs. That would be usefull in any case.

    I think it would also be usefull that simple cases (like those mentionned above) be part of the library. What do you think?

    opened by ipamo 1
Releases(1.1)
  • 1.1(Apr 20, 2022)

  • 1.0.2(Mar 8, 2022)

    • Fixed a bug where queries that returned an array containing dates (or other objects without a defined Python JSON serialization) would result in a 500 error. #146
    Source code(tar.gz)
    Source code(zip)
  • 1.0.1(Jul 6, 2021)

  • 1.0(Jul 1, 2021)

    • Implemented a new column cog menu, with options for sorting, counting distinct items and counting by values. #57
    • Fixed bug where columns named after PostgreSQL reserved words (such as on or group) produced invalid suggested SELECT queries. #134
    • New Docker Compose configuration to support Docker development environments. Thanks, Atul Varma. #128
    • Admin change list view now only shows dashboards the user has permission to edit. Thanks, Atul Varma. #130
    Source code(tar.gz)
    Source code(zip)
  • 0.16(Jun 6, 2021)

    • This release includes a small potentially backwards-incompatible change: the description field for a saved dashboard is now treated as Markdown and rendered as such when the saved dashboard is displayed. It is very unlikely that this will affect any of your existing dashboards but you should still check before applying the upgrade. Thanks, Atul Varma. #115
    Source code(tar.gz)
    Source code(zip)
  • 0.15.1(Jun 3, 2021)

  • 0.15(May 25, 2021)

  • 0.14(May 16, 2021)

    • Fixed a security and permissions flaw, where users without the execute_sql permission could still run custom queries by editing saved dashboards using the Django admin interface. #94
    • Bar charts now preserve the order in which the data is returned by the query. #106
    • Example select statements now include explicit columns. #105
    • Columns on the dashboard page now respond to media queries, collapsing to a single column on narrow or mobile browser windows. #106
    • Fixed hard-coded /dashboard/ URL, thanks Flávio Juvenal da Silva Junior. #99
    • Fixed bug where ?_save- parameters could be accidentally reflected in the query string. #104
    • Explicitly require at least Django 3.0. #101
    • Fixed a warning about AutoField migrations with Django 3.2. #103
    • Fixed a bug where users lacking permissions could end up in an infinite redirect. #30
    • Configuration and security documentation now recommends using a read-only database replica as the most robust option. #95
    • Added screenshots and demo links for all of the included widgets. #96
    Source code(tar.gz)
    Source code(zip)
  • 0.13(May 10, 2021)

    • New word cloud widget displayed when queries return wordcount_word and wordcount_count columns. #91
    • All pages are now served with cache-control: private header if the user is logged in. #92
    • Much improved README, including a detailed list of features. #40
    Source code(tar.gz)
    Source code(zip)
  • 0.12.3(May 9, 2021)

    • Fixed bug where saved dashboards relating to groups could be displayed multiple times. #90
    • Removed duplicate "Run queries" button. #89
    • HTML page titles now include named parameter values, if available. #88
    Source code(tar.gz)
    Source code(zip)
  • 0.12.2(May 9, 2021)

    • Documentation is now hosted at https://django-sql-dashboard.datasette.io/ #86
    • Dashboard index link is no longer shown on saved dashboards to to users without permission to view it. #87
    Source code(tar.gz)
    Source code(zip)
  • 0.12.1(May 9, 2021)

  • 0.12(May 9, 2021)

    First non-alpha release! Django SQL Dashboard is now ready for people to use against their production Django applications.

    • Saved dashboards can now be created from the interactive dashboard page. #44
    • New progress bar widget for queries that return numeric columns total_count and completed_count. #77
    • The list of available tables now better reflects your current permissions, and shows the columns for each listed table. #79, #80
    • The dashboard index page now lists saved dashboards that the user is able to view or edit. #81
    • "Edit" link for dashboards links to the Django Admin, which now respects the edit policy set for a dashboard. #44
    • New documentation section covering security. #6
    • Show row count on non-truncated results. #76
    • More robust extraction of named parameteters from queries, fixing some errer-cases. #75
    • Custom widgets can now extend a django_sql_dashboard/widgets/_base_widget.html base template. #78
    • Fixed a bug caused by errors on saved dashboard pages being displayed as editable text. #74
    • Unlisted public dashboards now include a robots tag to avoid being indexed by search engines. #42
    Source code(tar.gz)
    Source code(zip)
  • 0.11a0(Apr 26, 2021)

  • 0.9a1(Apr 25, 2021)

  • 0.9a0(Apr 25, 2021)

  • 0.10a1(Apr 25, 2021)

  • 0.10a0(Apr 25, 2021)

  • 0.8a2(Apr 14, 2021)

  • 0.8a1(Apr 14, 2021)

  • 0.8a0(Apr 14, 2021)

    • Make it easy to provide a custom base template. #7
    • Content-Security-Policy: frame-ancestors header. #64
    • Signing no longer uses base64/json. #45
    • DASHBOARD_UPGRADE_OLD_BASE64_LINKS mechanism. #65
    Source code(tar.gz)
    Source code(zip)
  • 0.7a0(Apr 12, 2021)

  • 0.6a0(Apr 9, 2021)

  • 0.5a0(Mar 24, 2021)

  • 0.4a2(Mar 21, 2021)

  • 0.4a1(Mar 21, 2021)

  • 0.4a0(Mar 19, 2021)

    • Documentation now lives at https://django-sql-dashboard.readthedocs.io/ (#36)
    • Ability to copy and paste TSV from the default table display. (#29)
    • Fixed two bugs with the way count links in column headers work. (#31, #32)
    • New permissions system: a saved dashboard can now be made public, private, unlisted, group-only, staff-only or superuser-only. (#27)
    Source code(tar.gz)
    Source code(zip)
  • 0.3a1(Mar 16, 2021)

    • Changed default permission policy: saved dashboards are now inaccessible to the public by default (#37). This will change when permissions are implemented fully in #27.
    Source code(tar.gz)
    Source code(zip)
  • 0.3a0(Mar 15, 2021)

  • 0.2a2(Mar 15, 2021)

Owner
Simon Willison
Simon Willison
Organize Django settings into multiple files and directories. Easily override and modify settings. Use wildcards and optional settings files.

Organize Django settings into multiple files and directories. Easily override and modify settings. Use wildcards in settings file paths and mark setti

Nikita Sobolev 940 Jan 03, 2023
scaffold django rest apis like a champion 🚀

dr_scaffold Scaffold django rest apis like a champion ⚡ . said no one before Overview This library will help you to scaffold full Restful API Resource

Abdenasser Elidrissi 133 Jan 05, 2023
The uncompromising Python code formatter

The Uncompromising Code Formatter “Any color you like.” Black is the uncompromising Python code formatter. By using it, you agree to cede control over

Python Software Foundation 30.7k Jan 03, 2023
Modular search for Django

Haystack Author: Daniel Lindsley Date: 2013/07/28 Haystack provides modular search for Django. It features a unified, familiar API that allows you to

Haystack Search 3.4k Jan 08, 2023
Wrapping Raml around Django rest-api's

Ramlwrap is a toolkit for Django which allows a combination of rapid server prototyping as well as enforcement of API definition from the RAML api. R

Jmons 8 Dec 27, 2021
CRUD with MySQL, Django and Sass.

CRUD with MySQL, Django and Sass. To have the same data in db: insert into crud_employee (first_name, last_name, email, phone, location, university) v

Luis Quiñones Requelme 1 Nov 19, 2021
The magical reactive component framework for Django ✨

Unicorn The magical full-stack framework for Django ✨ Unicorn is a reactive component framework that progressively enhances a normal Django view, make

Adam Hill 1.4k Jan 05, 2023
Thumbnails for Django

Thumbnails for Django. Features at a glance Support for Django 2.2, 3.0 and 3.1 following the Django supported versions policy Python 3 support Storag

Jazzband 1.6k Jan 03, 2023
Loguru is an exceeding easy way to do logging in Python

Django Easy Logging Easy Django logging with Loguru Loguru is an exceeding easy way to do logging in Python. django-easy-logging makes it exceedingly

Neutron Sync 8 Oct 17, 2022
Django Starter is a simple Skeleton to start with a Django project.

Django Starter Template Description Django Starter is a simple Skeleton to start

Numan Ibn Mazid 1 Jan 10, 2022
Django Serverless Cron - Run cron jobs easily in a serverless environment

Django Serverless Cron - Run cron jobs easily in a serverless environment

Paul Onteri 41 Dec 16, 2022
The new Python SDK for Sentry.io

Bad software is everywhere, and we're tired of it. Sentry is on a mission to help developers write better software faster, so we can get back to enjoy

Sentry 1.4k Jan 05, 2023
Django Simple Spam Blocker is blocking spam by regular expression.

Django Simple Spam Blocker is blocking spam by regular expression.

Masahiko Okada 23 Nov 29, 2022
xsendfile etc wrapper

Django Sendfile This is a wrapper around web-server specific methods for sending files to web clients. This is useful when Django needs to check permi

John Montgomery 476 Dec 01, 2022
Set the draft security HTTP header Permissions-Policy (previously Feature-Policy) on your Django app.

django-permissions-policy Set the draft security HTTP header Permissions-Policy (previously Feature-Policy) on your Django app. Requirements Python 3.

Adam Johnson 78 Jan 02, 2023
Send logs to RabbitMQ from Python/Django.

python-logging-rabbitmq Logging handler to ships logs to RabbitMQ. Compatible with Django. Installation Install using pip. pip install python_logging_

Alberto Menendez Romero 38 Nov 17, 2022
Money fields for Django forms and models.

django-money A little Django app that uses py-moneyed to add support for Money fields in your models and forms. Django versions supported: 1.11, 2.1,

1.4k Jan 06, 2023
Django-fast-export - Utilities for quickly streaming CSV responses to the client

django-fast-export Utilities for quickly streaming CSV responses to the client T

Matthias Kestenholz 4 Aug 24, 2022
Template de desarrollo Django

Template de desarrollo Django Python Django Docker Postgres Nginx CI/CD Descripción del proyecto : Proyecto template de directrices para la estandariz

Diego Esteban 1 Feb 25, 2022
Automated image processing for Django. Currently v4.0

ImageKit is a Django app for processing images. Need a thumbnail? A black-and-white version of a user-uploaded image? ImageKit will make them for you.

Matthew Dapena-Tretter 2.1k Dec 17, 2022