diff --git a/Project.toml b/Project.toml index 86509677..5b81acce 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "StippleUI" uuid = "a3c5d34a-b254-4859-a8fa-b86abb7e84a3" authors = ["Adrian Salceanu "] -version = "0.23.3" +version = "0.23.4" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -22,10 +22,10 @@ StippleUIDataFramesExt = "DataFrames" Colors = "0.12" DataFrames = "1" Dates = "1.6" -Genie = "5.23.8" +Genie = "5.30.4" OrderedCollections = "1" PrecompileTools = "1" -Stipple = "0.28.11" +Stipple = "0.28.14" Tables = "1" julia = "1.6" diff --git a/demos/tables/app.jl b/demos/tables/app.jl index e76d7047..83375928 100644 --- a/demos/tables/app.jl +++ b/demos/tables/app.jl @@ -5,9 +5,9 @@ using DataFrames @genietools @app begin - NO_OF_ROWS = 1000 - DATA = sort!(DataFrame(rand(NO_OF_ROWS, 2), ["x1", "x2"])) - ROWS_PER_PAGE = 10 + NO_OF_ROWS = 1_000_000 + DATA = sort!(DataFrame(rand(NO_OF_ROWS, 2), ["x1", "x2"]))::DataFrame # we only sort so that the changes are more visible when filtering and paginating + ROWS_PER_PAGE = 100 @out df = DATA @out dt = DataTable(DataFrame([]), DataTableOptions(DATA)) diff --git a/demos/tables/app2.jl b/demos/tables/app2.jl index 38b57060..866d97b4 100644 --- a/demos/tables/app2.jl +++ b/demos/tables/app2.jl @@ -4,40 +4,23 @@ using GenieFramework using DataFrames @genietools -@app begin - NO_OF_ROWS = 1000 - DATA = sort!(DataFrame(rand(NO_OF_ROWS, 2), ["x1", "x2"]))::DataFrame # we only sort so that the changes are more visible when filtering and paginating - ROWS_PER_PAGE = 10 +StippleUI.Tables.set_default_rows_per_page(20) +StippleUI.Tables.set_max_rows_client_side(11_000) - @out df = DATA - @out dt = DataTable(DataFrame([]), DataTableOptions(DATA)) - @out pagination = DataTablePagination(rows_per_page = ROWS_PER_PAGE, rows_number = NO_OF_ROWS) - @out loading = false - @in filter = "" +@app begin + big_data = sort!(DataFrame(rand(1_000_000, 2), ["x1", "x2"]))::DataFrame # we only sort so that the changes are more visible when filtering and paginating - @onchange isready begin - dt = DataTable(DATA[1:ROWS_PER_PAGE, :]) - filter = "" - end + @out dt1 = DataTable(big_data; server_side = true) + @out dt2 = DataTable(big_data) - @event request begin - loading = true - state = process_request(DATA, dt, pagination, filter) - dt = state.datatable - pagination = state.pagination - loading = false - end + @out loading = false - @onchange filter begin - loading = true - pagination.page = 1 - state = process_request(DATA, dt, pagination, filter) - dt = state.datatable - pagination = state.pagination - loading = false + @event dt1_request begin + @paginate(dt1, big_data) + @push end end -@page("/", "ui.jl") +@page("/", "ui2.jl") end \ No newline at end of file diff --git a/demos/tables/ui2.jl b/demos/tables/ui2.jl new file mode 100644 index 00000000..fe462863 --- /dev/null +++ b/demos/tables/ui2.jl @@ -0,0 +1,29 @@ +table(:dt1, + server_side = true, + loading = :loading, + title = "Server side data" +) +table(:dt2, + loading = :loading, + title = "Client side data" +) + +# ParsedHTMLString(""" +# +# +# +# """) diff --git a/demos/tables/ui2.jl.html b/demos/tables/ui2.jl.html new file mode 100644 index 00000000..b49a255f --- /dev/null +++ b/demos/tables/ui2.jl.html @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/Tables.jl b/src/Tables.jl index ff75ef70..d8eb370d 100644 --- a/src/Tables.jl +++ b/src/Tables.jl @@ -1,23 +1,34 @@ module Tables +using Base: @kwdef import Tables as TablesInterface using Genie, Stipple, StippleUI, StippleUI.API import Genie.Renderer.Html: HTMLString, normal_element, table, template, register_normal_element export Column, DataTablePagination, DataTableOptions, DataTable, DataTableSelection, DataTableWithSelection, rowselection, selectrows! export cell_template, qtd, qtr +export DataTable!, @paginate register_normal_element("q__table", context = @__MODULE__) const ID = "__id" -const DATAKEY = "data" # has to be changed to `rows` for Quasar 2 +const DATAKEY = "data" # has to be changed to `rows` for Quasar 2 const DataTableSelection = Vector{Dict{String, Any}} +const DEFAULT_ROWS_PER_PAGE = Ref(50) +const DEFAULT_MAX_ROWS_CLIENT_SIDE = Ref(10_000) + +set_default_rows_per_page(n) = (DEFAULT_ROWS_PER_PAGE[] = n) +get_default_rows_per_page() = DEFAULT_ROWS_PER_PAGE[] + +set_max_rows_client_side(n) = (DEFAULT_MAX_ROWS_CLIENT_SIDE[] = n) +get_max_rows_client_side() = DEFAULT_MAX_ROWS_CLIENT_SIDE[] + struct2dict(s::T) where T = Dict{Symbol, Any}(zip(fieldnames(T), getfield.(Ref(s), fieldnames(T)))) #===# -Base.@kwdef mutable struct Column +@kwdef mutable struct Column name::String required::Bool = false label::String = name @@ -71,11 +82,11 @@ end julia> DataTablePagination(rows_per_page=50) ``` """ -Base.@kwdef mutable struct DataTablePagination +@kwdef mutable struct DataTablePagination sort_by::Symbol = :desc descending::Bool = false page::Int = 1 - rows_per_page::Int = 10 + rows_per_page::Int = DEFAULT_ROWS_PER_PAGE[] rows_number::Union{Int,Nothing} = nothing _filter::AbstractString = "" # keep track of filter value for improving performance end @@ -103,7 +114,7 @@ julia> dt.opts.columnspecs[r"^(a|b)\$"] = opts(format = jsfunction(raw"(val, row julia> model.table[] = dt ``` """ -Base.@kwdef mutable struct DataTableOptions +@kwdef mutable struct DataTableOptions addid::Bool = false idcolumn::String = "ID" columns::Union{Vector{Column},Nothing} = nothing @@ -111,11 +122,6 @@ Base.@kwdef mutable struct DataTableOptions end -mutable struct DataTable{T} - data::T - opts::DataTableOptions -end - """ DataTable(data::T, opts::DataTableOptions) @@ -140,12 +146,82 @@ julia> Tables.table([1 2 3; 3 4 5], header = ["a", "b", "c"]) julia> dt = DataTable(t1) ``` """ -function DataTable(data::T) where {T} - DataTable{T}(data, DataTableOptions()) +mutable struct DataTable{T} + data::T + opts::DataTableOptions + pagination::DataTablePagination + filter::AbstractString end -function DataTable{T}() where {T} - DataTable{T}(T(), DataTableOptions()) +function DataTable{T}(data::T, opts::DataTableOptions, pagination::DataTablePagination) where {T} + DataTable{T}(data, opts, pagination, "") +end +function DataTable(data::T, opts::DataTableOptions, pagination::DataTablePagination) where {T} + DataTable{T}(data, opts, pagination) +end + +function DataTable{T}(data::T, opts::DataTableOptions) where {T} + DataTable{T}(data, opts, DataTablePagination(), "") +end +function DataTable(data::T, opts::DataTableOptions) where {T} + DataTable{T}(data, opts) +end + +function DataTable(data::T; kwargs...) where {T} + DataTable{T}(data; kwargs...) +end +function DataTable{T}( + data::T + + ; + filter::AbstractString = "", + + # DataTableOptions + addid::Bool = false, + idcolumn::String = "ID", + columns::Union{Vector{Column},Nothing} = nothing, + columnspecs::Dict = Dict(), + + # DataTablePagination + sort_by::Symbol = :desc, + descending::Bool = false, + page::Int = 1, + rows_per_page::Int = DEFAULT_ROWS_PER_PAGE[], + rows_number::Union{Int,Nothing} = nothing, + + # options + server_side::Bool = false +) where {T} + if (isnothing(rows_number) && ! server_side) && size(data, 1) > DEFAULT_MAX_ROWS_CLIENT_SIDE[] + @warn """ + The number of rows exceeds the maximum number of rows that can be displayed client side. + Loading too many rows can have a negative effects on performance, both in terms of loading time and responsiveness. + Automatically truncating your data to $(DEFAULT_MAX_ROWS_CLIENT_SIDE[]) rows. + + If you want to display more rows client side, + call `StippleUI.Tables.set_max_rows_client_side(n)` with `n` the number of rows you want to display. + Current maximum number of client side rows is: $(DEFAULT_MAX_ROWS_CLIENT_SIDE[]) + """ + try + data = data[1:DEFAULT_MAX_ROWS_CLIENT_SIDE[], :] + catch ex + @error "Failed to truncate data to $(DEFAULT_MAX_ROWS_CLIENT_SIDE[]) rows. Warning, this can have negative effects on performance." + @error ex + end + end + + try + isnothing(rows_number) && server_side && (rows_number = length(TablesInterface.rows(data))) + catch ex + @error ex + rows_number = nothing + end + + DataTable{T}( data, + DataTableOptions(addid, idcolumn, columns, columnspecs), + DataTablePagination(sort_by, descending, page, rows_per_page, rows_number, filter), + filter + ) end #===# @@ -211,9 +287,17 @@ function rows(t::T)::Vector{OrderedDict{String,Any}} where {T<:DataTable} end function data(t::T; datakey = "data", columnskey = "columns")::Dict{String,Any} where {T<:DataTable} + total_rows = size(t.data)[1] + max_rows = total_rows > t.pagination.rows_per_page && t.pagination.rows_number !== nothing ? + t.pagination.rows_per_page : + total_rows + 1 + data = total_rows >= max_rows ? t[1:max_rows, :] : t + OrderedDict( - columnskey => columns(t), - datakey => rows(t) + columnskey => columns(t), + datakey => data |> rows, + "pagination" => render(t.pagination), + "filter" => t.filter ) end @@ -326,7 +410,7 @@ function cell_template(; columns isa Vector || columns === nothing || (columns = [columns]) if edit isa Bool # `columns` decides on which columns should be editable - # + # columns === nothing && (columns = [""]) if edit edit_columns = columns @@ -353,7 +437,7 @@ function cell_template(; ) ]) push!(cell_templates, t) - end + end isempty(edit_columns) && return cell_templates @@ -361,7 +445,7 @@ function cell_template(; if change_class == "text-red " change_class = change_style === nothing ? "text-red" : nothing end - + # in contrast to `props.value` `props.row[props.col.name]` can be written to value = "props.row[props.col.name]" # ref_rows are calculated from ref_table, if not defined explicitly @@ -386,11 +470,11 @@ function cell_template(; if inner_class !== nothing && isempty(inner_class) inner_class = nothing end - + # add standard settings from stipplecore.css table_style = Dict("font-weight" => 400, "font-size" => "0.9rem", "padding-top" => 0, "padding-bottom" => 0) inner_style = inner_style === nothing ? table_style : [table_style, inner_style] - + # add custom style for changed entries if ref_rows !== nothing && change_style !== nothing change_style_js = JSON3.write(render(change_style)) @@ -479,7 +563,7 @@ function table( fieldname::Symbol, columnskey::String = "$fieldname.columns", filter::Union{Symbol,String,Nothing} = nothing, paginationsync::Union{Symbol,String,Nothing} = nothing, - + columns::Union{Nothing,Bool,Integer,AbstractString,Vector{<:AbstractString},Vector{<:Integer}} = nothing, cell_class::Union{Nothing,AbstractString,AbstractDict,Vector} = nothing, cell_style::Union{Nothing,AbstractString,AbstractDict,Vector} = nothing, @@ -492,9 +576,18 @@ function table( fieldname::Symbol, change_style::Union{Nothing,AbstractString,AbstractDict,Vector} = nothing, change_inner_class::Union{Nothing,AbstractString,AbstractDict,Vector} = nothing, change_inner_style::Union{Nothing,AbstractString,AbstractDict,Vector} = nothing, - + + filter_placeholder::Union{Symbol,String,Nothing} = "Search", + + server_side::Bool = false, # if true, the table data, pagination and filtering is rendered server side + server_side_event::Union{Symbol,String,Nothing} = nothing, # event name for server side handling + kwargs...) :: ParsedHTMLString + server_side && server_side_event === nothing && (server_side_event = "$(fieldname)_request") + paginationsync === nothing && (paginationsync = "$fieldname.pagination") + filter === nothing && (filter = Symbol("$fieldname.filter")) + if !isa(edit, Bool) || edit || cell_class !== nothing || cell_style !== nothing cell_kwargs, kwargs = filter_kwargs(kwargs) do p startswith(String(p[1]), "cell_") ? Symbol(String(p[1])[6:end]) => p[2] : nothing @@ -503,7 +596,7 @@ function table( fieldname::Symbol, startswith(String(p[1]), "inner_") ? p : nothing end - table_template = cell_template(; ref_table, ref_rows, rowkey, + table_template = cell_template(; ref_table, ref_rows, rowkey, edit, columns, class = cell_class, style = cell_style, type = cell_type, inner_class, inner_style, change_class, change_style, change_inner_class, change_inner_style, cell_kwargs..., inner_kwargs... ) @@ -511,10 +604,10 @@ function table( fieldname::Symbol, end - if filter !== nothing && paginationsync !== nothing # by convention, assume paginationsync is used only for server side filtering + if filter !== nothing && paginationsync !== nothing filter_input = [ParsedHTMLString("""