diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..b930fec --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,67 @@ +name: CI +on: + pull_request: + branches: + - master + push: + branches: + - master + tags: '*' +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '1' + - 'nightly' + os: + - ubuntu-latest + arch: + - x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info +# docs: +# name: Documentation +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v2 +# - uses: julia-actions/setup-julia@v1 +# with: +# version: '1' +# - run: | +# julia --project=docs -e ' +# using Pkg +# Pkg.develop(PackageSpec(path=pwd())) +# Pkg.instantiate()' +# - run: | +# julia --project=docs -e ' +# using Documenter: doctest +# using MYPACKAGE +# doctest(MYPACKAGE)' # change MYPACKAGE to the name of your package +# - run: julia --project=docs docs/make.jl +# env: +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +# DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml new file mode 100644 index 0000000..7784f24 --- /dev/null +++ b/.github/workflows/CompatHelper.yml @@ -0,0 +1,26 @@ +name: CompatHelper +on: + schedule: + - cron: 0 0 * * * + workflow_dispatch: +jobs: + CompatHelper: + runs-on: ubuntu-latest + steps: + - name: "Install CompatHelper" + run: | + import Pkg + name = "CompatHelper" + uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" + version = "2" + Pkg.add(; name, uuid, version) + shell: julia --color=yes {0} + - name: "Run CompatHelper" + run: | + import CompatHelper + CompatHelper.main() + shell: julia --color=yes {0} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} + # COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }} diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index d77d3a0..f49313b 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -1,11 +1,15 @@ name: TagBot on: - schedule: - - cron: 0 * * * * + issue_comment: + types: + - created + workflow_dispatch: jobs: TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest steps: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 703e2a4..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: julia -os: - - linux - - osx -julia: - - 1.0 - - 1.1 - - 1.2 - - 1.3 - - 1.4 - - nightly -matrix: - allow_failures: - - julia: nightly -notifications: - email: false diff --git a/Project.toml b/Project.toml index 8e7d87b..ec90109 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "TableView" uuid = "40c74d1a-b44c-5b06-a7c1-6cbea58ea978" -version = "0.6.8" +version = "0.8.0" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" @@ -12,9 +12,9 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" WebIO = "0f1e0344-ec1d-5b48-a673-e5cf874b6c29" [compat] -JSExpr = "0.4, 0.5" +JSExpr = "0.4, 0.5, 1" JSON = "0.18, 0.19, 0.20, 0.21" -Observables = "0.2,0.3" +Observables = "0.2, 0.3, 0.4, 0.5" Tables = "1" WebIO = "0.8" julia = "0.7, 1" diff --git a/README.md b/README.md index fc8f1b7..53980b2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TableView -[![Build Status](https://travis-ci.org/JuliaComputing/TableView.jl.svg?branch=master)](https://travis-ci.org/JuliaComputing/TableView.jl) +[![CI](https://github.com/JuliaComputing/TableView.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/JuliaComputing/TableView.jl/actions/workflows/CI.yml) TableView.jl is an [ag-grid](https://www.ag-grid.com/) based table viewer built on [WebIO.jl](https://github.com/JuliaGizmos/WebIO.jl). It can display arbitrarily large tables by lazy-loading additional data when scrolling (this is the default for datasets with more than 10k rows). diff --git a/ag-grid.version b/ag-grid.version index 2c0a9e3..4f3a069 100644 --- a/ag-grid.version +++ b/ag-grid.version @@ -1 +1 @@ -25.0.0 +30.0.2 diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index eaf4bc4..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,47 +0,0 @@ -environment: - matrix: - - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6-latest-win32.exe" - - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe" - - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe" - - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe" - -## uncomment the following lines to allow failures on nightly julia -## (tests will run but not make your overall status red) -#matrix: -# allow_failures: -# - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe" -# - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe" - -branches: - only: - - master - - /release-.*/ - -notifications: - - provider: Email - on_build_success: false - on_build_failure: false - on_build_status_changed: false - -install: - - ps: "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12" -# If there's a newer build queued for the same PR, cancel this one - - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` - https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` - Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` - throw "There are newer queued builds for this pull request, failing early." } -# Download most recent Julia Windows binary - - ps: (new-object net.webclient).DownloadFile( - $env:JULIA_URL, - "C:\projects\julia-binary.exe") -# Run installer silently, output to C:\projects\julia - - C:\projects\julia-binary.exe /S /D=C:\projects\julia - -build_script: -# Need to convert from shallow to complete for Pkg.clone to work - - IF EXIST .git\shallow (git fetch --unshallow) - - C:\projects\julia\bin\julia -e "versioninfo(); - Pkg.clone(pwd(), \"TableView\"); Pkg.build(\"TableView\")" - -test_script: - - C:\projects\julia\bin\julia -e "Pkg.test(\"TableView\")" diff --git a/deps/build.jl b/deps/build.jl index 3e42890..9478e16 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -8,10 +8,7 @@ ag_grid_base = joinpath(@__DIR__, "ag-grid-$(version)", "ag-grid.js") isfile(ag_grid_base) || download("https://cdn.jsdelivr.net/npm/ag-grid-$(distribution)@$(version)/dist/ag-grid-$(distribution).min.noStyle.js", ag_grid_base) ag_grid_base_style = joinpath(@__DIR__, "ag-grid-$(version)", "ag-grid.css") -isfile(ag_grid_base_style) || download("https://cdn.jsdelivr.net/npm/ag-grid-$(distribution)@$(version)/dist/styles/ag-grid.css", ag_grid_base_style) +isfile(ag_grid_base_style) || download("https://cdn.jsdelivr.net/npm/ag-grid-$(distribution)@$(version)/styles/ag-grid.css", ag_grid_base_style) -ag_grid_light = joinpath(@__DIR__, "ag-grid-$(version)", "ag-grid-light.css") -isfile(ag_grid_light) || download("https://cdn.jsdelivr.net/npm/ag-grid-$(distribution)@$(version)/dist/styles/ag-theme-balham.css", ag_grid_light) - -ag_grid_dark = joinpath(@__DIR__, "ag-grid-$(version)", "ag-grid-dark.css") -isfile(ag_grid_dark) || download("https://cdn.jsdelivr.net/npm/ag-grid-$(distribution)@$(version)/dist/styles/ag-theme-balham-dark.css", ag_grid_dark) +ag_grid_light = joinpath(@__DIR__, "ag-grid-$(version)", "ag-theme-balham.css") +isfile(ag_grid_light) || download("https://cdn.jsdelivr.net/npm/ag-grid-$(distribution)@$(version)/styles/ag-theme-balham.css", ag_grid_light) diff --git a/src/TableView.jl b/src/TableView.jl index aa3fd7b..9c16d4b 100644 --- a/src/TableView.jl +++ b/src/TableView.jl @@ -11,7 +11,7 @@ const ag_grid_imports = [] function __init__() version = readchomp(joinpath(@__DIR__, "..", "ag-grid.version")) empty!(ag_grid_imports) - for f in ["ag-grid.js", "ag-grid.css", "ag-grid-light.css", "ag-grid-dark.css"] + for f in ["ag-grid.js", "ag-grid.css", "ag-theme-balham.css"] push!(ag_grid_imports, normpath(joinpath(@__DIR__, "..", "deps", "ag-grid-$(version)", f))) end pushfirst!(ag_grid_imports, normpath(joinpath(@__DIR__, "rowNumberRenderer.js"))) @@ -47,25 +47,35 @@ end showtable(table::AbstractMatrix; kwargs...) = showtable(Tables.table(table); kwargs...) """ - showtable(table; dark = false, height = :auto, width = "100%", cell_changed = nothing) + showtable(table; options::Dict{Symbol, Any} = Dict{Symbol, Any}(), option_mutator! = identity, + dark = false, height = :auto, width = "100%", cell_changed = nothing) Return a `WebIO.Scope` that displays the provided `table`. Optional arguments: - - `dark`: Switch to a dark theme. - - `title`: Displayed above the table if non-empty; - - `height`/`width`: CSS attributes specifying the output height and with. - - `cell_changed`: Either `nothing` or a function that takes a single argument with the fields - `"new"`, `"old"`, `"row"`, and `"col"`. This function is called whenever the - user edits a table field. Note that all values will be strings, so you need to - do the necessary conversions yourself. + +- `options`: Directly passed to agGrid's `Grid` constructor. Refer to the + [documentation](https://www.ag-grid.com/documentation/) for more info. +- `options_mutator!`: Runs on the `options` dictionary populated by TableView and allows for + customizing the grid (at your own risk -- you can break the package by + supplying invalid options). +- `dark`: Switch to a dark theme. +- `title`: Displayed above the table if non-empty; +- `height`/`width`: CSS attributes specifying the output height and with. +- `cell_changed`: Either `nothing` or a function that takes a single argument with the fields + `"new"`, `"old"`, `"row"`, and `"col"`. This function is called whenever the + user edits a table field. Note that all values will be strings, so you need to + do the necessary conversions yourself. """ -function showtable(table, options::Dict{Symbol, Any} = Dict{Symbol, Any}(); +function showtable(table; + options::Dict{Symbol, Any} = Dict{Symbol, Any}(), + option_mutator! = identity, dark::Bool = false, title::String = "", height = :auto, width = "100%", - cell_changed = nothing + cell_changed = nothing, + async_threshold = 10_000, ) rows = Tables.rows(table) it_sz = Base.IteratorSize(rows) @@ -101,7 +111,7 @@ function showtable(table, options::Dict{Symbol, Any} = Dict{Symbol, Any}(); types = schema.types end - async = tablelength === nothing || tablelength > 10_000 + async = tablelength === nothing || tablelength > async_threshold w = Scope(imports = ag_grid_imports) @@ -190,15 +200,20 @@ function showtable(table, options::Dict{Symbol, Any} = Dict{Symbol, Any}(); ) ) + + showfun = async ? _showtable_async! : _showtable_sync! - showfun(w, schema, names, types, rows, coldefs, tablelength, id, options) + showfun(w, schema, types, rows, tablelength, id, options, option_mutator!) - w + return w end -function _showtable_sync!(w, schema, names, types, rows, coldefs, tablelength, id, options) +function _showtable_sync!(w, schema, types, rows, tablelength, id, options, option_mutator!) options[:rowData] = JSONText(table2json(schema, rows, types)) + + option_mutator!(options) + license = get(ENV, "AG_GRID_LICENSE_KEY", nothing) handler = @js function (RowNumberRenderer, agGrid) @var gridOptions = $options @@ -215,7 +230,7 @@ function _showtable_sync!(w, schema, names, types, rows, coldefs, tablelength, i onimport(w, handler) end -function _showtable_async!(w, schema, names, types, rows, coldefs, tablelength, id, options) +function _showtable_async!(w, schema, types, rows, tablelength, id, options, option_mutator!) rowparams = Observable(w, "rowparams", Dict("startRow" => 1, "endRow" => 100, "successCallback" => @js v -> nothing)) @@ -242,6 +257,8 @@ function _showtable_async!(w, schema, names, types, rows, coldefs, tablelength, ) license = get(ENV, "AG_GRID_LICENSE_KEY", nothing) + option_mutator!(options) + handler = @js function (RowNumberRenderer, agGrid) @var gridOptions = $options @var el = document.getElementById($id) @@ -258,6 +275,9 @@ function _showtable_async!(w, schema, names, types, rows, coldefs, tablelength, onimport(w, handler) end +# By default all objects must use repr or sprint +_is_javascript_safe(x::Real) = false + function _is_javascript_safe(x::Integer) min_safe_int = -(Int64(2)^53-1) max_safe_int = Int64(2)^53-1 diff --git a/test/runtests.jl b/test/runtests.jl index 3948cab..0edf890 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,11 +20,12 @@ end @test showtable(nttable) isa WebIO.Scope end @testset "inf, nan, and missing serializing" begin - rows = Tables.table([NaN Inf -Inf 0 missing]) names = [:a, :b, :c, :d, :e] + rows = Tables.table([NaN Inf -Inf 0 missing]; header=names) types = vcat([Float64 for _ in 1:4], [Missing]) Base.show(io::IO, x::Missing) = print(io, "test_missing") - json = TableView.table2json(Tables.Schema(names, types), rows, types) + schema = Tables.Schema(names, types) + json = TableView.table2json(schema, Tables.rows(rows), types) firstrow = JSON.parse(json)[1] @test firstrow["a"] == "NaN" @test firstrow["b"] == "Inf" @@ -33,20 +34,22 @@ end @test firstrow["e"] == "test_missing" end @testset "large integers" begin - rows = Tables.table([2^52 2^53 2^54]) names = [:a, :b, :c] + rows = Tables.table([2^52 2^53 2^54]; header=names) types = [Int64 for _ in 1:3] - json = TableView.table2json(Tables.Schema(names, types), rows, types) + schema = Tables.Schema(names, types) + json = TableView.table2json(schema, Tables.rows(rows), types) firstrow = JSON.parse(json)[1] @test firstrow["a"] == 4503599627370496 @test firstrow["b"] == "9007199254740992" @test firstrow["c"] == "18014398509481984" end @testset "large floats" begin - rows = Tables.table([1.0e50 1.0e100]) names = [:a, :b] + rows = Tables.table([1.0e50 1.0e100]; header=names) types = [Float64, Float64] - json = TableView.table2json(Tables.Schema(names, types), rows, types) + schema = Tables.Schema(names, types) + json = TableView.table2json(schema, Tables.rows(rows), types) firstrow = JSON.parse(json)[1] @test firstrow["a"] == "1.0e50" @test firstrow["b"] == "1.0e100"