From e5d2f192df556fdc8d75a1ec66f7aed4ba901c43 Mon Sep 17 00:00:00 2001 From: peter Date: Wed, 2 Sep 2026 14:56:20 +0100 Subject: [PATCH] Add json and csv artifact kinds with interactive viewers Gap 1 from planning/2026-09-02-artifact_kind_gaps_for_frequent_agent_output.md. Chose new first-class kinds over riding on html_fragment, resolving that doc's open design question in favor of filtering support, raw-source preservation (like markdown's .md sidecar), and server-owned validation. - json: validated with json.loads at post time (400 on invalid JSON), rendered with renderjson (collapsible tree), vendored locally. - csv: parsed server-side with the stdlib csv module into columns/rows and handed to Grid.js as plain data - no client-side CSV-parsing plugin needed, keeping normalization server-owned. Vendored locally (MIT). - Both preserve their original text as a sidecar file, generalizing what was previously markdown-only handling (storage.py's SIDECAR_SUFFIXES) so disk-size accounting, eviction cleanup, and ?raw=1 all cover json/csv too. - Raw JSON is embedded in a -injection payload against the actual running server, not just unit tests. New migration (0012) for the KIND_CHOICES change - SQLite doesn't enforce choices at the schema level, but Django's migration state tracks it. Co-Authored-By: Claude Sonnet 5 --- .../migrations/0012_alter_artifact_kind.py | 18 ++ artifacts/models.py | 4 + artifacts/storage.py | 29 ++- artifacts/views.py | 124 +++++++++- docs/api.md | 14 +- static/vendor/gridjs/LICENSE | 21 ++ static/vendor/gridjs/NOTICE.md | 3 + static/vendor/gridjs/gridjs.umd.js | 2 + static/vendor/gridjs/mermaid.min.css | 1 + static/vendor/renderjson/NOTICE.md | 4 + static/vendor/renderjson/renderjson.js | 216 ++++++++++++++++++ tests/api/test_artifacts.py | 58 +++++ 12 files changed, 476 insertions(+), 18 deletions(-) create mode 100644 artifacts/migrations/0012_alter_artifact_kind.py create mode 100644 static/vendor/gridjs/LICENSE create mode 100644 static/vendor/gridjs/NOTICE.md create mode 100644 static/vendor/gridjs/gridjs.umd.js create mode 100644 static/vendor/gridjs/mermaid.min.css create mode 100644 static/vendor/renderjson/NOTICE.md create mode 100644 static/vendor/renderjson/renderjson.js diff --git a/artifacts/migrations/0012_alter_artifact_kind.py b/artifacts/migrations/0012_alter_artifact_kind.py new file mode 100644 index 0000000..66bfa6d --- /dev/null +++ b/artifacts/migrations/0012_alter_artifact_kind.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.15 on 2026-09-02 13:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('artifacts', '0011_add_asset'), + ] + + operations = [ + migrations.AlterField( + model_name='artifact', + name='kind', + field=models.CharField(choices=[('html_fragment', 'HTML fragment'), ('html_full', 'Full HTML'), ('markdown', 'Markdown'), ('deck', 'Deck'), ('image', 'Image'), ('pdf', 'PDF'), ('binary', 'Binary'), ('json', 'JSON'), ('csv', 'CSV')], max_length=20), + ), + ] diff --git a/artifacts/models.py b/artifacts/models.py index 3ce7209..d0752fb 100644 --- a/artifacts/models.py +++ b/artifacts/models.py @@ -10,6 +10,8 @@ class Artifact(models.Model): IMAGE = "image" PDF = "pdf" BINARY = "binary" + JSON = "json" + CSV = "csv" AGENT = "agent" PERSON = "person" @@ -23,6 +25,8 @@ class Artifact(models.Model): (IMAGE, "Image"), (PDF, "PDF"), (BINARY, "Binary"), + (JSON, "JSON"), + (CSV, "CSV"), ] SOURCE_TYPE_CHOICES = [ diff --git a/artifacts/storage.py b/artifacts/storage.py index fda50b8..fbcf001 100644 --- a/artifacts/storage.py +++ b/artifacts/storage.py @@ -1,8 +1,9 @@ """Disk-usage accounting, per-user quota, and automatic eviction. -An artifact's on-disk footprint is the primary stored file plus, for markdown, -the preserved `.md` sidecar. We record it on the row (`size_bytes`) at ingest so -usage is a cheap SUM rather than a stat of every file. +An artifact's on-disk footprint is the primary stored file plus, for kinds +that preserve their original source (see `SIDECAR_SUFFIXES`), the sidecar +file. We record it on the row (`size_bytes`) at ingest so usage is a cheap +SUM rather than a stat of every file. """ import shutil @@ -19,22 +20,31 @@ class QuotaExceeded(Exception): """Raised when a post cannot fit even after evicting every unkept artifact.""" +# Kinds that preserve their original source alongside the rendered .html file. +SIDECAR_SUFFIXES = { + Artifact.MARKDOWN: ".md", + Artifact.JSON: ".json", + Artifact.CSV: ".csv", +} + + def artifact_disk_size(relative_path, kind): - """Bytes an artifact occupies on disk, including the markdown sidecar.""" + """Bytes an artifact occupies on disk, including any sidecar file.""" root = settings.ARTIFACTS_ROOT total = 0 primary = root / relative_path if primary.exists(): total += primary.stat().st_size - if kind == Artifact.MARKDOWN: - sidecar = primary.with_suffix(".md") + sidecar_suffix = SIDECAR_SUFFIXES.get(kind) + if sidecar_suffix: + sidecar = primary.with_suffix(sidecar_suffix) if sidecar.exists(): total += sidecar.stat().st_size return total def delete_artifact_files(artifact): - """Remove an artifact's files from disk (primary, markdown sidecar, assets). + """Remove an artifact's files from disk (primary, sidecar, assets). Assets are cascade-deleted from the DB by the FK, but their files must be removed here explicitly. @@ -42,8 +52,9 @@ def delete_artifact_files(artifact): root = settings.ARTIFACTS_ROOT primary = root / artifact.path primary.unlink(missing_ok=True) - if artifact.kind == Artifact.MARKDOWN: - primary.with_suffix(".md").unlink(missing_ok=True) + sidecar_suffix = SIDECAR_SUFFIXES.get(artifact.kind) + if sidecar_suffix: + primary.with_suffix(sidecar_suffix).unlink(missing_ok=True) for asset in artifact.assets.all(): delete_asset_file(asset) diff --git a/artifacts/views.py b/artifacts/views.py index faab737..afdbb56 100644 --- a/artifacts/views.py +++ b/artifacts/views.py @@ -1,5 +1,7 @@ +import csv import hashlib import html +import io import json import mimetypes import re @@ -34,6 +36,7 @@ from .forms import DaybookLoginForm from .models import Artifact, Asset, TagColor from .storage import ( QuotaExceeded, + SIDECAR_SUFFIXES, artifact_disk_size, delete_artifact_files, disk_free_bytes, @@ -45,7 +48,14 @@ from .storage import ( ) -TEXT_KINDS = {Artifact.HTML_FRAGMENT, Artifact.HTML_FULL, Artifact.MARKDOWN, Artifact.DECK} +TEXT_KINDS = { + Artifact.HTML_FRAGMENT, + Artifact.HTML_FULL, + Artifact.MARKDOWN, + Artifact.DECK, + Artifact.JSON, + Artifact.CSV, +} # Affects storage layout, renderer choice, or feed ordering - never editable after create. IMMUTABLE_PATCH_FIELDS = {"content", "source", "kind", "day"} @@ -104,7 +114,10 @@ class ArtifactListCreateView(APIView): data = request.data kind = data.get("kind") if kind not in TEXT_KINDS: - return Response({"detail": "kind must be html_fragment, html_full, markdown, or deck"}, status=400) + return Response( + {"detail": "kind must be html_fragment, html_full, markdown, deck, json, or csv"}, + status=400, + ) required = ["source", "title", "content"] missing = [field for field in required if not data.get(field)] @@ -285,7 +298,7 @@ class CapabilitiesView(APIView): "artifact_kinds": [choice[0] for choice in Artifact.KIND_CHOICES], "filters": ["source", "label", "day", "kind", "q"], "views": ["rendered", "raw"], - "raw_supported_for": [Artifact.MARKDOWN, Artifact.HTML_FULL], + "raw_supported_for": [Artifact.MARKDOWN, Artifact.HTML_FULL, Artifact.JSON, Artifact.CSV], "source_types": [choice[0] for choice in Artifact.SOURCE_TYPE_CHOICES], "auth": "bearer_token", "methods": { @@ -512,12 +525,19 @@ def create_text_artifact(data, request, user): theme, request, ) - (target_dir / f"{artifact_id}.md").write_text(content, encoding="utf-8") elif kind == Artifact.DECK: rendered = wrap_deck(content, title, request) + elif kind == Artifact.JSON: + rendered = wrap_json_viewer(content, title, request) + elif kind == Artifact.CSV: + rendered = wrap_csv_viewer(content, title, request) else: rendered = wrap_html(content, title, theme, request) + sidecar_suffix = SIDECAR_SUFFIXES.get(kind) + if sidecar_suffix: + (target_dir / f"{artifact_id}{sidecar_suffix}").write_text(content, encoding="utf-8") + stored_path = directory / f"{artifact_id}.html" (settings.ARTIFACTS_ROOT / stored_path).write_text(rendered, encoding="utf-8") @@ -638,6 +658,92 @@ def wrap_deck(body, title, request): """ +SCRIPT_CLOSE_RE = re.compile(r" tag it's placed inside. + + Safe specifically for JSON text: JSON's grammar treats `\\/` as an + escaped '/', so this round-trips correctly through JSON.parse - it is + not a general-purpose HTML/JS escaping technique. + """ + return SCRIPT_CLOSE_RE.sub("<\\/script", text) + + +def wrap_json_viewer(content, title, request): + renderjson_src = request.build_absolute_uri("/static/vendor/renderjson/renderjson.js") + safe_json = escape_script_body(content) + return f""" + + + + + {html.escape(title)} + + + + + +
+ + + +""" + + +def wrap_csv_viewer(content, title, request): + """Parses CSV server-side into columns/rows and hands Grid.js plain data - + no client-side CSV parsing plugin needed, keeping normalization server-owned. + """ + gridjs_js = request.build_absolute_uri("/static/vendor/gridjs/gridjs.umd.js") + gridjs_css = request.build_absolute_uri("/static/vendor/gridjs/mermaid.min.css") + rows = [row for row in csv.reader(io.StringIO(content)) if row] + columns = rows[0] if rows else [] + data = rows[1:] if len(rows) > 1 else [] + table_json = escape_script_body(json.dumps({"columns": columns, "data": data})) + return f""" + + + + + {html.escape(title)} + + + + + + +
+ + + +""" + + def generate_id(): while True: artifact_id = secrets.token_hex(4) @@ -757,6 +863,11 @@ def validate_content_shape(kind, content): ) if kind == Artifact.MARKDOWN and not (content or "").strip(): return Response({"detail": "markdown content cannot be blank"}, status=400) + if kind == Artifact.JSON: + try: + json.loads(content) + except (TypeError, ValueError) as exc: + return Response({"detail": f"content must be valid JSON: {exc}"}, status=400) return None @@ -875,10 +986,11 @@ def safe_artifact_path(relative_path): def raw_artifact_path(artifact): path = safe_artifact_path(artifact.path) - if artifact.kind == Artifact.MARKDOWN: - return path.with_suffix(".md") if artifact.kind == Artifact.HTML_FULL: return path + sidecar_suffix = SIDECAR_SUFFIXES.get(artifact.kind) + if sidecar_suffix: + return path.with_suffix(sidecar_suffix) return None diff --git a/docs/api.md b/docs/api.md index 5001d8a..65c331b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -35,6 +35,12 @@ Currently supported kinds: | `image` | Multipart file | Browser image preview | No | | `pdf` | Multipart file | Browser PDF preview | No | | `binary` | Multipart file | Stored and downloadable | No | +| `json` | JSON text | Interactive collapsible tree view (must be valid JSON) and original text is preserved | Yes | +| `csv` | JSON text | Interactive sortable/searchable table (parsed server-side) and original text is preserved | Yes | + +`json` and `csv` content is required to be a JSON/DRF **string** field even +though the value itself is JSON/CSV text — i.e. `{"kind": "json", "content": +"{\"a\": 1}"}`, not a nested JSON object in the request body. ## Source Fields @@ -84,15 +90,17 @@ Required fields: | Field | Notes | |---|---| | `source` | Required. Max 80 characters. | -| `kind` | One of `html_fragment`, `html_full`, `markdown`, `deck`. | +| `kind` | One of `html_fragment`, `html_full`, `markdown`, `deck`, `json`, `csv`. | | `title` | Required. Max 200 characters. | | `content` | Required text content. Must match the shape `kind` implies — see below. | `content` shape is validated per `kind`: `html_fragment` must not contain ` + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/static/vendor/gridjs/NOTICE.md b/static/vendor/gridjs/NOTICE.md new file mode 100644 index 0000000..367b70e --- /dev/null +++ b/static/vendor/gridjs/NOTICE.md @@ -0,0 +1,3 @@ +Vendored from https://cdn.jsdelivr.net/npm/gridjs@6.2.0 (MIT, see `LICENSE`). + +Files: `gridjs.umd.js` (library), `mermaid.min.css` (bundled theme). diff --git a/static/vendor/gridjs/gridjs.umd.js b/static/vendor/gridjs/gridjs.umd.js new file mode 100644 index 0000000..31761d7 --- /dev/null +++ b/static/vendor/gridjs/gridjs.umd.js @@ -0,0 +1,2 @@ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t||self).gridjs={})}(this,function(t){function n(t,n){for(var e=0;et.length)&&(n=t.length);for(var e=0,r=new Array(n);e=t.length?{done:!0}:{done:!1,value:t[r++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var l;!function(t){t[t.Init=0]="Init",t[t.Loading=1]="Loading",t[t.Loaded=2]="Loaded",t[t.Rendered=3]="Rendered",t[t.Error=4]="Error"}(l||(l={}));var c,f,p,d,h,_,m,v={},g=[],y=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function b(t,n){for(var e in n)t[e]=n[e];return t}function w(t){var n=t.parentNode;n&&n.removeChild(t)}function x(t,n,e){var r,o,i,u={};for(i in n)"key"==i?r=n[i]:"ref"==i?o=n[i]:u[i]=n[i];if(arguments.length>2&&(u.children=arguments.length>3?c.call(arguments,2):e),"function"==typeof t&&null!=t.defaultProps)for(i in t.defaultProps)void 0===u[i]&&(u[i]=t.defaultProps[i]);return k(t,u,r,o,null)}function k(t,n,e,r,o){var i={type:t,props:n,key:e,ref:r,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==o?++p:o};return null==o&&null!=f.vnode&&f.vnode(i),i}function P(t){return t.children}function S(t,n){this.props=t,this.context=n}function N(t,n){if(null==n)return t.__?N(t.__,t.__.__k.indexOf(t)+1):null;for(var e;n0?k(d.type,d.props,d.key,d.ref?d.ref:null,d.__v):d)){if(d.__=e,d.__b=e.__b+1,null===(p=y[c])||p&&d.key==p.key&&d.type===p.type)y[c]=void 0;else for(f=0;f0&&(this.callbacks[e].forEach(function(t){return t.apply(void 0,[].slice.call(n,1))}),!0)},t}();function Y(t,n){if(typeof t!=typeof n)return!1;if(null===t&&null===n)return!0;if("object"!=typeof t)return t===n;if(Array.isArray(t)&&Array.isArray(n)){if(t.length!==n.length)return!1;for(var e=0;e=e.__.length&&e.__.push({__V:ft}),e.__[t]}function gt(t){return lt=1,function(t,n,e){var r=vt(ot++,2);if(r.t=t,!r.__c&&(r.__=[Et(void 0,n),function(t){var n=r.__N?r.__N[0]:r.__[0],e=r.t(n,t);n!==e&&(r.__N=[e,r.__[1]],r.__c.setState({}))}],r.__c=it,!it.u)){it.u=!0;var o=it.shouldComponentUpdate;it.shouldComponentUpdate=function(t,n,e){if(!r.__c.__H)return!0;var i=r.__c.__H.__.filter(function(t){return t.__c});if(i.every(function(t){return!t.__N}))return!o||o.call(this,t,n,e);var u=!1;return i.forEach(function(t){if(t.__N){var n=t.__[0];t.__=t.__N,t.__N=void 0,n!==t.__[0]&&(u=!0)}}),!(!u&&r.__c.props===t)&&(!o||o.call(this,t,n,e))}}return r.__N||r.__}(Et,t)}function yt(t,n){var e=vt(ot++,3);!f.__s&&Ct(e.__H,n)&&(e.__=t,e.i=n,it.__H.__h.push(e))}function bt(t){return lt=5,wt(function(){return{current:t}},[])}function wt(t,n){var e=vt(ot++,7);return Ct(e.__H,n)?(e.__V=t(),e.i=n,e.__h=t,e.__V):e.__}function xt(){for(var t;t=ct.shift();)if(t.__P&&t.__H)try{t.__H.__h.forEach(St),t.__H.__h.forEach(Nt),t.__H.__h=[]}catch(n){t.__H.__h=[],f.__e(n,t.__v)}}f.__b=function(t){it=null,pt&&pt(t)},f.__r=function(t){dt&&dt(t),ot=0;var n=(it=t.__c).__H;n&&(ut===it?(n.__h=[],it.__h=[],n.__.forEach(function(t){t.__N&&(t.__=t.__N),t.__V=ft,t.__N=t.i=void 0})):(n.__h.forEach(St),n.__h.forEach(Nt),n.__h=[])),ut=it},f.diffed=function(t){ht&&ht(t);var n=t.__c;n&&n.__H&&(n.__H.__h.length&&(1!==ct.push(n)&&st===f.requestAnimationFrame||((st=f.requestAnimationFrame)||Pt)(xt)),n.__H.__.forEach(function(t){t.i&&(t.__H=t.i),t.__V!==ft&&(t.__=t.__V),t.i=void 0,t.__V=ft})),ut=it=null},f.__c=function(t,n){n.some(function(t){try{t.__h.forEach(St),t.__h=t.__h.filter(function(t){return!t.__||Nt(t)})}catch(e){n.some(function(t){t.__h&&(t.__h=[])}),n=[],f.__e(e,t.__v)}}),_t&&_t(t,n)},f.unmount=function(t){mt&&mt(t);var n,e=t.__c;e&&e.__H&&(e.__H.__.forEach(function(t){try{St(t)}catch(t){n=t}}),e.__H=void 0,n&&f.__e(n,e.__v))};var kt="function"==typeof requestAnimationFrame;function Pt(t){var n,e=function(){clearTimeout(r),kt&&cancelAnimationFrame(n),setTimeout(t)},r=setTimeout(e,100);kt&&(n=requestAnimationFrame(e))}function St(t){var n=it,e=t.__c;"function"==typeof e&&(t.__c=void 0,e()),it=n}function Nt(t){var n=it;t.__c=t.__(),it=n}function Ct(t,n){return!t||t.length!==n.length||n.some(function(n,e){return n!==t[e]})}function Et(t,n){return"function"==typeof n?n(t):n}function It(){return function(t){var n=it.context[t.__c],e=vt(ot++,9);return e.c=t,n?(null==e.__&&(e.__=!0,n.sub(it)),n.props.value):t.__}(fn)}var Tt={search:{placeholder:"Type a keyword..."},sort:{sortAsc:"Sort column ascending",sortDesc:"Sort column descending"},pagination:{previous:"Previous",next:"Next",navigate:function(t,n){return"Page "+t+" of "+n},page:function(t){return"Page "+t},showing:"Showing",of:"of",to:"to",results:"results"},loading:"Loading...",noRecordsFound:"No matching records found",error:"An error happened while fetching the data"},Lt=/*#__PURE__*/function(){function t(t){this._language=void 0,this._defaultLanguage=void 0,this._language=t,this._defaultLanguage=Tt}var n=t.prototype;return n.getString=function(t,n){if(!n||!t)return null;var e=t.split("."),r=e[0];if(n[r]){var o=n[r];return"string"==typeof o?function(){return o}:"function"==typeof o?o:this.getString(e.slice(1).join("."),o)}return null},n.translate=function(t){var n,e=this.getString(t,this._language);return(n=e||this.getString(t,this._defaultLanguage))?n.apply(void 0,[].slice.call(arguments,1)):t},t}();function At(){var t=It();return function(n){var e;return(e=t.translator).translate.apply(e,[n].concat([].slice.call(arguments,1)))}}var Ot=function(t){return function(n){return r({},n,{search:{keyword:t}})}};function jt(){return It().store}function Ht(t){var n=jt(),e=gt(t(n.getState())),r=e[0],o=e[1];return yt(function(){return n.subscribe(function(){var e=t(n.getState());r!==e&&o(e)})},[]),r}function Dt(){var t,n=gt(void 0),e=n[0],r=n[1],o=It(),i=o.search,u=At(),s=jt().dispatch,a=Ht(function(t){return t.search});yt(function(){e&&e.setProps({keyword:null==a?void 0:a.keyword})},[a,e]),yt(function(){r(i.server?new at({keyword:i.keyword,url:i.server.url,body:i.server.body}):new nt({keyword:i.keyword,columns:o.header&&o.header.columns,ignoreHiddenColumns:i.ignoreHiddenColumns||void 0===i.ignoreHiddenColumns,selector:i.selector})),i.keyword&&s(Ot(i.keyword))},[i]),yt(function(){if(e)return o.pipeline.register(e),function(){return o.pipeline.unregister(e)}},[o,e]);var l,c,f,p=function(t,n){return lt=8,wt(function(){return t},n)}((l=function(t){t.target instanceof HTMLInputElement&&s(Ot(t.target.value))},c=e instanceof at?i.debounceTimeout||250:0,function(){var t=arguments;return new Promise(function(n){f&&clearTimeout(f),f=setTimeout(function(){return n(l.apply(void 0,[].slice.call(t)))},c)})}),[i,e]);return x("div",{className:et(rt("search",null==(t=o.className)?void 0:t.search))},x("input",{type:"search",placeholder:u("search.placeholder"),"aria-label":u("search.placeholder"),onInput:p,className:rt(et("input"),et("search","input")),defaultValue:(null==a?void 0:a.keyword)||""}))}var Mt=/*#__PURE__*/function(t){function n(){return t.apply(this,arguments)||this}o(n,t);var r=n.prototype;return r.validateProps=function(){if(isNaN(Number(this.props.limit))||isNaN(Number(this.props.page)))throw Error("Invalid parameters passed")},r._process=function(t){var n=this.props.page;return new J(t.rows.slice(n*this.props.limit,(n+1)*this.props.limit))},e(n,[{key:"type",get:function(){return K.Limit}}]),n}(tt),Rt=/*#__PURE__*/function(t){function n(){return t.apply(this,arguments)||this}return o(n,t),n.prototype._process=function(t){var n={};return this.props.url&&(n.url=this.props.url(t.url,this.props.page,this.props.limit)),this.props.body&&(n.body=this.props.body(t.body,this.props.page,this.props.limit)),r({},t,n)},e(n,[{key:"type",get:function(){return K.ServerLimit}}]),n}(tt);function Ft(){var t=It(),n=t.pagination,e=n.server,r=n.summary,o=void 0===r||r,i=n.nextButton,u=void 0===i||i,s=n.prevButton,a=void 0===s||s,l=n.buttonsCount,c=void 0===l?3:l,f=n.limit,p=void 0===f?10:f,d=n.page,h=void 0===d?0:d,_=n.resetPageOnUpdate,m=void 0===_||_,v=bt(null),g=gt(h),y=g[0],b=g[1],w=gt(0),k=w[0],S=w[1],N=At();yt(function(){return e?(v.current=new Rt({limit:p,page:y,url:e.url,body:e.body}),t.pipeline.register(v.current)):(v.current=new Mt({limit:p,page:y}),t.pipeline.register(v.current)),v.current instanceof Rt?t.pipeline.on("afterProcess",function(t){return S(t.length)}):v.current instanceof Mt&&v.current.on("beforeProcess",function(t){return S(t.length)}),t.pipeline.on("updated",C),t.pipeline.on("error",function(){S(0),b(0)}),function(){t.pipeline.unregister(v.current),t.pipeline.off("updated",C)}},[]);var C=function(t){m&&t!==v.current&&(b(0),0!==v.current.props.page&&v.current.setProps({page:0}))},E=function(){return Math.ceil(k/p)},I=function(t){if(t>=E()||t<0||t===y)return null;b(t),v.current.setProps({page:t})};return x("div",{className:rt(et("pagination"),t.className.pagination)},x(P,null,o&&k>0&&x("div",{role:"status","aria-live":"polite",className:rt(et("summary"),t.className.paginationSummary),title:N("pagination.navigate",y+1,E())},N("pagination.showing")," ",x("b",null,N(""+(y*p+1)))," ",N("pagination.to")," ",x("b",null,N(""+Math.min((y+1)*p,k)))," ",N("pagination.of")," ",x("b",null,N(""+k))," ",N("pagination.results"))),x("div",{className:et("pages")},a&&x("button",{tabIndex:0,role:"button",disabled:0===y,onClick:function(){return I(y-1)},title:N("pagination.previous"),"aria-label":N("pagination.previous"),className:rt(t.className.paginationButton,t.className.paginationButtonPrev)},N("pagination.previous")),function(){if(c<=0)return null;var n=Math.min(E(),c),e=Math.min(y,Math.floor(n/2));return y+Math.floor(n/2)>=E()&&(e=n-(E()-y)),x(P,null,E()>n&&y-e>0&&x(P,null,x("button",{tabIndex:0,role:"button",onClick:function(){return I(0)},title:N("pagination.firstPage"),"aria-label":N("pagination.firstPage"),className:t.className.paginationButton},N("1")),x("button",{tabIndex:-1,className:rt(et("spread"),t.className.paginationButton)},"...")),Array.from(Array(n).keys()).map(function(t){return y+(t-e)}).map(function(n){return x("button",{tabIndex:0,role:"button",onClick:function(){return I(n)},className:rt(y===n?rt(et("currentPage"),t.className.paginationButtonCurrent):null,t.className.paginationButton),title:N("pagination.page",n+1),"aria-label":N("pagination.page",n+1)},N(""+(n+1)))}),E()>n&&E()>y+e+1&&x(P,null,x("button",{tabIndex:-1,className:rt(et("spread"),t.className.paginationButton)},"..."),x("button",{tabIndex:0,role:"button",onClick:function(){return I(E()-1)},title:N("pagination.page",E()),"aria-label":N("pagination.page",E()),className:t.className.paginationButton},N(""+E()))))}(),u&&x("button",{tabIndex:0,role:"button",disabled:E()===y+1||0===E(),onClick:function(){return I(y+1)},title:N("pagination.next"),"aria-label":N("pagination.next"),className:rt(t.className.paginationButton,t.className.paginationButtonNext)},N("pagination.next"))))}function Ut(t,n){return"string"==typeof t?t.indexOf("%")>-1?n/100*parseInt(t,10):parseInt(t,10):t}function Wt(t){return t?Math.floor(t)+"px":""}function Bt(t){var n=t.tableRef.cloneNode(!0);return n.style.position="absolute",n.style.width="100%",n.style.zIndex="-2147483640",n.style.visibility="hidden",x("div",{ref:function(t){t&&t.appendChild(n)}})}function qt(t){if(!t)return"";var n=t.split(" ");return 1===n.length&&/([a-z][A-Z])+/g.test(t)?t:n.map(function(t,n){return 0==n?t.toLowerCase():t.charAt(0).toUpperCase()+t.slice(1).toLowerCase()}).join("")}var zt,Vt=new(/*#__PURE__*/function(){function t(){}var n=t.prototype;return n.format=function(t,n){return"[Grid.js] ["+n.toUpperCase()+"]: "+t},n.error=function(t,n){void 0===n&&(n=!1);var e=this.format(t,"error");if(n)throw Error(e);console.error(e)},n.warn=function(t){console.warn(this.format(t,"warn"))},n.info=function(t){console.info(this.format(t,"info"))},t}());t.PluginPosition=void 0,(zt=t.PluginPosition||(t.PluginPosition={}))[zt.Header=0]="Header",zt[zt.Footer=1]="Footer",zt[zt.Cell=2]="Cell";var $t=/*#__PURE__*/function(){function t(){this.plugins=void 0,this.plugins=[]}var n=t.prototype;return n.get=function(t){return this.plugins.find(function(n){return n.id===t})},n.add=function(t){return t.id?this.get(t.id)?(Vt.error("Duplicate plugin ID: "+t.id),this):(this.plugins.push(t),this):(Vt.error("Plugin ID cannot be empty"),this)},n.remove=function(t){var n=this.get(t);return n&&this.plugins.splice(this.plugins.indexOf(n),1),this},n.list=function(t){var n;return n=null!=t||null!=t?this.plugins.filter(function(n){return n.position===t}):this.plugins,n.sort(function(t,n){return t.order&&n.order?t.order-n.order:1})},t}();function Gt(t){var n=this,e=It();if(t.pluginId){var o=e.plugin.get(t.pluginId);return o?x(P,{},x(o.component,r({plugin:o},t.props))):null}return void 0!==t.position?x(P,{},e.plugin.list(t.position).map(function(t){return x(t.component,r({plugin:t},n.props.props))})):null}var Kt=/*#__PURE__*/function(n){function i(){var t;return(t=n.call(this)||this)._columns=void 0,t._columns=[],t}o(i,n);var u=i.prototype;return u.adjustWidth=function(t,n,e){var o=t.container,u=t.autoWidth;if(!o)return this;var s=o.clientWidth,l={};n.current&&u&&(q(x(Bt,{tableRef:n.current}),e.current),l=function(t){var n=t.querySelector("table");if(!n)return{};var e=n.className,o=n.style.cssText;n.className=e+" "+et("shadowTable"),n.style.tableLayout="auto",n.style.width="auto",n.style.padding="0",n.style.margin="0",n.style.border="none",n.style.outline="none";var i=Array.from(n.parentNode.querySelectorAll("thead th")).reduce(function(t,n){var e;return n.style.width=n.clientWidth+"px",r(((e={})[n.getAttribute("data-column-id")]={minWidth:n.clientWidth},e),t)},{});return n.className=e,n.style.cssText=o,n.style.tableLayout="auto",Array.from(n.parentNode.querySelectorAll("thead th")).reduce(function(t,n){return t[n.getAttribute("data-column-id")].width=n.clientWidth,t},i)}(e.current));for(var c,f=a(i.tabularFormat(this.columns).reduce(function(t,n){return t.concat(n)},[]));!(c=f()).done;){var p=c.value;p.columns&&p.columns.length>0||(!p.width&&u?p.id in l&&(p.width=Wt(l[p.id].width),p.minWidth=Wt(l[p.id].minWidth)):p.width=Wt(Ut(p.width,s)))}return n.current&&u&&q(null,e.current),this},u.setSort=function(t,n){for(var e,o=a(n||this.columns||[]);!(e=o()).done;){var i=e.value;i.columns&&i.columns.length>0?i.sort=void 0:void 0===i.sort&&t?i.sort={}:i.sort?"object"==typeof i.sort&&(i.sort=r({},i.sort)):i.sort=void 0,i.columns&&this.setSort(t,i.columns)}},u.setResizable=function(t,n){for(var e,r=a(n||this.columns||[]);!(e=r()).done;){var o=e.value;void 0===o.resizable&&(o.resizable=t),o.columns&&this.setResizable(t,o.columns)}},u.setID=function(t){for(var n,e=a(t||this.columns||[]);!(n=e()).done;){var r=n.value;r.id||"string"!=typeof r.name||(r.id=qt(r.name)),r.id||Vt.error('Could not find a valid ID for one of the columns. Make sure a valid "id" is set for all columns.'),r.columns&&this.setID(r.columns)}},u.populatePlugins=function(n,e){for(var o,i=a(e);!(o=i()).done;){var u=o.value;void 0!==u.plugin&&n.add(r({id:u.id},u.plugin,{position:t.PluginPosition.Cell}))}},i.fromColumns=function(t){for(var n,e=new i,r=a(t);!(n=r()).done;){var o=n.value;if("string"==typeof o||d(o))e.columns.push({name:o});else if("object"==typeof o){var u=o;u.columns&&(u.columns=i.fromColumns(u.columns).columns),"object"==typeof u.plugin&&void 0===u.data&&(u.data=null),e.columns.push(o)}}return e},i.createFromConfig=function(t){var n=new i;return t.from?n.columns=i.fromHTMLTable(t.from).columns:t.columns?n.columns=i.fromColumns(t.columns).columns:!t.data||"object"!=typeof t.data[0]||t.data[0]instanceof Array||(n.columns=Object.keys(t.data[0]).map(function(t){return{name:t}})),n.columns.length?(n.setID(),n.setSort(t.sort),n.setResizable(t.resizable),n.populatePlugins(t.plugin,n.columns),n):null},i.fromHTMLTable=function(t){for(var n,e=new i,r=a(t.querySelector("thead").querySelectorAll("th"));!(n=r()).done;){var o=n.value;e.columns.push({name:o.innerHTML,width:o.width})}return e},i.tabularFormat=function(t){var n=[],e=t||[],r=[];if(e&&e.length){n.push(e);for(var o,i=a(e);!(o=i()).done;){var u=o.value;u.columns&&u.columns.length&&(r=r.concat(u.columns))}r.length&&(n=n.concat(this.tabularFormat(r)))}return n},i.leafColumns=function(t){var n=[],e=t||[];if(e&&e.length)for(var r,o=a(e);!(r=o()).done;){var i=r.value;i.columns&&0!==i.columns.length||n.push(i),i.columns&&(n=n.concat(this.leafColumns(i.columns)))}return n},i.maximumDepth=function(t){return this.tabularFormat([t]).length-1},e(i,[{key:"columns",get:function(){return this._columns},set:function(t){this._columns=t}},{key:"visibleColumns",get:function(){return this._columns.filter(function(t){return!t.hidden})}}]),i}(V),Xt=function(){},Zt=/*#__PURE__*/function(t){function n(n){var e;return(e=t.call(this)||this).data=void 0,e.set(n),e}o(n,t);var e=n.prototype;return e.get=function(){try{return Promise.resolve(this.data()).then(function(t){return{data:t,total:t.length}})}catch(t){return Promise.reject(t)}},e.set=function(t){return t instanceof Array?this.data=function(){return t}:t instanceof Function&&(this.data=t),this},n}(Xt),Jt=/*#__PURE__*/function(t){function n(n){var e;return(e=t.call(this)||this).options=void 0,e.options=n,e}o(n,t);var e=n.prototype;return e.handler=function(t){return"function"==typeof this.options.handle?this.options.handle(t):t.ok?t.json():(Vt.error("Could not fetch data: "+t.status+" - "+t.statusText,!0),null)},e.get=function(t){var n=r({},this.options,t);return"function"==typeof n.data?n.data(n):fetch(n.url,n).then(this.handler.bind(this)).then(function(t){return{data:n.then(t),total:"function"==typeof n.total?n.total(t):void 0}})},n}(Xt),Qt=/*#__PURE__*/function(){function t(){}return t.createFromConfig=function(t){var n=null;return t.data&&(n=new Zt(t.data)),t.from&&(n=new Zt(this.tableElementToArray(t.from)),t.from.style.display="none"),t.server&&(n=new Jt(t.server)),n||Vt.error("Could not determine the storage type",!0),n},t.tableElementToArray=function(t){for(var n,e,r=[],o=a(t.querySelector("tbody").querySelectorAll("tr"));!(n=o()).done;){for(var i,u=[],s=a(n.value.querySelectorAll("td"));!(i=s()).done;){var l=i.value;1===l.childNodes.length&&l.childNodes[0].nodeType===Node.TEXT_NODE?u.push((e=l.innerHTML,(new DOMParser).parseFromString(e,"text/html").documentElement.textContent)):u.push(G(l.innerHTML))}r.push(u)}return r},t}(),Yt="undefined"!=typeof Symbol?Symbol.iterator||(Symbol.iterator=Symbol("Symbol.iterator")):"@@iterator";function tn(t,n,e){if(!t.s){if(e instanceof nn){if(!e.s)return void(e.o=tn.bind(null,t,n));1&n&&(n=e.s),e=e.v}if(e&&e.then)return void e.then(tn.bind(null,t,n),tn.bind(null,t,2));t.s=n,t.v=e;var r=t.o;r&&r(t)}}var nn=/*#__PURE__*/function(){function t(){}return t.prototype.then=function(n,e){var r=new t,o=this.s;if(o){var i=1&o?n:e;if(i){try{tn(r,1,i(this.v))}catch(t){tn(r,2,t)}return r}return this}return this.o=function(t){try{var o=t.v;1&t.s?tn(r,1,n?n(o):o):e?tn(r,1,e(o)):tn(r,2,o)}catch(t){tn(r,2,t)}},r},t}();function en(t){return t instanceof nn&&1&t.s}var rn=/*#__PURE__*/function(t){function n(n){var e;return(e=t.call(this)||this)._steps=new Map,e.cache=new Map,e.lastProcessorIndexUpdated=-1,n&&n.forEach(function(t){return e.register(t)}),e}o(n,t);var r=n.prototype;return r.clearCache=function(){this.cache=new Map,this.lastProcessorIndexUpdated=-1},r.register=function(t,n){if(void 0===n&&(n=null),!t)throw Error("Processor is not defined");if(null===t.type)throw Error("Processor type is not defined");if(this.findProcessorIndexByID(t.id)>-1)throw Error("Processor ID "+t.id+" is already defined");return t.on("propsUpdated",this.processorPropsUpdated.bind(this)),this.addProcessorByPriority(t,n),this.afterRegistered(t),t},r.tryRegister=function(t,n){void 0===n&&(n=null);try{return this.register(t,n)}catch(t){}},r.unregister=function(t){if(t&&-1!==this.findProcessorIndexByID(t.id)){var n=this._steps.get(t.type);n&&n.length&&(this._steps.set(t.type,n.filter(function(n){return n!=t})),this.emit("updated",t))}},r.addProcessorByPriority=function(t,n){var e=this._steps.get(t.type);if(!e){var r=[];this._steps.set(t.type,r),e=r}if(null===n||n<0)e.push(t);else if(e[n]){var o=e.slice(0,n-1),i=e.slice(n+1);this._steps.set(t.type,o.concat(t).concat(i))}else e[n]=t},r.getStepsByType=function(t){return this.steps.filter(function(n){return n.type===t})},r.getSortedProcessorTypes=function(){return Object.keys(K).filter(function(t){return!isNaN(Number(t))}).map(function(t){return Number(t)})},r.process=function(t){try{var n=function(t){return e.lastProcessorIndexUpdated=o.length,e.emit("afterProcess",i),i},e=this,r=e.lastProcessorIndexUpdated,o=e.steps,i=t,u=function(t,n){try{var u=function(t,n,e){if("function"==typeof t[Yt]){var r,o,i,u=t[Yt]();if(function t(e){try{for(;!(r=u.next()).done;)if((e=n(r.value))&&e.then){if(!en(e))return void e.then(t,i||(i=tn.bind(null,o=new nn,2)));e=e.v}o?tn(o,1,e):o=e}catch(t){tn(o||(o=new nn),2,t)}}(),u.return){var s=function(t){try{r.done||u.return()}catch(t){}return t};if(o&&o.then)return o.then(s,function(t){throw s(t)});s()}return o}if(!("length"in t))throw new TypeError("Object is not iterable");for(var a=[],l=0;l=r)return Promise.resolve(t.process(i)).then(function(n){e.cache.set(t.id,i=n)});i=e.cache.get(t.id)}();if(o&&o.then)return o.then(function(){})})}catch(t){return n(t)}return u&&u.then?u.then(void 0,n):u}(0,function(t){throw Vt.error(t),e.emit("error",i),t});return Promise.resolve(u&&u.then?u.then(n):n())}catch(t){return Promise.reject(t)}},r.findProcessorIndexByID=function(t){return this.steps.findIndex(function(n){return n.id==t})},r.setLastProcessorIndex=function(t){var n=this.findProcessorIndexByID(t.id);this.lastProcessorIndexUpdated>n&&(this.lastProcessorIndexUpdated=n)},r.processorPropsUpdated=function(t){this.setLastProcessorIndex(t),this.emit("propsUpdated"),this.emit("updated",t)},r.afterRegistered=function(t){this.setLastProcessorIndex(t),this.emit("afterRegister"),this.emit("updated",t)},e(n,[{key:"steps",get:function(){for(var t,n=[],e=a(this.getSortedProcessorTypes());!(t=e()).done;){var r=this._steps.get(t.value);r&&r.length&&(n=n.concat(r))}return n.filter(function(t){return t})}}]),n}(Q),on=/*#__PURE__*/function(t){function n(){return t.apply(this,arguments)||this}return o(n,t),n.prototype._process=function(t){try{return Promise.resolve(this.props.storage.get(t))}catch(t){return Promise.reject(t)}},e(n,[{key:"type",get:function(){return K.Extractor}}]),n}(tt),un=/*#__PURE__*/function(t){function n(){return t.apply(this,arguments)||this}return o(n,t),n.prototype._process=function(t){var n=J.fromArray(t.data);return n.length=t.total,n},e(n,[{key:"type",get:function(){return K.Transformer}}]),n}(tt),sn=/*#__PURE__*/function(t){function n(){return t.apply(this,arguments)||this}return o(n,t),n.prototype._process=function(){return Object.entries(this.props.serverStorageOptions).filter(function(t){return"function"!=typeof t[1]}).reduce(function(t,n){var e;return r({},t,((e={})[n[0]]=n[1],e))},{})},e(n,[{key:"type",get:function(){return K.Initiator}}]),n}(tt),an=/*#__PURE__*/function(t){function n(){return t.apply(this,arguments)||this}o(n,t);var r=n.prototype;return r.castData=function(t){if(!t||!t.length)return[];if(!this.props.header||!this.props.header.columns)return t;var n=Kt.leafColumns(this.props.header.columns);return t[0]instanceof Array?t.map(function(t){var e=0;return n.map(function(n,r){return void 0!==n.data?(e++,"function"==typeof n.data?n.data(t):n.data):t[r-e]})}):"object"!=typeof t[0]||t[0]instanceof Array?[]:t.map(function(t){return n.map(function(n,e){return void 0!==n.data?"function"==typeof n.data?n.data(t):n.data:n.id?t[n.id]:(Vt.error("Could not find the correct cell for column at position "+e+".\n Make sure either 'id' or 'selector' is defined for all columns."),null)})})},r._process=function(t){return{data:this.castData(t.data),total:t.total}},e(n,[{key:"type",get:function(){return K.Transformer}}]),n}(tt),ln=/*#__PURE__*/function(){function t(){}return t.createFromConfig=function(t){var n=new rn;return t.storage instanceof Jt&&n.register(new sn({serverStorageOptions:t.server})),n.register(new on({storage:t.storage})),n.register(new an({header:t.header})),n.register(new un),n},t}(),cn=function(t){var n=this;this.state=void 0,this.listeners=[],this.isDispatching=!1,this.getState=function(){return n.state},this.getListeners=function(){return n.listeners},this.dispatch=function(t){if("function"!=typeof t)throw new Error("Reducer is not a function");if(n.isDispatching)throw new Error("Reducers may not dispatch actions");n.isDispatching=!0;var e=n.state;try{n.state=t(n.state)}finally{n.isDispatching=!1}for(var r,o=a(n.listeners);!(r=o()).done;)(0,r.value)(n.state,e);return n.state},this.subscribe=function(t){if("function"!=typeof t)throw new Error("Listener is not a function");return n.listeners=[].concat(n.listeners,[t]),function(){return n.listeners=n.listeners.filter(function(n){return n!==t})}},this.state=t},fn=function(t,n){var e={__c:n="__cC"+m++,__:null,Consumer:function(t,n){return t.children(n)},Provider:function(t){var e,r;return this.getChildContext||(e=[],(r={})[n]=this,this.getChildContext=function(){return r},this.shouldComponentUpdate=function(t){this.props.value!==t.value&&e.some(E)},this.sub=function(t){e.push(t);var n=t.componentWillUnmount;t.componentWillUnmount=function(){e.splice(e.indexOf(t),1),n&&n.call(t)}}),t.children}};return e.Provider.__=e.Consumer.contextType=e}(),pn=/*#__PURE__*/function(){function n(){Object.assign(this,n.defaultConfig())}var e=n.prototype;return e.assign=function(t){return Object.assign(this,t)},e.update=function(t){return t?(this.assign(n.fromPartialConfig(r({},this,t))),this):this},n.defaultConfig=function(){return{store:new cn({status:l.Init,header:void 0,data:null}),plugin:new $t,tableRef:{current:null},width:"100%",height:"auto",processingThrottleMs:100,autoWidth:!0,style:{},className:{}}},n.fromPartialConfig=function(e){var r=(new n).assign(e);return"boolean"==typeof e.sort&&e.sort&&r.assign({sort:{multiColumn:!0}}),r.assign({header:Kt.createFromConfig(r)}),r.assign({storage:Qt.createFromConfig(r)}),r.assign({pipeline:ln.createFromConfig(r)}),r.assign({translator:new Lt(r.language)}),r.plugin=new $t,r.search&&r.plugin.add({id:"search",position:t.PluginPosition.Header,component:Dt}),r.pagination&&r.plugin.add({id:"pagination",position:t.PluginPosition.Footer,component:Ft}),r.plugins&&r.plugins.forEach(function(t){return r.plugin.add(t)}),r},n}();function dn(t){var n,e=It();return x("td",r({role:t.role,colSpan:t.colSpan,"data-column-id":t.column&&t.column.id,className:rt(et("td"),t.className,e.className.td),style:r({},t.style,e.style.td),onClick:function(n){t.messageCell||e.eventEmitter.emit("cellClick",n,t.cell,t.column,t.row)}},(n=t.column)?"function"==typeof n.attributes?n.attributes(t.cell.data,t.row,t.column):n.attributes:{}),t.column&&"function"==typeof t.column.formatter?t.column.formatter(t.cell.data,t.row,t.column):t.column&&t.column.plugin?x(Gt,{pluginId:t.column.id,props:{column:t.column,cell:t.cell,row:t.row}}):t.cell.data)}function hn(t){var n=It(),e=Ht(function(t){return t.header});return x("tr",{className:rt(et("tr"),n.className.tr),onClick:function(e){t.messageRow||n.eventEmitter.emit("rowClick",e,t.row)}},t.children?t.children:t.row.cells.map(function(n,r){var o=function(t){if(e){var n=Kt.leafColumns(e.columns);if(n)return n[t]}return null}(r);return o&&o.hidden?null:x(dn,{key:n.id,cell:n,row:t.row,column:o})}))}function _n(t){return x(hn,{messageRow:!0},x(dn,{role:"alert",colSpan:t.colSpan,messageCell:!0,cell:new X(t.message),className:rt(et("message"),t.className?t.className:null)}))}function mn(){var t=It(),n=Ht(function(t){return t.data}),e=Ht(function(t){return t.status}),r=Ht(function(t){return t.header}),o=At(),i=function(){return r?r.visibleColumns.length:0};return x("tbody",{className:rt(et("tbody"),t.className.tbody)},n&&n.rows.map(function(t){return x(hn,{key:t.id,row:t})}),e===l.Loading&&(!n||0===n.length)&&x(_n,{message:o("loading"),colSpan:i(),className:rt(et("loading"),t.className.loading)}),e===l.Rendered&&n&&0===n.length&&x(_n,{message:o("noRecordsFound"),colSpan:i(),className:rt(et("notfound"),t.className.notfound)}),e===l.Error&&x(_n,{message:o("error"),colSpan:i(),className:rt(et("error"),t.className.error)}))}var vn=/*#__PURE__*/function(t){function n(){return t.apply(this,arguments)||this}o(n,t);var r=n.prototype;return r.validateProps=function(){for(var t,n=a(this.props.columns);!(t=n()).done;){var e=t.value;void 0===e.direction&&(e.direction=1),1!==e.direction&&-1!==e.direction&&Vt.error("Invalid sort direction "+e.direction)}},r.compare=function(t,n){return t>n?1:t1&&(f=!0,c=!0):0===a?c=!0:a>0&&!e?(c=!0,f=!0):a>0&&e&&(c=!0),f&&(s=[]),c)s.push({index:t,direction:n,compare:o});else if(d){var h=s.indexOf(l);s[h].direction=n}else if(p){var _=s.indexOf(l);s.splice(_,1)}return r({},i,{sort:{columns:s}})}},yn=function(t,n,e){return function(o){var i=(o.sort?[].concat(o.sort.columns):[]).find(function(n){return n.index===t});return r({},o,i?gn(t,1===i.direction?-1:1,n,e)(o):gn(t,1,n,e)(o))}},bn=/*#__PURE__*/function(t){function n(){return t.apply(this,arguments)||this}return o(n,t),n.prototype._process=function(t){var n={};return this.props.url&&(n.url=this.props.url(t.url,this.props.columns)),this.props.body&&(n.body=this.props.body(t.body,this.props.columns)),r({},t,n)},e(n,[{key:"type",get:function(){return K.ServerSort}}]),n}(tt);function wn(t){var n=It(),e=jt().dispatch,o=At(),i=gt(0),u=i[0],s=i[1],a=n.sort,l=Ht(function(t){return t.sort}),c="object"==typeof(null==a?void 0:a.server)?K.ServerSort:K.Sort,f=function(){var t=n.pipeline.getStepsByType(c);if(t.length)return t[0]};return yt(function(){var t=f()||(c===K.ServerSort?new bn(r({columns:l?l.columns:[]},a.server)):new vn({columns:l?l.columns:[]}));return n.pipeline.tryRegister(t),function(){return n.pipeline.unregister(t)}},[n]),yt(function(){if(l){var n,e=l.columns.find(function(n){return n.index===t.index});e?(0===u&&(e.direction=null!=(n=t.direction)?n:1),s(e.direction)):s(0)}},[l]),yt(function(){var t=f();t&&l&&t.setProps({columns:l.columns})},[l]),x("button",{tabIndex:-1,"aria-label":o("sort.sort"+(1===u?"Desc":"Asc")),title:o("sort.sort"+(1===u?"Desc":"Asc")),className:rt(et("sort"),et("sort",function(t){return 1===t?"asc":-1===t?"desc":"neutral"}(u)),n.className.sort),onClick:function(n){n.preventDefault(),n.stopPropagation(),e(yn(t.index,!0===n.shiftKey&&a.multiColumn,t.compare))}})}var xn=function(t,n){var e;void 0===n&&(n=100);var r=Date.now(),o=function(){r=Date.now(),t.apply(void 0,[].slice.call(arguments))};return function(){var t=[].slice.call(arguments),i=Date.now(),u=i-r;u>=n?o.apply(void 0,t):(e&&clearTimeout(e),e=setTimeout(function(){o.apply(void 0,t),e=null},n-u))}};function kn(t){var n,e=function(t){return t instanceof MouseEvent?Math.floor(t.pageX):Math.floor(t.changedTouches[0].pageX)},r=function(r){r.stopPropagation();var u=parseInt(t.thRef.current.style.width,10)-e(r);n=xn(function(t){return o(t,u)},10),document.addEventListener("mouseup",i),document.addEventListener("touchend",i),document.addEventListener("mousemove",n),document.addEventListener("touchmove",n)},o=function(n,r){n.stopPropagation();var o=t.thRef.current;r+e(n)>=parseInt(o.style.minWidth,10)&&(o.style.width=r+e(n)+"px")},i=function t(e){e.stopPropagation(),document.removeEventListener("mouseup",t),document.removeEventListener("mousemove",n),document.removeEventListener("touchmove",n),document.removeEventListener("touchend",t)};return x("div",{className:rt(et("th"),et("resizable")),onMouseDown:r,onTouchStart:r,onClick:function(t){return t.stopPropagation()}})}function Pn(t){var n=It(),e=bt(null),o=gt({}),i=o[0],u=o[1],s=jt().dispatch;yt(function(){if(n.fixedHeader&&e.current){var t=e.current.offsetTop;"number"==typeof t&&u({top:t})}},[e]);var a,l=function(){return null!=t.column.sort},c=function(e){e.stopPropagation(),l()&&s(yn(t.index,!0===e.shiftKey&&n.sort.multiColumn,t.column.sort.compare))};return x("th",r({ref:e,"data-column-id":t.column&&t.column.id,className:rt(et("th"),l()?et("th","sort"):null,n.fixedHeader?et("th","fixed"):null,n.className.th),onClick:c,style:r({},n.style.th,{minWidth:t.column.minWidth,width:t.column.width},i,t.style),onKeyDown:function(t){l()&&13===t.which&&c(t)},rowSpan:t.rowSpan>1?t.rowSpan:void 0,colSpan:t.colSpan>1?t.colSpan:void 0},(a=t.column)?"function"==typeof a.attributes?a.attributes(null,null,t.column):a.attributes:{},l()?{tabIndex:0}:{}),x("div",{className:et("th","content")},void 0!==t.column.name?t.column.name:void 0!==t.column.plugin?x(Gt,{pluginId:t.column.plugin.id,props:{column:t.column}}):null),l()&&x(wn,r({index:t.index},t.column.sort)),t.column.resizable&&t.index0?(Vt.error("The container element "+t+" is not empty. Make sure the container is empty and call render() again"),this):(this.config.container=t,q(this.createElement(),t),this)},n}(Q);t.Cell=X,t.Component=S,t.Config=pn,t.Grid=Ln,t.Row=Z,t.className=et,t.createElement=x,t.createRef=function(){return{current:null}},t.h=x,t.html=G,t.useConfig=It,t.useEffect=yt,t.useRef=bt,t.useSelector=Ht,t.useState=gt,t.useStore=jt,t.useTranslator=At}); +//# sourceMappingURL=gridjs.umd.js.map diff --git a/static/vendor/gridjs/mermaid.min.css b/static/vendor/gridjs/mermaid.min.css new file mode 100644 index 0000000..21332c7 --- /dev/null +++ b/static/vendor/gridjs/mermaid.min.css @@ -0,0 +1 @@ +.gridjs-footer button,.gridjs-head button{background-color:transparent;background-image:none;border:none;cursor:pointer;margin:0;outline:none;padding:0}.gridjs-temp{position:relative}.gridjs-head{margin-bottom:5px;padding:5px 1px;width:100%}.gridjs-head:after{clear:both;content:"";display:block}.gridjs-head:empty{border:none;padding:0}.gridjs-container{color:#000;display:inline-block;overflow:hidden;padding:2px;position:relative;z-index:0}.gridjs-footer{background-color:#fff;border-bottom-width:1px;border-color:#e5e7eb;border-radius:0 0 8px 8px;border-top:1px solid #e5e7eb;box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.26);display:block;padding:12px 24px;position:relative;width:100%;z-index:5}.gridjs-footer:empty{border:none;padding:0}input.gridjs-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border:1px solid #d2d6dc;border-radius:5px;font-size:14px;line-height:1.45;outline:none;padding:10px 13px}input.gridjs-input:focus{border-color:#9bc2f7;box-shadow:0 0 0 3px rgba(149,189,243,.5)}.gridjs-pagination{color:#3d4044}.gridjs-pagination:after{clear:both;content:"";display:block}.gridjs-pagination .gridjs-summary{float:left;margin-top:5px}.gridjs-pagination .gridjs-pages{float:right}.gridjs-pagination .gridjs-pages button{background-color:#fff;border:1px solid #d2d6dc;border-right:none;outline:none;padding:5px 14px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.gridjs-pagination .gridjs-pages button:focus{border-right:1px solid #d2d6dc;box-shadow:0 0 0 2px rgba(149,189,243,.5);margin-right:-1px;position:relative}.gridjs-pagination .gridjs-pages button:hover{background-color:#f7f7f7;color:#3c4257;outline:none}.gridjs-pagination .gridjs-pages button:disabled,.gridjs-pagination .gridjs-pages button:hover:disabled,.gridjs-pagination .gridjs-pages button[disabled]{background-color:#fff;color:#6b7280;cursor:default}.gridjs-pagination .gridjs-pages button.gridjs-spread{background-color:#fff;box-shadow:none;cursor:default}.gridjs-pagination .gridjs-pages button.gridjs-currentPage{background-color:#f7f7f7;font-weight:700}.gridjs-pagination .gridjs-pages button:last-child{border-bottom-right-radius:6px;border-right:1px solid #d2d6dc;border-top-right-radius:6px}.gridjs-pagination .gridjs-pages button:first-child{border-bottom-left-radius:6px;border-top-left-radius:6px}.gridjs-pagination .gridjs-pages button:last-child:focus{margin-right:0}button.gridjs-sort{background-color:transparent;background-position-x:center;background-repeat:no-repeat;background-size:contain;border:none;cursor:pointer;float:right;height:24px;margin:0;outline:none;padding:0;width:13px}button.gridjs-sort-neutral{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDEuOTk4IiBoZWlnaHQ9IjQwMS45OTgiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDQwMS45OTggNDAxLjk5OCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTczLjA5MiAxNjQuNDUyaDI1NS44MTNjNC45NDkgMCA5LjIzMy0xLjgwNyAxMi44NDgtNS40MjQgMy42MTMtMy42MTYgNS40MjctNy44OTggNS40MjctMTIuODQ3cy0xLjgxMy05LjIyOS01LjQyNy0xMi44NUwyMTMuODQ2IDUuNDI0QzIxMC4yMzIgMS44MTIgMjA1Ljk1MSAwIDIwMC45OTkgMHMtOS4yMzMgMS44MTItMTIuODUgNS40MjRMNjAuMjQyIDEzMy4zMzFjLTMuNjE3IDMuNjE3LTUuNDI0IDcuOTAxLTUuNDI0IDEyLjg1IDAgNC45NDggMS44MDcgOS4yMzEgNS40MjQgMTIuODQ3IDMuNjIxIDMuNjE3IDcuOTAyIDUuNDI0IDEyLjg1IDUuNDI0ek0zMjguOTA1IDIzNy41NDlINzMuMDkyYy00Ljk1MiAwLTkuMjMzIDEuODA4LTEyLjg1IDUuNDIxLTMuNjE3IDMuNjE3LTUuNDI0IDcuODk4LTUuNDI0IDEyLjg0N3MxLjgwNyA5LjIzMyA1LjQyNCAxMi44NDhMMTg4LjE0OSAzOTYuNTdjMy42MjEgMy42MTcgNy45MDIgNS40MjggMTIuODUgNS40MjhzOS4yMzMtMS44MTEgMTIuODQ3LTUuNDI4bDEyNy45MDctMTI3LjkwNmMzLjYxMy0zLjYxNCA1LjQyNy03Ljg5OCA1LjQyNy0xMi44NDggMC00Ljk0OC0xLjgxMy05LjIyOS01LjQyNy0xMi44NDctMy42MTQtMy42MTYtNy44OTktNS40Mi0xMi44NDgtNS40MnoiLz48L3N2Zz4=");background-position-y:center;opacity:.3}button.gridjs-sort-asc{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIuMzYyIiBoZWlnaHQ9IjI5Mi4zNjEiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDI5Mi4zNjIgMjkyLjM2MSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTI4Ni45MzUgMTk3LjI4NyAxNTkuMDI4IDY5LjM4MWMtMy42MTMtMy42MTctNy44OTUtNS40MjQtMTIuODQ3LTUuNDI0cy05LjIzMyAxLjgwNy0xMi44NSA1LjQyNEw1LjQyNCAxOTcuMjg3QzEuODA3IDIwMC45MDQgMCAyMDUuMTg2IDAgMjEwLjEzNHMxLjgwNyA5LjIzMyA1LjQyNCAxMi44NDdjMy42MjEgMy42MTcgNy45MDIgNS40MjUgMTIuODUgNS40MjVoMjU1LjgxM2M0Ljk0OSAwIDkuMjMzLTEuODA4IDEyLjg0OC01LjQyNSAzLjYxMy0zLjYxMyA1LjQyNy03Ljg5OCA1LjQyNy0xMi44NDdzLTEuODE0LTkuMjMtNS40MjctMTIuODQ3eiIvPjwvc3ZnPg==");background-position-y:35%;background-size:10px}button.gridjs-sort-desc{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOTIuMzYyIiBoZWlnaHQ9IjI5Mi4zNjIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDI5Mi4zNjIgMjkyLjM2MiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTI4Ni45MzUgNjkuMzc3Yy0zLjYxNC0zLjYxNy03Ljg5OC01LjQyNC0xMi44NDgtNS40MjRIMTguMjc0Yy00Ljk1MiAwLTkuMjMzIDEuODA3LTEyLjg1IDUuNDI0QzEuODA3IDcyLjk5OCAwIDc3LjI3OSAwIDgyLjIyOGMwIDQuOTQ4IDEuODA3IDkuMjI5IDUuNDI0IDEyLjg0N2wxMjcuOTA3IDEyNy45MDdjMy42MjEgMy42MTcgNy45MDIgNS40MjggMTIuODUgNS40MjhzOS4yMzMtMS44MTEgMTIuODQ3LTUuNDI4TDI4Ni45MzUgOTUuMDc0YzMuNjEzLTMuNjE3IDUuNDI3LTcuODk4IDUuNDI3LTEyLjg0NyAwLTQuOTQ4LTEuODE0LTkuMjI5LTUuNDI3LTEyLjg1eiIvPjwvc3ZnPg==");background-position-y:65%;background-size:10px}button.gridjs-sort:focus{outline:none}table.gridjs-table{border-collapse:collapse;display:table;margin:0;max-width:100%;overflow:auto;padding:0;table-layout:fixed;text-align:left;width:100%}.gridjs-tbody,td.gridjs-td{background-color:#fff}td.gridjs-td{border:1px solid #e5e7eb;box-sizing:content-box;padding:12px 24px}td.gridjs-td:first-child{border-left:none}td.gridjs-td:last-child{border-right:none}td.gridjs-message{text-align:center}th.gridjs-th{background-color:#f9fafb;border:1px solid #e5e7eb;border-top:none;box-sizing:border-box;color:#6b7280;outline:none;padding:14px 24px;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}th.gridjs-th .gridjs-th-content{float:left;overflow:hidden;text-overflow:ellipsis;width:100%}th.gridjs-th-sort{cursor:pointer}th.gridjs-th-sort .gridjs-th-content{width:calc(100% - 15px)}th.gridjs-th-sort:focus,th.gridjs-th-sort:hover{background-color:#e5e7eb}th.gridjs-th-fixed{box-shadow:0 1px 0 0 #e5e7eb;position:sticky}@supports (-moz-appearance:none){th.gridjs-th-fixed{box-shadow:0 0 0 1px #e5e7eb}}th.gridjs-th:first-child{border-left:none}th.gridjs-th:last-child{border-right:none}.gridjs-tr{border:none}.gridjs-tr-selected td{background-color:#ebf5ff}.gridjs-tr:last-child td{border-bottom:0}.gridjs *,.gridjs :after,.gridjs :before{box-sizing:border-box}.gridjs-wrapper{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;border-color:#e5e7eb;border-radius:8px 8px 0 0;border-top-width:1px;box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.26);display:block;overflow:auto;position:relative;width:100%;z-index:1}.gridjs-wrapper:nth-last-of-type(2){border-bottom-width:1px;border-radius:8px}.gridjs-search{float:left}.gridjs-search-input{width:250px}.gridjs-loading-bar{background-color:#fff;opacity:.5;z-index:10}.gridjs-loading-bar,.gridjs-loading-bar:after{bottom:0;left:0;position:absolute;right:0;top:0}.gridjs-loading-bar:after{animation:shimmer 2s infinite;background-image:linear-gradient(90deg,hsla(0,0%,80%,0),hsla(0,0%,80%,.2) 20%,hsla(0,0%,80%,.5) 60%,hsla(0,0%,80%,0));content:"";transform:translateX(-100%)}@keyframes shimmer{to{transform:translateX(100%)}}.gridjs-td .gridjs-checkbox{cursor:pointer;display:block;margin:auto}.gridjs-resizable{bottom:0;position:absolute;right:0;top:0;width:5px}.gridjs-resizable:hover{background-color:#9bc2f7;cursor:ew-resize} \ No newline at end of file diff --git a/static/vendor/renderjson/NOTICE.md b/static/vendor/renderjson/NOTICE.md new file mode 100644 index 0000000..2fef93e --- /dev/null +++ b/static/vendor/renderjson/NOTICE.md @@ -0,0 +1,4 @@ +Vendored from https://github.com/caldwell/renderjson (v1.4.0). + +No standalone `LICENSE` file exists in the upstream repo; its `package.json` +declares `"license": "ISC"` (MIT-equivalent, permissive). diff --git a/static/vendor/renderjson/renderjson.js b/static/vendor/renderjson/renderjson.js new file mode 100644 index 0000000..9d41d2d --- /dev/null +++ b/static/vendor/renderjson/renderjson.js @@ -0,0 +1,216 @@ +// Copyright © 2013-2017 David Caldwell +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// Usage +// ----- +// The module exports one entry point, the `renderjson()` function. It takes in +// the JSON you want to render as a single argument and returns an HTML +// element. +// +// Options +// ------- +// renderjson.set_icons("+", "-") +// This Allows you to override the disclosure icons. +// +// renderjson.set_show_to_level(level) +// Pass the number of levels to expand when rendering. The default is 0, which +// starts with everything collapsed. As a special case, if level is the string +// "all" then it will start with everything expanded. +// +// renderjson.set_max_string_length(length) +// Strings will be truncated and made expandable if they are longer than +// `length`. As a special case, if `length` is the string "none" then +// there will be no truncation. The default is "none". +// +// renderjson.set_sort_objects(sort_bool) +// Sort objects by key (default: false) +// +// renderjson.set_replacer(replacer_function) +// Equivalent of JSON.stringify() `replacer` argument when it's a function +// +// renderjson.set_collapse_msg(collapse_function) +// Accepts a function (len:number):string => {} where len is the length of the +// object collapsed. Function should return the message displayed when a +// object is collapsed. The default message is "X items" +// +// renderjson.set_property_list(property_list) +// Equivalent of JSON.stringify() `replacer` argument when it's an array +// +// Theming +// ------- +// The HTML output uses a number of classes so that you can theme it the way +// you'd like: +// .disclosure ("⊕", "⊖") +// .syntax (",", ":", "{", "}", "[", "]") +// .string (includes quotes) +// .number +// .boolean +// .key (object key) +// .keyword ("null", "undefined") +// .object.syntax ("{", "}") +// .array.syntax ("[", "]") + +var module, window, define, renderjson=(function() { + var themetext = function(/* [class, text]+ */) { + var spans = []; + while (arguments.length) + spans.push(append(span(Array.prototype.shift.call(arguments)), + text(Array.prototype.shift.call(arguments)))); + return spans; + }; + var append = function(/* el, ... */) { + var el = Array.prototype.shift.call(arguments); + for (var a=0; a 0 && type != "string") + show(); + return el; + }; + + if (json === null) return themetext(null, my_indent, "keyword", "null"); + if (json === void 0) return themetext(null, my_indent, "keyword", "undefined"); + + if (typeof(json) == "string" && json.length > options.max_string_length) + return disclosure('"', json.substr(0,options.max_string_length)+" ...", '"', "string", function () { + return append(span("string"), themetext(null, my_indent, "string", JSON.stringify(json))); + }); + + if (typeof(json) != "object" || [Number, String, Boolean, Date].indexOf(json.constructor) >= 0) // Strings, numbers and bools + return themetext(null, my_indent, typeof(json), JSON.stringify(json)); + + if (json.constructor == Array) { + if (json.length == 0) return themetext(null, my_indent, "array syntax", "[]"); + + return disclosure("[", options.collapse_msg(json.length), "]", "array", function () { + var as = append(span("array"), themetext("array syntax", "[", null, "\n")); + for (var i=0; idef', stored_html) self.assertIn('href="http://testserver/static/themes/pygments.css"', stored_html) + + # -- json kind ------------------------------------------------------------ + + def test_create_json_artifact(self): + response = self.create_artifact(kind="json", content='{"a": 1, "b": [1, 2, 3]}') + self.assertEqual(response.status_code, 201) + + artifact = Artifact.objects.get(pk=response.data["id"]) + stored_html = (self.artifacts_root / artifact.path).read_text(encoding="utf-8") + self.assertIn('renderjson/renderjson.js', stored_html) + self.assertIn('type="application/json"', stored_html) + + # raw sidecar preserves the exact original JSON text + raw_path = (self.artifacts_root / artifact.path).with_suffix(".json") + self.assertEqual(raw_path.read_text(encoding="utf-8"), '{"a": 1, "b": [1, 2, 3]}') + + def test_create_json_artifact_rejects_invalid_json(self): + response = self.create_artifact(kind="json", content="{not valid json") + self.assertEqual(response.status_code, 400) + + def test_json_artifact_escapes_script_close_tag_safely(self): + payload = json.dumps({"value": ""}) + response = self.create_artifact(kind="json", content=payload) + self.assertEqual(response.status_code, 201) + + artifact = Artifact.objects.get(pk=response.data["id"]) + stored_html = (self.artifacts_root / artifact.path).read_text(encoding="utf-8") + + # The page has exactly 3 real "), 3) + + # The escaped content must still round-trip to the exact original value. + start = stored_html.index('id="daybook-data">') + len('id="daybook-data">') + end = stored_html.index("", start) + embedded = stored_html[start:end] + self.assertEqual(json.loads(embedded)["value"], "") + + # -- csv kind --------------------------------------------------------------- + + def test_create_csv_artifact(self): + response = self.create_artifact(kind="csv", content="name,score\nalice,10\nbob,7") + self.assertEqual(response.status_code, 201) + + artifact = Artifact.objects.get(pk=response.data["id"]) + stored_html = (self.artifacts_root / artifact.path).read_text(encoding="utf-8") + self.assertIn("gridjs/gridjs.umd.js", stored_html) + self.assertIn('"columns": ["name", "score"]', stored_html) + self.assertIn('["alice", "10"]', stored_html) + + raw_path = (self.artifacts_root / artifact.path).with_suffix(".csv") + self.assertEqual(raw_path.read_text(encoding="utf-8"), "name,score\nalice,10\nbob,7") + + def test_create_csv_artifact_with_header_only(self): + response = self.create_artifact(kind="csv", content="name,score") + self.assertEqual(response.status_code, 201)