├── .codeclimate.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── ruby.yml │ └── stale.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── README.md ├── Rakefile ├── docs └── development.md ├── lib └── oxidized │ ├── web.rb │ └── web │ ├── public │ ├── css │ │ └── oxidized.css │ ├── images │ │ ├── favicon.ico │ │ ├── oxidizing.png │ │ └── oxidizing_40px.png │ ├── scripts │ │ └── oxidized.js │ └── weblibs │ │ ├── bootstrap-icons.css │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── buttons.bootstrap5.css │ │ ├── buttons.bootstrap5.js │ │ ├── buttons.colVis.js │ │ ├── dataTables.bootstrap5.css │ │ ├── dataTables.bootstrap5.js │ │ ├── dataTables.buttons.js │ │ ├── dataTables.js │ │ ├── fonts │ │ ├── bootstrap-icons.woff │ │ └── bootstrap-icons.woff2 │ │ └── jquery.js │ ├── version.rb │ ├── views │ ├── conf_search.haml │ ├── diffs.haml │ ├── footer.haml │ ├── head.haml │ ├── layout.haml │ ├── node.haml │ ├── nodes.haml │ ├── stats.haml │ ├── version.haml │ └── versions.haml │ └── webapp.rb ├── oxidized-web.gemspec ├── package-lock.json ├── package.json └── spec ├── node_spec.rb ├── node_version_spec.rb ├── nodes_spec.rb ├── root_spec.rb ├── spec_helper.rb └── webapp_spec.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | engines: 4 | duplication: 5 | enabled: true 6 | config: 7 | languages: 8 | - ruby 9 | - javascript 10 | fixme: 11 | enabled: true 12 | eslint: 13 | enabled: true 14 | rubocop: 15 | enabled: true 16 | scss-lint: 17 | enabled: true 18 | 19 | ratings: 20 | paths: 21 | - "**.rb" 22 | - "**.js" 23 | - "**.css" 24 | - "**.sass" 25 | 26 | exclude_paths: 27 | - 'lib/oxidized/web/public/css/*' 28 | - 'lib/oxidized/web/public/scripts/bootstrap.min.js' 29 | - 'lib/oxidized/web/public/scripts/dataTables*' 30 | - 'lib/oxidized/web/public/scripts/jquery*' 31 | 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Pre-Request Checklist 2 | 3 | 4 | - [ ] Passes rubocop code analysis (try `rubocop --auto-correct`) 5 | - [ ] Tests added or adapted (try `rake test`) 6 | - [ ] Changes are reflected in the documentation 7 | - [ ] User-visible changes appended to [CHANGELOG.md](/CHANGELOG.md) 8 | 9 | ## Description 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | target-branch: "master" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | open-pull-requests-limit: 10 14 | - package-ecosystem: "bundler" 15 | target-branch: "master" 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | open-pull-requests-limit: 10 20 | - package-ecosystem: "npm" 21 | target-branch: "master" 22 | directory: "/" 23 | schedule: 24 | interval: "daily" 25 | open-pull-requests-limit: 10 26 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '44 21 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'ruby' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v3 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v3 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v3 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: CI 9 | 10 | on: [ push, pull_request ] 11 | # push: 12 | # branches: [ master ] 13 | # pull_request: 14 | # branches: [ master ] 15 | 16 | jobs: 17 | test: 18 | 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | ruby-version: ['3.1', '3.2', '3.3', '3.4', 'ruby-head'] 23 | continue-on-error: ${{ matrix.ruby-version == 'ruby-head' }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Set up Ruby 28 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 29 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 30 | uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: ${{ matrix.ruby-version }} 33 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 34 | - name: Run tests 35 | run: bundle exec rake 36 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Stale Issue/PR cleanup" 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/stale@v9 15 | with: 16 | stale-issue-message: 'This issue is stale because it has been open 90 days with no activity.' 17 | stale-pr-message: 'This PR is stale because it has been open 90 days with no activity.' 18 | operations-per-run: 500 19 | days-before-stale: 90 20 | days-before-close: -1 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | .idea/ 13 | 14 | # Used by dotenv library to load environment variables. 15 | # .env 16 | 17 | ## Specific to RubyMotion: 18 | .dat* 19 | .repl_history 20 | build/ 21 | *.bridgesupport 22 | build-iPhoneOS/ 23 | build-iPhoneSimulator/ 24 | 25 | ## Specific to RubyMotion (use of CocoaPods): 26 | # 27 | # We recommend against adding the Pods directory to your .gitignore. However 28 | # you should judge for yourself, the pros and cons are mentioned at: 29 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 30 | # 31 | # vendor/Pods/ 32 | 33 | ## Documentation cache and generated files: 34 | /.yardoc/ 35 | /_yardoc/ 36 | /doc/ 37 | /rdoc/ 38 | 39 | ## Environment normalization: 40 | /.bundle/ 41 | /vendor/bundle 42 | /lib/bundler/man/ 43 | 44 | # for a library or gem, you might want to ignore these files since the code is 45 | # intended to run in multiple environments; otherwise, check them in: 46 | Gemfile.lock 47 | .ruby-version 48 | .ruby-gemset 49 | 50 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 51 | .rvmrc 52 | 53 | # npm files 54 | /node_modules/ 55 | /.rake_tasks~ 56 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | plugins: 3 | - rubocop-rails 4 | - rubocop-rake 5 | - rubocop-minitest 6 | 7 | # Do not attempt to police vendored code, and exclude special cases 8 | AllCops: 9 | NewCops: enable 10 | TargetRubyVersion: 3.1 11 | Exclude: 12 | - 'vendor/**/*' 13 | 14 | Gemspec/DevelopmentDependencies: 15 | EnforcedStyle: gemspec 16 | 17 | Metrics/ClassLength: 18 | Max: 300 19 | 20 | Metrics/BlockLength: 21 | Max: 150 22 | 23 | Style/Documentation: 24 | Enabled: false 25 | 26 | Style/StringLiterals: 27 | Enabled: false 28 | 29 | Style/FrozenStringLiteralComment: 30 | Enabled: false 31 | 32 | Layout/LineLength: 33 | Enabled: false 34 | 35 | Lint/RaiseException: 36 | Enabled: true 37 | 38 | Lint/StructNewOverride: 39 | Enabled: true 40 | 41 | # Stick to verbose until https://bugs.ruby-lang.org/issues/10177 is closed. 42 | Style/PreferredHashMethods: 43 | EnforcedStyle: verbose 44 | 45 | Style/Not: 46 | Enabled: false 47 | 48 | # comply with @ytti's exacting specifications 49 | Style/CommandLiteral: 50 | EnforcedStyle: percent_x 51 | 52 | Style/FormatString: 53 | EnforcedStyle: percent 54 | 55 | Style/FormatStringToken: 56 | EnforcedStyle: unannotated 57 | 58 | Style/HashEachMethods: 59 | Enabled: true 60 | 61 | Style/HashTransformKeys: 62 | Enabled: true 63 | 64 | Style/HashTransformValues: 65 | Enabled: true 66 | 67 | Style/HashSyntax: 68 | EnforcedShorthandSyntax: either 69 | 70 | Style/RescueModifier: 71 | Enabled: false 72 | 73 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2025-03-26 07:50:20 UTC using RuboCop version 1.74.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 3 10 | # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. 11 | Metrics/AbcSize: 12 | Max: 31 13 | 14 | # Offense count: 2 15 | # Configuration parameters: AllowedMethods, AllowedPatterns. 16 | Metrics/CyclomaticComplexity: 17 | Max: 9 18 | 19 | # Offense count: 5 20 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. 21 | Metrics/MethodLength: 22 | Max: 28 23 | 24 | # Offense count: 2 25 | # Configuration parameters: AllowedMethods, AllowedPatterns. 26 | Metrics/PerceivedComplexity: 27 | Max: 11 28 | 29 | # Offense count: 1 30 | # Configuration parameters: Include. 31 | # Include: app/**/*.rb, config/**/*.rb, lib/**/*.rb 32 | Rails/Exit: 33 | Exclude: 34 | - 'lib/oxidized/web.rb' 35 | 36 | # Offense count: 7 37 | # This cop supports unsafe autocorrection (--autocorrect-all). 38 | # Configuration parameters: Include. 39 | # Include: **/Rakefile, **/*.rake 40 | Rails/RakeEnvironment: 41 | Exclude: 42 | - 'Rakefile' 43 | 44 | # Offense count: 1 45 | # This cop supports safe autocorrection (--autocorrect). 46 | Rake/Desc: 47 | Exclude: 48 | - 'Rakefile' 49 | 50 | # Offense count: 2 51 | Rake/DuplicateTask: 52 | Exclude: 53 | - 'Rakefile' 54 | 55 | # Offense count: 1 56 | Security/Eval: 57 | Exclude: 58 | - 'Rakefile' 59 | 60 | # Offense count: 1 61 | # This cop supports unsafe autocorrection (--autocorrect-all). 62 | Security/JSONLoad: 63 | Exclude: 64 | - 'lib/oxidized/web/webapp.rb' 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | cache: bundler 4 | before_install: 5 | - gem install bundler 6 | rvm: 7 | - 2.3 8 | - 2.4 9 | - 2.5 10 | - 2.6 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 6 | ## [0.16.0 - 2025-03-25] 7 | This release introduces the possibility for an extended configuration of 8 | oxidized-web in the oxidized configuration file. Oxidized versions after 0.32.2 9 | will only work with oxidized-web version 0.16.0 or later. 10 | 11 | ### Changed 12 | - Allow connection to any virtual hosts + prepare extended configuration to specify which vhosts to accept. Fixes #298 (@robertcheramy) 13 | 14 | ### Fixed 15 | - the table preferences (pagelength, column visibility, search) are stored in the local browser cache. Fixes #315 #314 #265 #211 (@robertcheramy) 16 | - Update "refresh" and "Upldate node list" to more meaningfull texts (@robertcheramy) 17 | 18 | ## [0.15.1 – 2025-02-20] 19 | This patch release fixes javascript errors. 20 | 21 | ### Fixed 22 | - Fix javascript not working (@robertcheramy) 23 | 24 | ## [0.15.0 – 2025-02-17] 25 | This release fixes a security issue on the RANCID migration page. 26 | A non-authenticated user could gain control over the Linux user running 27 | oxidized-web. The RANCID migration page was already deprecated in version 28 | 0.14.0, so it has been completely removed in this new version. 29 | Thank you to Jon O'Reilly and Jamie Riden from NetSPI for discovering and 30 | reporting this security issue! 31 | 32 | ### Changed 33 | - Update datatables.net to 2.2.2 and datatables.net-buttons to 3.2.2 (@robertcheramy) 34 | - remove the RANCID migration page (@robertchreamy) 35 | - dependency on oxidized 0.31 (@robertchreamy) 36 | - Update datatables.net to 2.2.1 and datatables.net-buttons to 3.2.1 (@robertcheramy) 37 | 38 | ### Fixed 39 | - #302: group name containing a '/' produced a Sinatra error (@robertcheramy) 40 | 41 | 42 | ## [0.14.0 - 2024-06-28] 43 | 44 | ### Added 45 | - CHANGELOG.md created (@robertchreamy) 46 | - First minitest: get / (@robertchreamy) 47 | - docs/development.md created (@robertchreamy) 48 | - weblibs are maintained with npm (@robertchreamy) 49 | - display Oxidized-web version in the footer (@robertcheramy) 50 | 51 | ### Changed 52 | - gem dependencies updated (@robertchreamy) 53 | - support for ruby 3.0 dropped (@robertchreamy) 54 | - #215: weblibs (jQuery, bootstrap, datatables.net) updated (@robertchreamy) 55 | - the web design follows where possible bootstrap without specific css (@robertchreamy) 56 | - deprecating the RANCID migration page (@robertchreamy) 57 | 58 | ### Fixed 59 | - #232: escape_once not supported in haml 6.0 (@robertchreamy) 60 | - #253: deprecated sass dependency dropped (@robertchreamy) 61 | - rubocop warnings fixed (@robertchreamy) 62 | - #234: removed link to not working live demo (@robertchreamy) 63 | - group name containing a '/' producing a Sinatra error (@robertcheramy) 64 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oxidized Web 2 | 3 | [![Build Status](https://github.com/ytti/oxidized-web/actions/workflows/ruby.yml/badge.svg)](https://github.com/ytti/oxidized-web/actions/workflows/ruby.yml) 4 | [![Gem Version](https://badge.fury.io/rb/oxidized-web.svg)](http://badge.fury.io/rb/oxidized-web) 5 | 6 | Web userinterface and RESTful API for Oxidized. 7 | 8 | This is not useful independently, see https://github.com/ytti/oxidized for install instructions. 9 | 10 | If you wonder how to run oxidized-web from git for development, have a look at 11 | [docs/development.md](docs/development.md). 12 | 13 | ## License and Copyright 14 | 15 | Copyright 2013-2015 Saku Ytti 16 | 2013-2015 Samer Abdel-Hafez 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | You may obtain a copy of the License at 21 | 22 | http://www.apache.org/licenses/LICENSE-2.0 23 | 24 | Unless required by applicable law or agreed to in writing, software 25 | distributed under the License is distributed on an "AS IS" BASIS, 26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | See the License for the specific language governing permissions and 28 | limitations under the License. 29 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | gemspec = eval(File.read(Dir['*.gemspec'].first)) 5 | gemfile = "#{[gemspec.name, gemspec.version].join('-')}.gem" 6 | 7 | # Integrate Rubocop if available 8 | begin 9 | require 'rubocop/rake_task' 10 | 11 | RuboCop::RakeTask.new 12 | task(:default).prerequisites << task(:rubocop) 13 | rescue LoadError 14 | task :rubocop do 15 | puts 'Install rubocop to run its rake tasks' 16 | end 17 | end 18 | 19 | desc 'Validate gemspec' 20 | task :gemspec do 21 | gemspec.validate 22 | end 23 | 24 | desc 'Run minitest' 25 | task :test do 26 | Rake::TestTask.new do |t| 27 | t.libs << 'spec' 28 | t.test_files = FileList['spec/**/*_spec.rb'] 29 | t.ruby_opts = ['-W:deprecated'] 30 | t.warning = true 31 | t.verbose = true 32 | end 33 | end 34 | 35 | task build: :chmod 36 | ## desc 'Install gem' 37 | ## task :install => :build do 38 | ## system "sudo -Es sh -c \'umask 022; gem install gems/#{file}\'" 39 | ## end 40 | 41 | desc 'Remove gems' 42 | task :clean do 43 | FileUtils.rm_rf 'pkg' 44 | end 45 | 46 | desc 'Tag the release' 47 | task :tag do 48 | system "git tag #{gemspec.version} -m 'Release #{gemspec.version}'" 49 | end 50 | 51 | desc 'Push to rubygems' 52 | task push: :tag do 53 | system "gem push pkg/#{gemfile}" 54 | end 55 | 56 | desc 'Normalise file permissions' 57 | task :chmod do 58 | xbit = %w[] 59 | dirs = [] 60 | %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }.each do |file| 61 | dirs.push(File.dirname(file)) 62 | xbit.include?(file) ? File.chmod(0o0755, file) : File.chmod(0o0644, file) 63 | end 64 | dirs.sort.uniq.each { |dir| File.chmod(0o0755, dir) } 65 | end 66 | 67 | desc 'Copy web packages from npm into public' 68 | task :weblibs do 69 | weblibs = [] 70 | fonts = [] 71 | 72 | # jQuery 73 | weblibs << 'node_modules/jquery/dist/jquery.js' 74 | 75 | # Bootstrap 76 | weblibs << 'node_modules/bootstrap/dist/js/bootstrap.js' 77 | weblibs << 'node_modules/bootstrap/dist/js/bootstrap.js.map' 78 | weblibs << 'node_modules/bootstrap/dist/css/bootstrap.css' 79 | weblibs << 'node_modules/bootstrap/dist/css/bootstrap.css.map' 80 | weblibs << 'node_modules/bootstrap/dist/js/bootstrap.bundle.js' 81 | weblibs << 'node_modules/bootstrap/dist/js/bootstrap.bundle.js.map' 82 | 83 | # Bootstrap-icons 84 | weblibs << 'node_modules/bootstrap-icons/font/bootstrap-icons.css' 85 | fonts << 'node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff' 86 | fonts << 'node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2' 87 | 88 | # Datatables 89 | weblibs << 'node_modules/datatables.net/js/dataTables.js' 90 | 91 | # Datatables + Bootstrap 92 | weblibs << 'node_modules/datatables.net-bs5/js/dataTables.bootstrap5.js' 93 | weblibs << 'node_modules/datatables.net-bs5/css/dataTables.bootstrap5.css' 94 | 95 | # Datatables Buttons + Bootstrap 96 | weblibs << 'node_modules/datatables.net-buttons-bs5/js/buttons.bootstrap5.js' 97 | weblibs << 'node_modules/datatables.net-buttons-bs5/css/buttons.bootstrap5.css' 98 | # colVis 99 | weblibs << 'node_modules/datatables.net-buttons/js/dataTables.buttons.js' 100 | weblibs << 'node_modules/datatables.net-buttons/js/buttons.colVis.js' 101 | 102 | weblibs.each do |w| 103 | cp(w, 'lib/oxidized/web/public/weblibs') 104 | end 105 | 106 | fonts.each do |f| 107 | cp(f, 'lib/oxidized/web/public/weblibs/fonts') 108 | end 109 | end 110 | 111 | task default: :test 112 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Index 2 | 1. [Run from git](#how-to-run--develop-oxidized-web-from-git) 3 | 2. [Update weblibs](#update-the-weblibs) 4 | 3. [Release](#how-to-release-a-new-version-of-oxidized-web) 5 | 6 | # How to run / develop oxidized-web from git 7 | ## Using the latest oxidized package 8 | When you develop oxidized-web, it is quite simple to run it from git. As it depends on oxidized, 9 | oxidized will be included in `bundle install`, and you just have to run `bundle exec oxidized`. 10 | You need bundler, if not installed yet. On debian-based systems, you can run `sudo apt install ruby-bundler` to install it. 11 | 12 | All steps in one to copy & pase: 13 | ```shell 14 | git clone git@github.com:ytti/oxidized-web.git 15 | cd oxidized-web 16 | bundle config set --local path 'vendor/bundle' 17 | bundle install 18 | bundle exec oxidized 19 | ``` 20 | 21 | Changes to haml templates are reloaded on the fly. For changes to the ruby 22 | scripts, you have to stop an restart `bundle exec oxidized`. 23 | 24 | ## Paralell development between oxidized and oxidized-web 25 | You may need to make some changes in oxidized **and** oxidized-web. For this, 26 | git clone oxidized and oxidized-web in a common root directory. 27 | 28 | Then you can link your oxidized-web with the oxidized, add the direct 29 | dependency to ../oxidized-web in oxidized and run oxidized from the oxidized 30 | repo: 31 | 32 | ```shell 33 | git clone git@github.com:ytti/oxidized-web.git 34 | git clone git@github.com:ytti/oxidized.git 35 | cd oxidized 36 | bundle config set --local path 'vendor/bundle' 37 | echo "gem 'oxidized-web', path: '../oxidized-web'" >> Gemfile 38 | bundle install 39 | bundle exec bin/oxidized 40 | ``` 41 | 42 | Be careful when commiting your work in oxidized *NOT* to include the changes to 43 | Gemfile, as this is a local change for development. I (@robertcheramy) didn't 44 | find a better way to do this, better ideas are welcome :-) 45 | 46 | If your changes to oxidized **AND** oxidzed-web are dependent from another, make 47 | sure you document this in the respectives CHANGELOG.md, so that everyone is 48 | informed at the next release. 49 | 50 | Note: you can also add the dependency to oxidized in oxidized-web, in the same 51 | fashion: 52 | ```shell 53 | git clone git@github.com:ytti/oxidized-web.git 54 | git clone git@github.com:ytti/oxidized.git 55 | cd oxidized-web 56 | bundle config set --local path 'vendor/bundle' 57 | echo "gem 'oxidized', path: '../oxidized'" >> Gemfile 58 | bundle install 59 | bundle exec oxidized 60 | ``` 61 | 62 | 63 | # Update the weblibs 64 | The weblibs are beeing downloaded and maintained by `npm`. 65 | 66 | Run `npm install` to download the weblibs. They will be stored under 67 | `node_modules`. 68 | The file `package-lock.json` (wich is tracked in git) ensures that every 69 | developer gets the same versions. 70 | 71 | Run `npm outdated` to get a list of new releases: 72 | 73 | ```shell 74 | oxidized-web$ npm outdated 75 | Package Current Wanted Latest Location Depended by 76 | datatables.net-bs5 2.0.7 2.0.8 2.0.8 node_modules/datatables.net-bs5 oxidized-web 77 | ``` 78 | 79 | Run `npm update` to get the latest releases. They still are not used 80 | oxidzed-web. Run `rake weblibs` to sync `node_modules` with 81 | `lib/oxidized/web/public/weblibs`. 82 | 83 | Test, and commit the changes to the weblibs **and** package-lock.json. Don't 84 | forget to document your changes in CHANGELOG.md. 85 | 86 | # How to release a new version of Oxidized-web? 87 | 88 | ## Review changes 89 | Run `git diff 0.xx.yy..master` (where `0.xx.yy` is to be changed to the last 90 | release) and review all the changes that have been done. Have a specific look 91 | at changes you don't understand. 92 | 93 | It is nicer to read in a GUI, so you can use something like 94 | `git difftool --tool=kdiff3 -d 0.xx.yy..master` to see it in kdiff3. 95 | 96 | ## Update the gem dependencies to the latest versions 97 | ``` 98 | bundle outaded 99 | bundle update 100 | bundle outaded 101 | ``` 102 | 103 | Retest after updating! 104 | 105 | ## Update the weblibs to the latest versions 106 | ``` 107 | npm outdated 108 | ``` 109 | 110 | Retest after updating! 111 | 112 | ## Update rubocup .rubocop_todo.yml 113 | Run `bundle exec rubocop --auto-gen-config`, 114 | and make sure `bundle exec rake` passes after it. 115 | 116 | If you change some code => Restart the release process at the beginning ;-) 117 | 118 | ## Make sure the file permissions are correct 119 | Run `bundle exec rake chmod` 120 | 121 | ## Test, test test! 122 | Run `bundle exec rake` on the git repository to check the code against rubocop and rund the 123 | defined tests in `/spec`. 124 | 125 | Run Oxidized-web from git against the latest Oxidized version `bundle exec oxdized` 126 | 127 | Run Oxidized-web from git against the latest git of Oxidized (see above). 128 | 129 | Do not integrate late PRs into master if they do not fix issues for the release. The must wait for the next release. 130 | 131 | When testing the web application, open the javascript console in the browser to 132 | see any errors. 133 | 134 | ## Version numbering 135 | Oxidized-web versions are nummered like major.minor.patch 136 | - currently, the major version is 0. 137 | - minor is incremented when releasing new features. 138 | - patch is incremented when releasing fixes only. 139 | 140 | ## Release 141 | 1. Checkout the master branch of oxidized-web. Make sure you are up to date with origin. 142 | 2. Change the version in lib/oxidized/web/version.rb 143 | 3. Change CHANGELOG.md to replace [Unreleased] with [0.xx.yy – 202Y-MM-DD] 144 | 4. Run `git diff` to check your changes 145 | 5. Commit the changes to the local git repository with a commit message “chore(release): release version 0.xx.yy” 146 | 6. Tag the commit with `git tag -a 0.xx.yy -m "Release 0.xx.yy"` 147 | 7. Build the gem with ‘rake build’ 148 | 8. Install an test the gem locally: 149 | ``` 150 | gem install --user-install pkg/oxidized-web-0.xx.yy.gem 151 | ~/.local/share/gem/ruby/3.1.0/bin/oxidized 152 | ``` 153 | ## Release in github 154 | 1. Push the commit and the tag to github: 155 | ``` 156 | git push 157 | git push origin 0.xx.yy 158 | ``` 159 | 160 | 2. Make a release from the tag in github 161 | - Thank the contributors 162 | - Only describe major changes, and refer to CHANGELOG.md 163 | - List new contributors (generated automatically) 164 | 165 | ## Release in rubygems 166 | Push the gem with ‘rake push’ 167 | 168 | You need an account at rubygems which is allowed to push oxidized 169 | 170 | ## Update CHANGELOG.md for next release 171 | Add 172 | ``` 173 | ## [Unreleased] 174 | 175 | ### Added 176 | 177 | ### Changed 178 | 179 | ### Fixed 180 | 181 | ``` 182 | And push to github 183 | 184 | ## Congratulate yourself 185 | Great job! Treat yourself to your favorite drink, you deserve it! 186 | 187 | -------------------------------------------------------------------------------- /lib/oxidized/web.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module Oxidized 4 | module API 5 | class Web 6 | require 'rack/handler/puma' 7 | attr_reader :thread 8 | 9 | def initialize(nodes, configuration) 10 | require 'oxidized/web/webapp' 11 | if configuration.instance_of? Asetus::ConfigStruct 12 | # New configuration syle: extensions.oxidized-web 13 | addr = configuration.listen? || '127.0.0.1' 14 | port = configuration.port? || 8888 15 | uri = configuration.url_prefix? || '' 16 | vhosts = configuration.vhosts? || [] 17 | else 18 | # Old configuration stlyle: "rest: 127.0.0.1:8888/prefix" 19 | listen, uri = configuration.split '/' 20 | addr, _, port = listen.rpartition ':' 21 | unless port 22 | port = addr 23 | addr = nil 24 | end 25 | vhosts = [] 26 | end 27 | uri = "/#{uri}" 28 | @opts = { 29 | Host: addr, 30 | Port: port 31 | } 32 | WebApp.set :nodes, nodes 33 | WebApp.set :host_authorization, { permitted_hosts: vhosts } 34 | @app = Rack::Builder.new do 35 | map uri do 36 | run WebApp 37 | end 38 | end 39 | end 40 | 41 | def run 42 | @thread = Thread.new do 43 | Rack::Handler::Puma.run @app, **@opts 44 | exit! 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/oxidized/web/public/css/oxidized.css: -------------------------------------------------------------------------------- 1 | /* Display status of device in /nodes and /stats */ 2 | .status, .success, .no_connection, .never { 3 | float: right; 4 | width: 15px; 5 | height: 15px; 6 | border: 1px solid; 7 | margin-right: 70%; 8 | margin-bottom: 0px; 9 | border-radius: 2px; } 10 | 11 | .success { 12 | background-color: #64FE2E; } 13 | 14 | .no_connection { 15 | background-color: #FF0000; } 16 | 17 | .never { 18 | background-color: #58ACFA; } 19 | 20 | /* node/version/diffs */ 21 | .added { 22 | background-color: #DBFFDB; } 23 | 24 | .deleted { 25 | background-color: #FFDDDD; } 26 | 27 | 28 | .diff-index { 29 | background-color: #FFFF99; } 30 | 31 | .diff-empty { 32 | background-color: #F0F0F0; } 33 | 34 | .diffs, .diffs_old, .diffs_new { 35 | background-color: #FAFAFA; 36 | border: 2px solid #ccc; 37 | border-radius: 5px; 38 | padding: 3px; 39 | font-family: Consolas, "Lucida Console", monospace; 40 | white-space: pre; } 41 | 42 | .diffs_old { 43 | float: left; 44 | width: 49%; 45 | overflow: auto; } 46 | 47 | .diffs_new { 48 | margin-left: 51%; 49 | width: 49%; 50 | overflow: auto; } 51 | 52 | .old_version_title { 53 | float: left; 54 | width: 49%; 55 | font-weight: bold; } 56 | 57 | .new_version_title { 58 | margin-left: 51%; 59 | font-weight: bold; } 60 | -------------------------------------------------------------------------------- /lib/oxidized/web/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytti/oxidized-web/ec46b4493528908ea54e35431cd71c4d063c971d/lib/oxidized/web/public/images/favicon.ico -------------------------------------------------------------------------------- /lib/oxidized/web/public/images/oxidizing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytti/oxidized-web/ec46b4493528908ea54e35431cd71c4d063c971d/lib/oxidized/web/public/images/oxidizing.png -------------------------------------------------------------------------------- /lib/oxidized/web/public/images/oxidizing_40px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytti/oxidized-web/ec46b4493528908ea54e35431cd71c4d063c971d/lib/oxidized/web/public/images/oxidizing_40px.png -------------------------------------------------------------------------------- /lib/oxidized/web/public/scripts/oxidized.js: -------------------------------------------------------------------------------- 1 | var convertTime = function() { 2 | /* Convert UTC times to local browser times 3 | * Requires that the times on the server are UTC 4 | * Requires a class name of `time` to be set on element desired to be changed 5 | * Requires that element have a text in the format of `YYYY-mm-dd HH:MM:SS` 6 | * See ytti/oxidized-web #16 7 | */ 8 | $('.time').each(function() { 9 | var content = $(this).text(); 10 | if(content === 'never' || content === 'unknown' || content === '') { return; } 11 | var utcTime = content.split(' '); 12 | var date = new Date(utcTime[0] + 'T' + utcTime[1] + 'Z'); 13 | var year = date.getFullYear(); 14 | var month = ("0"+(date.getMonth()+1)).slice(-2); 15 | var day = ("0" + date.getDate()).slice(-2); 16 | var hour = ("0" + date.getHours()).slice(-2); 17 | var minute = ("0" + date.getMinutes()).slice(-2); 18 | var second = ("0" + date.getSeconds()).slice(-2); 19 | var timeZone = date.toString().match(/[A-Z]{3,4}[+-][0-9]{4}/)[0]; 20 | $(this).text(year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second + ' ' + timeZone); 21 | }); 22 | }; 23 | 24 | $(function() { 25 | convertTime(); 26 | 27 | // Reloads the nodes from a source by calling the /reload.json URI 28 | $('#reload').click(function() { 29 | $.get(window.location.pathname.replace(/nodes.*/g, '')+'reload.json') 30 | .done(function(data) { 31 | $('#flashMessage') 32 | .removeClass('alert-danger') 33 | .addClass('alert-success') 34 | .text(data); 35 | }) 36 | .fail(function() { 37 | var data = 'Unable to reload nodes' 38 | $('#flashMessage') 39 | .removeClass('alert-success') 40 | .addClass('alert-danger') 41 | .text(data); 42 | }) 43 | .always(function() { 44 | $('#flashMessage').removeClass('hidden'); 45 | }); 46 | }); 47 | 48 | // Update timestamp on next button click for DataTables 49 | $('.paginate_button').on('click', function() { 50 | convertTime(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/oxidized/web/public/weblibs/buttons.bootstrap5.css: -------------------------------------------------------------------------------- 1 | @keyframes dtb-spinner { 2 | 100% { 3 | transform: rotate(360deg); 4 | } 5 | } 6 | @-o-keyframes dtb-spinner { 7 | 100% { 8 | -o-transform: rotate(360deg); 9 | transform: rotate(360deg); 10 | } 11 | } 12 | @-ms-keyframes dtb-spinner { 13 | 100% { 14 | -ms-transform: rotate(360deg); 15 | transform: rotate(360deg); 16 | } 17 | } 18 | @-webkit-keyframes dtb-spinner { 19 | 100% { 20 | -webkit-transform: rotate(360deg); 21 | transform: rotate(360deg); 22 | } 23 | } 24 | @-moz-keyframes dtb-spinner { 25 | 100% { 26 | -moz-transform: rotate(360deg); 27 | transform: rotate(360deg); 28 | } 29 | } 30 | div.dataTables_wrapper { 31 | position: relative; 32 | } 33 | 34 | div.dt-buttons { 35 | position: initial; 36 | } 37 | div.dt-buttons .dt-button { 38 | overflow: hidden; 39 | text-overflow: ellipsis; 40 | white-space: nowrap; 41 | } 42 | 43 | div.dt-button-info { 44 | position: fixed; 45 | top: 50%; 46 | left: 50%; 47 | width: 400px; 48 | margin-top: -100px; 49 | margin-left: -200px; 50 | background-color: white; 51 | border-radius: 0.75em; 52 | box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.8); 53 | text-align: center; 54 | z-index: 2003; 55 | overflow: hidden; 56 | } 57 | div.dt-button-info h2 { 58 | padding: 2rem 2rem 1rem 2rem; 59 | margin: 0; 60 | font-weight: normal; 61 | } 62 | div.dt-button-info > div { 63 | padding: 1em 2em 2em 2em; 64 | } 65 | 66 | div.dtb-popover-close { 67 | position: absolute; 68 | top: 6px; 69 | right: 6px; 70 | width: 22px; 71 | height: 22px; 72 | text-align: center; 73 | border-radius: 3px; 74 | cursor: pointer; 75 | z-index: 2003; 76 | } 77 | 78 | button.dtb-hide-drop { 79 | display: none !important; 80 | } 81 | 82 | div.dt-button-collection-title { 83 | text-align: center; 84 | padding: 0.3em 0.5em 0.5em; 85 | margin-left: 0.5em; 86 | margin-right: 0.5em; 87 | font-size: 0.9em; 88 | white-space: nowrap; 89 | overflow: hidden; 90 | text-overflow: ellipsis; 91 | } 92 | 93 | div.dt-button-collection-title:empty { 94 | display: none; 95 | } 96 | 97 | span.dt-button-spacer { 98 | display: inline-block; 99 | margin: 0.5em; 100 | white-space: nowrap; 101 | } 102 | span.dt-button-spacer.bar { 103 | border-left: 1px solid rgba(0, 0, 0, 0.3); 104 | vertical-align: middle; 105 | padding-left: 0.5em; 106 | } 107 | span.dt-button-spacer.bar:empty { 108 | height: 1em; 109 | width: 1px; 110 | padding-left: 0; 111 | } 112 | 113 | div.dt-button-collection .dt-button-active { 114 | padding-right: 3em; 115 | } 116 | div.dt-button-collection .dt-button-active:after { 117 | position: absolute; 118 | top: 50%; 119 | margin-top: -10px; 120 | right: 1em; 121 | display: inline-block; 122 | content: "✓"; 123 | color: inherit; 124 | } 125 | div.dt-button-collection .dt-button-active.dt-button-split { 126 | padding-right: 0; 127 | } 128 | div.dt-button-collection .dt-button-active.dt-button-split:after { 129 | display: none; 130 | } 131 | div.dt-button-collection .dt-button-active.dt-button-split > *:first-child { 132 | padding-right: 3em; 133 | } 134 | div.dt-button-collection .dt-button-active.dt-button-split > *:first-child:after { 135 | position: absolute; 136 | top: 50%; 137 | margin-top: -10px; 138 | right: 1em; 139 | display: inline-block; 140 | content: "✓"; 141 | color: inherit; 142 | } 143 | div.dt-button-collection .dt-button-active-a a { 144 | padding-right: 3em; 145 | } 146 | div.dt-button-collection .dt-button-active-a a:after { 147 | position: absolute; 148 | right: 1em; 149 | display: inline-block; 150 | content: "✓"; 151 | color: inherit; 152 | } 153 | div.dt-button-collection span.dt-button-spacer { 154 | width: 100%; 155 | font-size: 0.9em; 156 | text-align: center; 157 | margin: 0.5em 0; 158 | } 159 | div.dt-button-collection span.dt-button-spacer:empty { 160 | height: 0; 161 | width: 100%; 162 | } 163 | div.dt-button-collection span.dt-button-spacer.bar { 164 | border-left: none; 165 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 166 | padding-left: 0; 167 | } 168 | 169 | @media print { 170 | table.dataTable tr > * { 171 | box-shadow: none !important; 172 | } 173 | } 174 | div.dt-buttons div.btn-group { 175 | position: initial; 176 | } 177 | div.dt-buttons span.dt-button-spacer.empty { 178 | margin: 1px; 179 | } 180 | div.dt-buttons span.dt-button-spacer.bar:empty { 181 | height: inherit; 182 | } 183 | div.dt-buttons .btn.processing { 184 | color: rgba(0, 0, 0, 0.2); 185 | } 186 | div.dt-buttons .btn.processing:after { 187 | position: absolute; 188 | top: 50%; 189 | left: 50%; 190 | width: 16px; 191 | height: 16px; 192 | margin: -8px 0 0 -8px; 193 | box-sizing: border-box; 194 | display: block; 195 | content: " "; 196 | border: 2px solid rgb(40, 40, 40); 197 | border-radius: 50%; 198 | border-left-color: transparent; 199 | border-right-color: transparent; 200 | animation: dtb-spinner 1500ms infinite linear; 201 | -o-animation: dtb-spinner 1500ms infinite linear; 202 | -ms-animation: dtb-spinner 1500ms infinite linear; 203 | -webkit-animation: dtb-spinner 1500ms infinite linear; 204 | -moz-animation: dtb-spinner 1500ms infinite linear; 205 | } 206 | 207 | div.dropdown-menu.dt-button-collection { 208 | margin-top: 4px; 209 | width: 200px; 210 | } 211 | div.dropdown-menu.dt-button-collection .dt-button { 212 | position: relative; 213 | } 214 | div.dropdown-menu.dt-button-collection .dt-button.dropdown-toggle::after { 215 | position: absolute; 216 | right: 12px; 217 | top: 14px; 218 | } 219 | div.dropdown-menu.dt-button-collection div.dt-button-split { 220 | display: flex; 221 | flex-direction: row; 222 | flex-wrap: wrap; 223 | justify-content: flex-start; 224 | align-content: flex-start; 225 | align-items: stretch; 226 | } 227 | div.dropdown-menu.dt-button-collection div.dt-button-split a:first-child { 228 | min-width: auto; 229 | flex: 1 0 50px; 230 | padding-right: 0; 231 | } 232 | div.dropdown-menu.dt-button-collection div.dt-button-split button:last-child { 233 | min-width: 33px; 234 | flex: 0; 235 | background: transparent; 236 | border: none; 237 | line-height: 1rem; 238 | color: var(--bs-dropdown-link-color); 239 | padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); 240 | overflow: visible; 241 | } 242 | div.dropdown-menu.dt-button-collection div.dt-button-split button:last-child:hover { 243 | color: var(--bs-dropdown-link-hover-color); 244 | background-color: var(--bs-dropdown-link-hover-bg); 245 | } 246 | div.dropdown-menu.dt-button-collection.fixed { 247 | position: fixed; 248 | display: block; 249 | top: 50%; 250 | left: 50%; 251 | margin-left: -75px; 252 | border-radius: 5px; 253 | background-color: white; 254 | padding: 0.5em; 255 | } 256 | div.dropdown-menu.dt-button-collection.fixed.two-column { 257 | margin-left: -200px; 258 | } 259 | div.dropdown-menu.dt-button-collection.fixed.three-column { 260 | margin-left: -225px; 261 | } 262 | div.dropdown-menu.dt-button-collection.fixed.four-column { 263 | margin-left: -300px; 264 | } 265 | div.dropdown-menu.dt-button-collection.fixed.columns { 266 | margin-left: -409px; 267 | } 268 | @media screen and (max-width: 1024px) { 269 | div.dropdown-menu.dt-button-collection.fixed.columns { 270 | margin-left: -308px; 271 | } 272 | } 273 | @media screen and (max-width: 640px) { 274 | div.dropdown-menu.dt-button-collection.fixed.columns { 275 | margin-left: -203px; 276 | } 277 | } 278 | @media screen and (max-width: 460px) { 279 | div.dropdown-menu.dt-button-collection.fixed.columns { 280 | margin-left: -100px; 281 | } 282 | } 283 | div.dropdown-menu.dt-button-collection.fixed > :last-child { 284 | max-height: 100vh; 285 | overflow: auto; 286 | } 287 | div.dropdown-menu.dt-button-collection.two-column > :last-child, div.dropdown-menu.dt-button-collection.three-column > :last-child, div.dropdown-menu.dt-button-collection.four-column > :last-child { 288 | display: block !important; 289 | column-gap: 8px; 290 | } 291 | div.dropdown-menu.dt-button-collection.two-column > :last-child > *, div.dropdown-menu.dt-button-collection.three-column > :last-child > *, div.dropdown-menu.dt-button-collection.four-column > :last-child > * { 292 | -webkit-column-break-inside: avoid; 293 | break-inside: avoid; 294 | } 295 | div.dropdown-menu.dt-button-collection.two-column { 296 | width: 400px; 297 | } 298 | div.dropdown-menu.dt-button-collection.two-column > :last-child { 299 | padding-bottom: 1px; 300 | column-count: 2; 301 | } 302 | div.dropdown-menu.dt-button-collection.three-column { 303 | width: 450px; 304 | } 305 | div.dropdown-menu.dt-button-collection.three-column > :last-child { 306 | padding-bottom: 1px; 307 | column-count: 3; 308 | } 309 | div.dropdown-menu.dt-button-collection.four-column { 310 | width: 600px; 311 | } 312 | div.dropdown-menu.dt-button-collection.four-column > :last-child { 313 | padding-bottom: 1px; 314 | column-count: 4; 315 | } 316 | div.dropdown-menu.dt-button-collection .dt-button { 317 | border-radius: 0; 318 | } 319 | div.dropdown-menu.dt-button-collection.columns { 320 | width: auto; 321 | } 322 | div.dropdown-menu.dt-button-collection.columns > :last-child { 323 | display: flex; 324 | flex-wrap: wrap; 325 | justify-content: flex-start; 326 | align-items: center; 327 | gap: 6px; 328 | width: 818px; 329 | padding-bottom: 1px; 330 | } 331 | div.dropdown-menu.dt-button-collection.columns > :last-child .dt-button { 332 | min-width: 200px; 333 | flex: 0 1; 334 | margin: 0; 335 | } 336 | div.dropdown-menu.dt-button-collection.columns.dtb-b3 > :last-child, div.dropdown-menu.dt-button-collection.columns.dtb-b2 > :last-child, div.dropdown-menu.dt-button-collection.columns.dtb-b1 > :last-child { 337 | justify-content: space-between; 338 | } 339 | div.dropdown-menu.dt-button-collection.columns.dtb-b3 .dt-button { 340 | flex: 1 1 32%; 341 | } 342 | div.dropdown-menu.dt-button-collection.columns.dtb-b2 .dt-button { 343 | flex: 1 1 48%; 344 | } 345 | div.dropdown-menu.dt-button-collection.columns.dtb-b1 .dt-button { 346 | flex: 1 1 100%; 347 | } 348 | @media screen and (max-width: 1024px) { 349 | div.dropdown-menu.dt-button-collection.columns > :last-child { 350 | width: 612px; 351 | } 352 | } 353 | @media screen and (max-width: 640px) { 354 | div.dropdown-menu.dt-button-collection.columns > :last-child { 355 | width: 406px; 356 | } 357 | div.dropdown-menu.dt-button-collection.columns.dtb-b3 .dt-button { 358 | flex: 0 1 32%; 359 | } 360 | } 361 | @media screen and (max-width: 460px) { 362 | div.dropdown-menu.dt-button-collection.columns > :last-child { 363 | width: 200px; 364 | } 365 | } 366 | 367 | div.dt-button-background { 368 | position: fixed; 369 | top: 0; 370 | left: 0; 371 | width: 100%; 372 | height: 100%; 373 | z-index: 999; 374 | } 375 | 376 | @media screen and (max-width: 767px) { 377 | div.dt-buttons { 378 | float: none; 379 | width: 100%; 380 | text-align: center; 381 | margin-bottom: 0.5em; 382 | } 383 | div.dt-buttons a.btn { 384 | float: none; 385 | } 386 | } 387 | div.dt-button-info { 388 | background-color: var(--bs-body-bg); 389 | border: 1px solid var(--bs-border-color-translucent); 390 | } 391 | 392 | :root[data-bs-theme=dark] div.dropdown-menu.dt-button-collection.fixed { 393 | background-color: var(--bs-body-bg); 394 | border: 1px solid var(--bs-border-color-translucent); 395 | } 396 | -------------------------------------------------------------------------------- /lib/oxidized/web/public/weblibs/buttons.bootstrap5.js: -------------------------------------------------------------------------------- 1 | /*! Bootstrap integration for DataTables' Buttons 2 | * © SpryMedia Ltd - datatables.net/license 3 | */ 4 | 5 | (function( factory ){ 6 | if ( typeof define === 'function' && define.amd ) { 7 | // AMD 8 | define( ['jquery', 'datatables.net-bs5', 'datatables.net-buttons'], function ( $ ) { 9 | return factory( $, window, document ); 10 | } ); 11 | } 12 | else if ( typeof exports === 'object' ) { 13 | // CommonJS 14 | var jq = require('jquery'); 15 | var cjsRequires = function (root, $) { 16 | if ( ! $.fn.dataTable ) { 17 | require('datatables.net-bs5')(root, $); 18 | } 19 | 20 | if ( ! $.fn.dataTable.Buttons ) { 21 | require('datatables.net-buttons')(root, $); 22 | } 23 | }; 24 | 25 | if (typeof window === 'undefined') { 26 | module.exports = function (root, $) { 27 | if ( ! root ) { 28 | // CommonJS environments without a window global must pass a 29 | // root. This will give an error otherwise 30 | root = window; 31 | } 32 | 33 | if ( ! $ ) { 34 | $ = jq( root ); 35 | } 36 | 37 | cjsRequires( root, $ ); 38 | return factory( $, root, root.document ); 39 | }; 40 | } 41 | else { 42 | cjsRequires( window, jq ); 43 | module.exports = factory( jq, window, window.document ); 44 | } 45 | } 46 | else { 47 | // Browser 48 | factory( jQuery, window, document ); 49 | } 50 | }(function( $, window, document ) { 51 | 'use strict'; 52 | var DataTable = $.fn.dataTable; 53 | 54 | 55 | 56 | $.extend(true, DataTable.Buttons.defaults, { 57 | dom: { 58 | container: { 59 | className: 'dt-buttons btn-group flex-wrap' 60 | }, 61 | button: { 62 | className: 'btn btn-secondary', 63 | active: 'active', 64 | dropHtml: '', 65 | dropClass: 'dropdown-toggle' 66 | }, 67 | collection: { 68 | container: { 69 | tag: 'div', 70 | className: 'dropdown-menu dt-button-collection' 71 | }, 72 | closeButton: false, 73 | button: { 74 | tag: 'a', 75 | className: 'dt-button dropdown-item', 76 | active: 'dt-button-active', 77 | disabled: 'disabled', 78 | spacer: { 79 | className: 'dropdown-divider', 80 | tag: 'hr' 81 | } 82 | } 83 | }, 84 | split: { 85 | action: { 86 | tag: 'a', 87 | className: 'btn btn-secondary dt-button-split-drop-button', 88 | closeButton: false 89 | }, 90 | dropdown: { 91 | tag: 'button', 92 | className: 93 | 'btn btn-secondary dt-button-split-drop dropdown-toggle-split', 94 | closeButton: false, 95 | align: 'split-left', 96 | splitAlignClass: 'dt-button-split-left' 97 | }, 98 | wrapper: { 99 | tag: 'div', 100 | className: 'dt-button-split btn-group', 101 | closeButton: false 102 | } 103 | } 104 | }, 105 | buttonCreated: function (config, button) { 106 | return config.buttons ? $('
').append(button) : button; 107 | } 108 | }); 109 | 110 | DataTable.ext.buttons.collection.rightAlignClassName = 'dropdown-menu-right'; 111 | 112 | 113 | return DataTable; 114 | })); 115 | -------------------------------------------------------------------------------- /lib/oxidized/web/public/weblibs/buttons.colVis.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Column visibility buttons for Buttons and DataTables. 3 | * © SpryMedia Ltd - datatables.net/license 4 | */ 5 | 6 | (function( factory ){ 7 | if ( typeof define === 'function' && define.amd ) { 8 | // AMD 9 | define( ['jquery', 'datatables.net', 'datatables.net-buttons'], function ( $ ) { 10 | return factory( $, window, document ); 11 | } ); 12 | } 13 | else if ( typeof exports === 'object' ) { 14 | // CommonJS 15 | var jq = require('jquery'); 16 | var cjsRequires = function (root, $) { 17 | if ( ! $.fn.dataTable ) { 18 | require('datatables.net')(root, $); 19 | } 20 | 21 | if ( ! $.fn.dataTable.Buttons ) { 22 | require('datatables.net-buttons')(root, $); 23 | } 24 | }; 25 | 26 | if (typeof window === 'undefined') { 27 | module.exports = function (root, $) { 28 | if ( ! root ) { 29 | // CommonJS environments without a window global must pass a 30 | // root. This will give an error otherwise 31 | root = window; 32 | } 33 | 34 | if ( ! $ ) { 35 | $ = jq( root ); 36 | } 37 | 38 | cjsRequires( root, $ ); 39 | return factory( $, root, root.document ); 40 | }; 41 | } 42 | else { 43 | cjsRequires( window, jq ); 44 | module.exports = factory( jq, window, window.document ); 45 | } 46 | } 47 | else { 48 | // Browser 49 | factory( jQuery, window, document ); 50 | } 51 | }(function( $, window, document ) { 52 | 'use strict'; 53 | var DataTable = $.fn.dataTable; 54 | 55 | 56 | 57 | $.extend(DataTable.ext.buttons, { 58 | // A collection of column visibility buttons 59 | colvis: function (dt, conf) { 60 | var node = null; 61 | var buttonConf = { 62 | extend: 'collection', 63 | init: function (dt, n) { 64 | node = n; 65 | }, 66 | text: function (dt) { 67 | return dt.i18n('buttons.colvis', 'Column visibility'); 68 | }, 69 | className: 'buttons-colvis', 70 | closeButton: false, 71 | buttons: [ 72 | { 73 | extend: 'columnsToggle', 74 | columns: conf.columns, 75 | columnText: conf.columnText 76 | } 77 | ] 78 | }; 79 | 80 | // Rebuild the collection with the new column structure if columns are reordered 81 | dt.on('column-reorder.dt' + conf.namespace, function () { 82 | dt.button(null, dt.button(null, node).node()).collectionRebuild([ 83 | { 84 | extend: 'columnsToggle', 85 | columns: conf.columns, 86 | columnText: conf.columnText 87 | } 88 | ]); 89 | }); 90 | 91 | return buttonConf; 92 | }, 93 | 94 | // Selected columns with individual buttons - toggle column visibility 95 | columnsToggle: function (dt, conf) { 96 | var columns = dt 97 | .columns(conf.columns) 98 | .indexes() 99 | .map(function (idx) { 100 | return { 101 | extend: 'columnToggle', 102 | columns: idx, 103 | columnText: conf.columnText 104 | }; 105 | }) 106 | .toArray(); 107 | 108 | return columns; 109 | }, 110 | 111 | // Single button to toggle column visibility 112 | columnToggle: function (dt, conf) { 113 | return { 114 | extend: 'columnVisibility', 115 | columns: conf.columns, 116 | columnText: conf.columnText 117 | }; 118 | }, 119 | 120 | // Selected columns with individual buttons - set column visibility 121 | columnsVisibility: function (dt, conf) { 122 | var columns = dt 123 | .columns(conf.columns) 124 | .indexes() 125 | .map(function (idx) { 126 | return { 127 | extend: 'columnVisibility', 128 | columns: idx, 129 | visibility: conf.visibility, 130 | columnText: conf.columnText 131 | }; 132 | }) 133 | .toArray(); 134 | 135 | return columns; 136 | }, 137 | 138 | // Single button to set column visibility 139 | columnVisibility: { 140 | columns: undefined, // column selector 141 | text: function (dt, button, conf) { 142 | return conf._columnText(dt, conf); 143 | }, 144 | className: 'buttons-columnVisibility', 145 | action: function (e, dt, button, conf) { 146 | var col = dt.columns(conf.columns); 147 | var curr = col.visible(); 148 | 149 | col.visible( 150 | conf.visibility !== undefined ? conf.visibility : !(curr.length ? curr[0] : false) 151 | ); 152 | }, 153 | init: function (dt, button, conf) { 154 | var that = this; 155 | button.attr('data-cv-idx', conf.columns); 156 | 157 | dt.on('column-visibility.dt' + conf.namespace, function (e, settings) { 158 | if (!settings.bDestroying && settings.nTable == dt.settings()[0].nTable) { 159 | that.active(dt.column(conf.columns).visible()); 160 | } 161 | }).on('column-reorder.dt' + conf.namespace, function () { 162 | // Button has been removed from the DOM 163 | if (conf.destroying) { 164 | return; 165 | } 166 | 167 | if (dt.columns(conf.columns).count() !== 1) { 168 | return; 169 | } 170 | 171 | // This button controls the same column index but the text for the column has 172 | // changed 173 | that.text(conf._columnText(dt, conf)); 174 | 175 | // Since its a different column, we need to check its visibility 176 | that.active(dt.column(conf.columns).visible()); 177 | }); 178 | 179 | this.active(dt.column(conf.columns).visible()); 180 | }, 181 | destroy: function (dt, button, conf) { 182 | dt.off('column-visibility.dt' + conf.namespace).off( 183 | 'column-reorder.dt' + conf.namespace 184 | ); 185 | }, 186 | 187 | _columnText: function (dt, conf) { 188 | if (typeof conf.text === 'string') { 189 | return conf.text; 190 | } 191 | 192 | var title = dt.column(conf.columns).title(); 193 | var idx = dt.column(conf.columns).index(); 194 | 195 | title = title 196 | .replace(/\n/g, ' ') // remove new lines 197 | .replace(//gi, ' ') // replace line breaks with spaces 198 | .replace(//gi, ''); // remove select tags, including options text 199 | 200 | // Strip HTML comments 201 | title = DataTable.Buttons.stripHtmlComments(title); 202 | 203 | // Use whatever HTML stripper DataTables is configured for 204 | title = DataTable.util.stripHtml(title).trim(); 205 | 206 | return conf.columnText ? conf.columnText(dt, idx, title) : title; 207 | } 208 | }, 209 | 210 | colvisRestore: { 211 | className: 'buttons-colvisRestore', 212 | 213 | text: function (dt) { 214 | return dt.i18n('buttons.colvisRestore', 'Restore visibility'); 215 | }, 216 | 217 | init: function (dt, button, conf) { 218 | // Use a private parameter on the column. This gets moved around with the 219 | // column if ColReorder changes the order 220 | dt.columns().every(function () { 221 | var init = this.init(); 222 | 223 | if (init.__visOriginal === undefined) { 224 | init.__visOriginal = this.visible(); 225 | } 226 | }); 227 | }, 228 | 229 | action: function (e, dt, button, conf) { 230 | dt.columns().every(function (i) { 231 | var init = this.init(); 232 | 233 | this.visible(init.__visOriginal); 234 | }); 235 | } 236 | }, 237 | 238 | colvisGroup: { 239 | className: 'buttons-colvisGroup', 240 | 241 | action: function (e, dt, button, conf) { 242 | dt.columns(conf.show).visible(true, false); 243 | dt.columns(conf.hide).visible(false, false); 244 | 245 | dt.columns.adjust(); 246 | }, 247 | 248 | show: [], 249 | 250 | hide: [] 251 | } 252 | }); 253 | 254 | 255 | return DataTable; 256 | })); 257 | -------------------------------------------------------------------------------- /lib/oxidized/web/public/weblibs/dataTables.bootstrap5.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dt-row-selected: 13, 110, 253; 3 | --dt-row-selected-text: 255, 255, 255; 4 | --dt-row-selected-link: 9, 10, 11; 5 | --dt-row-stripe: 0, 0, 0; 6 | --dt-row-hover: 0, 0, 0; 7 | --dt-column-ordering: 0, 0, 0; 8 | --dt-html-background: white; 9 | } 10 | :root.dark { 11 | --dt-html-background: rgb(33, 37, 41); 12 | } 13 | 14 | table.dataTable td.dt-control { 15 | text-align: center; 16 | cursor: pointer; 17 | } 18 | table.dataTable td.dt-control:before { 19 | display: inline-block; 20 | box-sizing: border-box; 21 | content: ""; 22 | border-top: 5px solid transparent; 23 | border-left: 10px solid rgba(0, 0, 0, 0.5); 24 | border-bottom: 5px solid transparent; 25 | border-right: 0px solid transparent; 26 | } 27 | table.dataTable tr.dt-hasChild td.dt-control:before { 28 | border-top: 10px solid rgba(0, 0, 0, 0.5); 29 | border-left: 5px solid transparent; 30 | border-bottom: 0px solid transparent; 31 | border-right: 5px solid transparent; 32 | } 33 | table.dataTable tfoot:empty { 34 | display: none; 35 | } 36 | 37 | html.dark table.dataTable td.dt-control:before, 38 | :root[data-bs-theme=dark] table.dataTable td.dt-control:before, 39 | :root[data-theme=dark] table.dataTable td.dt-control:before { 40 | border-left-color: rgba(255, 255, 255, 0.5); 41 | } 42 | html.dark table.dataTable tr.dt-hasChild td.dt-control:before, 43 | :root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before, 44 | :root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before { 45 | border-top-color: rgba(255, 255, 255, 0.5); 46 | border-left-color: transparent; 47 | } 48 | 49 | div.dt-scroll { 50 | width: 100%; 51 | } 52 | 53 | div.dt-scroll-body thead tr, 54 | div.dt-scroll-body tfoot tr { 55 | height: 0; 56 | } 57 | div.dt-scroll-body thead tr th, div.dt-scroll-body thead tr td, 58 | div.dt-scroll-body tfoot tr th, 59 | div.dt-scroll-body tfoot tr td { 60 | height: 0 !important; 61 | padding-top: 0px !important; 62 | padding-bottom: 0px !important; 63 | border-top-width: 0px !important; 64 | border-bottom-width: 0px !important; 65 | } 66 | div.dt-scroll-body thead tr th div.dt-scroll-sizing, div.dt-scroll-body thead tr td div.dt-scroll-sizing, 67 | div.dt-scroll-body tfoot tr th div.dt-scroll-sizing, 68 | div.dt-scroll-body tfoot tr td div.dt-scroll-sizing { 69 | height: 0 !important; 70 | overflow: hidden !important; 71 | } 72 | 73 | table.dataTable thead > tr > th:active, 74 | table.dataTable thead > tr > td:active { 75 | outline: none; 76 | } 77 | table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, 78 | table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before, 79 | table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before { 80 | position: absolute; 81 | display: block; 82 | bottom: 50%; 83 | content: "\25B2"; 84 | content: "\25B2"/""; 85 | } 86 | table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, 87 | table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after, 88 | table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { 89 | position: absolute; 90 | display: block; 91 | top: 50%; 92 | content: "\25BC"; 93 | content: "\25BC"/""; 94 | } 95 | table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc, table.dataTable thead > tr > th.dt-ordering-asc, table.dataTable thead > tr > th.dt-ordering-desc, 96 | table.dataTable thead > tr > td.dt-orderable-asc, 97 | table.dataTable thead > tr > td.dt-orderable-desc, 98 | table.dataTable thead > tr > td.dt-ordering-asc, 99 | table.dataTable thead > tr > td.dt-ordering-desc { 100 | position: relative; 101 | padding-right: 30px; 102 | } 103 | table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order, 104 | table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order, 105 | table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order, 106 | table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order, 107 | table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order { 108 | position: absolute; 109 | right: 12px; 110 | top: 0; 111 | bottom: 0; 112 | width: 12px; 113 | } 114 | table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, 115 | table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before, 116 | table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:after, 117 | table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:before, 118 | table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after, 119 | table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before, 120 | table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:after, 121 | table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:before, 122 | table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { 123 | left: 0; 124 | opacity: 0.125; 125 | line-height: 9px; 126 | font-size: 0.8em; 127 | } 128 | table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc, 129 | table.dataTable thead > tr > td.dt-orderable-asc, 130 | table.dataTable thead > tr > td.dt-orderable-desc { 131 | cursor: pointer; 132 | } 133 | table.dataTable thead > tr > th.dt-orderable-asc:hover, table.dataTable thead > tr > th.dt-orderable-desc:hover, 134 | table.dataTable thead > tr > td.dt-orderable-asc:hover, 135 | table.dataTable thead > tr > td.dt-orderable-desc:hover { 136 | outline: 2px solid rgba(0, 0, 0, 0.05); 137 | outline-offset: -2px; 138 | } 139 | table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, 140 | table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before, 141 | table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { 142 | opacity: 0.6; 143 | } 144 | table.dataTable thead > tr > th.sorting_desc_disabled span.dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled span.dt-column-order:before, 145 | table.dataTable thead > tr > td.sorting_desc_disabled span.dt-column-order:after, 146 | table.dataTable thead > tr > td.sorting_asc_disabled span.dt-column-order:before { 147 | display: none; 148 | } 149 | table.dataTable thead > tr > th:active, 150 | table.dataTable thead > tr > td:active { 151 | outline: none; 152 | } 153 | 154 | div.dt-scroll-body > table.dataTable > thead > tr > th, 155 | div.dt-scroll-body > table.dataTable > thead > tr > td { 156 | overflow: hidden; 157 | } 158 | 159 | :root.dark table.dataTable thead > tr > th.dt-orderable-asc:hover, :root.dark table.dataTable thead > tr > th.dt-orderable-desc:hover, 160 | :root.dark table.dataTable thead > tr > td.dt-orderable-asc:hover, 161 | :root.dark table.dataTable thead > tr > td.dt-orderable-desc:hover, 162 | :root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-asc:hover, 163 | :root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-desc:hover, 164 | :root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-asc:hover, 165 | :root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-desc:hover { 166 | outline: 2px solid rgba(255, 255, 255, 0.05); 167 | } 168 | 169 | div.dt-processing { 170 | position: absolute; 171 | top: 50%; 172 | left: 50%; 173 | width: 200px; 174 | margin-left: -100px; 175 | margin-top: -22px; 176 | text-align: center; 177 | padding: 2px; 178 | z-index: 10; 179 | } 180 | div.dt-processing > div:last-child { 181 | position: relative; 182 | width: 80px; 183 | height: 15px; 184 | margin: 1em auto; 185 | } 186 | div.dt-processing > div:last-child > div { 187 | position: absolute; 188 | top: 0; 189 | width: 13px; 190 | height: 13px; 191 | border-radius: 50%; 192 | background: rgb(13, 110, 253); 193 | background: rgb(var(--dt-row-selected)); 194 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 195 | } 196 | div.dt-processing > div:last-child > div:nth-child(1) { 197 | left: 8px; 198 | animation: datatables-loader-1 0.6s infinite; 199 | } 200 | div.dt-processing > div:last-child > div:nth-child(2) { 201 | left: 8px; 202 | animation: datatables-loader-2 0.6s infinite; 203 | } 204 | div.dt-processing > div:last-child > div:nth-child(3) { 205 | left: 32px; 206 | animation: datatables-loader-2 0.6s infinite; 207 | } 208 | div.dt-processing > div:last-child > div:nth-child(4) { 209 | left: 56px; 210 | animation: datatables-loader-3 0.6s infinite; 211 | } 212 | 213 | @keyframes datatables-loader-1 { 214 | 0% { 215 | transform: scale(0); 216 | } 217 | 100% { 218 | transform: scale(1); 219 | } 220 | } 221 | @keyframes datatables-loader-3 { 222 | 0% { 223 | transform: scale(1); 224 | } 225 | 100% { 226 | transform: scale(0); 227 | } 228 | } 229 | @keyframes datatables-loader-2 { 230 | 0% { 231 | transform: translate(0, 0); 232 | } 233 | 100% { 234 | transform: translate(24px, 0); 235 | } 236 | } 237 | table.dataTable.nowrap th, table.dataTable.nowrap td { 238 | white-space: nowrap; 239 | } 240 | table.dataTable th, 241 | table.dataTable td { 242 | box-sizing: border-box; 243 | } 244 | table.dataTable th.dt-type-numeric, table.dataTable th.dt-type-date, 245 | table.dataTable td.dt-type-numeric, 246 | table.dataTable td.dt-type-date { 247 | text-align: right; 248 | } 249 | table.dataTable th.dt-left, 250 | table.dataTable td.dt-left { 251 | text-align: left; 252 | } 253 | table.dataTable th.dt-center, 254 | table.dataTable td.dt-center { 255 | text-align: center; 256 | } 257 | table.dataTable th.dt-right, 258 | table.dataTable td.dt-right { 259 | text-align: right; 260 | } 261 | table.dataTable th.dt-justify, 262 | table.dataTable td.dt-justify { 263 | text-align: justify; 264 | } 265 | table.dataTable th.dt-nowrap, 266 | table.dataTable td.dt-nowrap { 267 | white-space: nowrap; 268 | } 269 | table.dataTable th.dt-empty, 270 | table.dataTable td.dt-empty { 271 | text-align: center; 272 | vertical-align: top; 273 | } 274 | table.dataTable thead th, 275 | table.dataTable thead td, 276 | table.dataTable tfoot th, 277 | table.dataTable tfoot td { 278 | text-align: left; 279 | } 280 | table.dataTable thead th.dt-head-left, 281 | table.dataTable thead td.dt-head-left, 282 | table.dataTable tfoot th.dt-head-left, 283 | table.dataTable tfoot td.dt-head-left { 284 | text-align: left; 285 | } 286 | table.dataTable thead th.dt-head-center, 287 | table.dataTable thead td.dt-head-center, 288 | table.dataTable tfoot th.dt-head-center, 289 | table.dataTable tfoot td.dt-head-center { 290 | text-align: center; 291 | } 292 | table.dataTable thead th.dt-head-right, 293 | table.dataTable thead td.dt-head-right, 294 | table.dataTable tfoot th.dt-head-right, 295 | table.dataTable tfoot td.dt-head-right { 296 | text-align: right; 297 | } 298 | table.dataTable thead th.dt-head-justify, 299 | table.dataTable thead td.dt-head-justify, 300 | table.dataTable tfoot th.dt-head-justify, 301 | table.dataTable tfoot td.dt-head-justify { 302 | text-align: justify; 303 | } 304 | table.dataTable thead th.dt-head-nowrap, 305 | table.dataTable thead td.dt-head-nowrap, 306 | table.dataTable tfoot th.dt-head-nowrap, 307 | table.dataTable tfoot td.dt-head-nowrap { 308 | white-space: nowrap; 309 | } 310 | table.dataTable tbody th.dt-body-left, 311 | table.dataTable tbody td.dt-body-left { 312 | text-align: left; 313 | } 314 | table.dataTable tbody th.dt-body-center, 315 | table.dataTable tbody td.dt-body-center { 316 | text-align: center; 317 | } 318 | table.dataTable tbody th.dt-body-right, 319 | table.dataTable tbody td.dt-body-right { 320 | text-align: right; 321 | } 322 | table.dataTable tbody th.dt-body-justify, 323 | table.dataTable tbody td.dt-body-justify { 324 | text-align: justify; 325 | } 326 | table.dataTable tbody th.dt-body-nowrap, 327 | table.dataTable tbody td.dt-body-nowrap { 328 | white-space: nowrap; 329 | } 330 | 331 | /*! Bootstrap 5 integration for DataTables 332 | * 333 | * ©2020 SpryMedia Ltd, all rights reserved. 334 | * License: MIT datatables.net/license/mit 335 | */ 336 | table.table.dataTable { 337 | clear: both; 338 | margin-bottom: 0; 339 | max-width: none; 340 | border-spacing: 0; 341 | } 342 | table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { 343 | box-shadow: none; 344 | } 345 | table.table.dataTable > :not(caption) > * > * { 346 | background-color: var(--bs-table-bg); 347 | } 348 | table.table.dataTable > tbody > tr { 349 | background-color: transparent; 350 | } 351 | table.table.dataTable > tbody > tr.selected > * { 352 | box-shadow: inset 0 0 0 9999px rgb(13, 110, 253); 353 | box-shadow: inset 0 0 0 9999px rgb(var(--dt-row-selected)); 354 | color: rgb(255, 255, 255); 355 | color: rgb(var(--dt-row-selected-text)); 356 | } 357 | table.table.dataTable > tbody > tr.selected a { 358 | color: rgb(9, 10, 11); 359 | color: rgb(var(--dt-row-selected-link)); 360 | } 361 | table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { 362 | box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05); 363 | } 364 | table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1).selected > * { 365 | box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95); 366 | box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95); 367 | } 368 | table.table.dataTable.table-hover > tbody > tr:hover > * { 369 | box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075); 370 | } 371 | table.table.dataTable.table-hover > tbody > tr.selected:hover > * { 372 | box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975); 373 | box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975); 374 | } 375 | 376 | div.dt-container div.dt-layout-start > *:not(:last-child) { 377 | margin-right: 1em; 378 | } 379 | div.dt-container div.dt-layout-end > *:not(:first-child) { 380 | margin-left: 1em; 381 | } 382 | div.dt-container div.dt-layout-full { 383 | width: 100%; 384 | } 385 | div.dt-container div.dt-layout-full > *:only-child { 386 | margin-left: auto; 387 | margin-right: auto; 388 | } 389 | div.dt-container div.dt-layout-table > div { 390 | display: block !important; 391 | } 392 | 393 | @media screen and (max-width: 767px) { 394 | div.dt-container div.dt-layout-start > *:not(:last-child) { 395 | margin-right: 0; 396 | } 397 | div.dt-container div.dt-layout-end > *:not(:first-child) { 398 | margin-left: 0; 399 | } 400 | } 401 | div.dt-container div.dt-length label { 402 | font-weight: normal; 403 | text-align: left; 404 | white-space: nowrap; 405 | } 406 | div.dt-container div.dt-length select { 407 | width: auto; 408 | display: inline-block; 409 | margin-right: 0.5em; 410 | } 411 | div.dt-container div.dt-search { 412 | text-align: right; 413 | } 414 | div.dt-container div.dt-search label { 415 | font-weight: normal; 416 | white-space: nowrap; 417 | text-align: left; 418 | } 419 | div.dt-container div.dt-search input { 420 | margin-left: 0.5em; 421 | display: inline-block; 422 | width: auto; 423 | } 424 | div.dt-container div.dt-paging { 425 | margin: 0; 426 | } 427 | div.dt-container div.dt-paging ul.pagination { 428 | margin: 2px 0; 429 | flex-wrap: wrap; 430 | } 431 | div.dt-container div.dt-row { 432 | position: relative; 433 | } 434 | 435 | div.dt-scroll-head table.dataTable { 436 | margin-bottom: 0 !important; 437 | } 438 | 439 | div.dt-scroll-body { 440 | border-bottom-color: var(--bs-border-color); 441 | border-bottom-width: var(--bs-border-width); 442 | border-bottom-style: solid; 443 | } 444 | div.dt-scroll-body > table { 445 | border-top: none; 446 | margin-top: 0 !important; 447 | margin-bottom: 0 !important; 448 | } 449 | div.dt-scroll-body > table > tbody > tr:first-child { 450 | border-top-width: 0; 451 | } 452 | div.dt-scroll-body > table > thead > tr { 453 | border-width: 0 !important; 454 | } 455 | div.dt-scroll-body > table > tbody > tr:last-child > * { 456 | border-bottom: none; 457 | } 458 | 459 | div.dt-scroll-foot > .dt-scroll-footInner { 460 | box-sizing: content-box; 461 | } 462 | div.dt-scroll-foot > .dt-scroll-footInner > table { 463 | margin-top: 0 !important; 464 | border-top: none; 465 | } 466 | div.dt-scroll-foot > .dt-scroll-footInner > table > tfoot > tr:first-child { 467 | border-top-width: 0 !important; 468 | } 469 | 470 | @media screen and (max-width: 767px) { 471 | div.dt-container div.dt-length, 472 | div.dt-container div.dt-search, 473 | div.dt-container div.dt-info, 474 | div.dt-container div.dt-paging { 475 | text-align: center; 476 | } 477 | div.dt-container .row { 478 | --bs-gutter-y: 0.5rem; 479 | } 480 | div.dt-container div.dt-paging ul.pagination { 481 | justify-content: center !important; 482 | } 483 | } 484 | table.dataTable.table-sm > thead > tr th.dt-orderable-asc, table.dataTable.table-sm > thead > tr th.dt-orderable-desc, table.dataTable.table-sm > thead > tr th.dt-ordering-asc, table.dataTable.table-sm > thead > tr th.dt-ordering-desc, 485 | table.dataTable.table-sm > thead > tr td.dt-orderable-asc, 486 | table.dataTable.table-sm > thead > tr td.dt-orderable-desc, 487 | table.dataTable.table-sm > thead > tr td.dt-ordering-asc, 488 | table.dataTable.table-sm > thead > tr td.dt-ordering-desc { 489 | padding-right: 20px; 490 | } 491 | table.dataTable.table-sm > thead > tr th.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc span.dt-column-order, 492 | table.dataTable.table-sm > thead > tr td.dt-orderable-asc span.dt-column-order, 493 | table.dataTable.table-sm > thead > tr td.dt-orderable-desc span.dt-column-order, 494 | table.dataTable.table-sm > thead > tr td.dt-ordering-asc span.dt-column-order, 495 | table.dataTable.table-sm > thead > tr td.dt-ordering-desc span.dt-column-order { 496 | right: 5px; 497 | } 498 | 499 | div.dt-scroll-head table.table-bordered { 500 | border-bottom-width: 0; 501 | } 502 | 503 | div.table-responsive > div.dt-container > div.row { 504 | margin: 0; 505 | } 506 | div.table-responsive > div.dt-container > div.row > div[class^=col-]:first-child { 507 | padding-left: 0; 508 | } 509 | div.table-responsive > div.dt-container > div.row > div[class^=col-]:last-child { 510 | padding-right: 0; 511 | } 512 | 513 | :root[data-bs-theme=dark] { 514 | --dt-row-hover: 255, 255, 255; 515 | --dt-row-stripe: 255, 255, 255; 516 | --dt-column-ordering: 255, 255, 255; 517 | } 518 | -------------------------------------------------------------------------------- /lib/oxidized/web/public/weblibs/dataTables.bootstrap5.js: -------------------------------------------------------------------------------- 1 | /*! DataTables Bootstrap 5 integration 2 | * © SpryMedia Ltd - datatables.net/license 3 | */ 4 | 5 | (function( factory ){ 6 | if ( typeof define === 'function' && define.amd ) { 7 | // AMD 8 | define( ['jquery', 'datatables.net'], function ( $ ) { 9 | return factory( $, window, document ); 10 | } ); 11 | } 12 | else if ( typeof exports === 'object' ) { 13 | // CommonJS 14 | var jq = require('jquery'); 15 | var cjsRequires = function (root, $) { 16 | if ( ! $.fn.dataTable ) { 17 | require('datatables.net')(root, $); 18 | } 19 | }; 20 | 21 | if (typeof window === 'undefined') { 22 | module.exports = function (root, $) { 23 | if ( ! root ) { 24 | // CommonJS environments without a window global must pass a 25 | // root. This will give an error otherwise 26 | root = window; 27 | } 28 | 29 | if ( ! $ ) { 30 | $ = jq( root ); 31 | } 32 | 33 | cjsRequires( root, $ ); 34 | return factory( $, root, root.document ); 35 | }; 36 | } 37 | else { 38 | cjsRequires( window, jq ); 39 | module.exports = factory( jq, window, window.document ); 40 | } 41 | } 42 | else { 43 | // Browser 44 | factory( jQuery, window, document ); 45 | } 46 | }(function( $, window, document ) { 47 | 'use strict'; 48 | var DataTable = $.fn.dataTable; 49 | 50 | 51 | 52 | /** 53 | * DataTables integration for Bootstrap 5. 54 | * 55 | * This file sets the defaults and adds options to DataTables to style its 56 | * controls using Bootstrap. See https://datatables.net/manual/styling/bootstrap 57 | * for further information. 58 | */ 59 | 60 | /* Set the defaults for DataTables initialisation */ 61 | $.extend( true, DataTable.defaults, { 62 | renderer: 'bootstrap' 63 | } ); 64 | 65 | 66 | /* Default class modification */ 67 | $.extend( true, DataTable.ext.classes, { 68 | container: "dt-container dt-bootstrap5", 69 | search: { 70 | input: "form-control form-control-sm" 71 | }, 72 | length: { 73 | select: "form-select form-select-sm" 74 | }, 75 | processing: { 76 | container: "dt-processing card" 77 | }, 78 | layout: { 79 | row: 'row mt-2 justify-content-between', 80 | cell: 'd-md-flex justify-content-between align-items-center', 81 | tableCell: 'col-12', 82 | start: 'dt-layout-start col-md-auto me-auto', 83 | end: 'dt-layout-end col-md-auto ms-auto', 84 | full: 'dt-layout-full col-md' 85 | } 86 | } ); 87 | 88 | 89 | /* Bootstrap paging button renderer */ 90 | DataTable.ext.renderer.pagingButton.bootstrap = function (settings, buttonType, content, active, disabled) { 91 | var btnClasses = ['dt-paging-button', 'page-item']; 92 | 93 | if (active) { 94 | btnClasses.push('active'); 95 | } 96 | 97 | if (disabled) { 98 | btnClasses.push('disabled') 99 | } 100 | 101 | var li = $('
  • ').addClass(btnClasses.join(' ')); 102 | var a = $('' 1013 | ) 1014 | .html(this.c.dom.button.dropHtml) 1015 | .addClass(this.c.dom.button.dropClass) 1016 | .on('click.dtb', function (e) { 1017 | e.preventDefault(); 1018 | e.stopPropagation(); 1019 | 1020 | if (!dropButton.hasClass(dom.disabled)) { 1021 | splitAction(e, dt, dropButton, dropButtonConfig); 1022 | } 1023 | if (clickBlurs) { 1024 | dropButton.trigger('blur'); 1025 | } 1026 | }) 1027 | .on('keypress.dtb', function (e) { 1028 | if (e.keyCode === 13) { 1029 | e.preventDefault(); 1030 | 1031 | if (!dropButton.hasClass(dom.disabled)) { 1032 | splitAction(e, dt, dropButton, dropButtonConfig); 1033 | } 1034 | } 1035 | }); 1036 | 1037 | if (config.split.length === 0) { 1038 | dropButton.addClass('dtb-hide-drop'); 1039 | } 1040 | 1041 | splitDiv.append(dropButton).attr(dropButtonConfig.attr); 1042 | } 1043 | 1044 | return { 1045 | conf: config, 1046 | node: isSplit ? splitDiv.get(0) : button.get(0), 1047 | inserter: isSplit ? splitDiv : inserter, 1048 | buttons: [], 1049 | inCollection: inCollection, 1050 | isSplit: isSplit, 1051 | inSplit: inSplit, 1052 | collection: null, 1053 | textNode: textNode 1054 | }; 1055 | }, 1056 | 1057 | /** 1058 | * Spin over buttons checking if splits should be enabled or not. 1059 | * @param {*} buttons Array of buttons to check 1060 | */ 1061 | _checkSplitEnable: function (buttons) { 1062 | if (! buttons) { 1063 | buttons = this.s.buttons; 1064 | } 1065 | 1066 | for (var i=0 ; i 30) { 1281 | // Protect against misconfiguration killing the browser 1282 | throw 'Buttons: Too many iterations'; 1283 | } 1284 | } 1285 | 1286 | return Array.isArray(base) ? base : $.extend({}, base); 1287 | }; 1288 | 1289 | conf = toConfObject(conf); 1290 | 1291 | while (conf && conf.extend) { 1292 | // Use `toConfObject` in case the button definition being extended 1293 | // is itself a string or a function 1294 | if (!_dtButtons[conf.extend]) { 1295 | throw 'Cannot extend unknown button type: ' + conf.extend; 1296 | } 1297 | 1298 | var objArray = toConfObject(_dtButtons[conf.extend]); 1299 | if (Array.isArray(objArray)) { 1300 | return objArray; 1301 | } 1302 | else if (!objArray) { 1303 | // This is a little brutal as it might be possible to have a 1304 | // valid button without the extend, but if there is no extend 1305 | // then the host button would be acting in an undefined state 1306 | return false; 1307 | } 1308 | 1309 | // Stash the current class name 1310 | var originalClassName = objArray.className; 1311 | 1312 | if (conf.config !== undefined && objArray.config !== undefined) { 1313 | conf.config = $.extend({}, objArray.config, conf.config); 1314 | } 1315 | 1316 | conf = $.extend({}, objArray, conf); 1317 | 1318 | // The extend will have overwritten the original class name if the 1319 | // `conf` object also assigned a class, but we want to concatenate 1320 | // them so they are list that is combined from all extended buttons 1321 | if (originalClassName && conf.className !== originalClassName) { 1322 | conf.className = originalClassName + ' ' + conf.className; 1323 | } 1324 | 1325 | // Although we want the `conf` object to overwrite almost all of 1326 | // the properties of the object being extended, the `extend` 1327 | // property should come from the object being extended 1328 | conf.extend = objArray.extend; 1329 | } 1330 | 1331 | // Buttons to be added to a collection -gives the ability to define 1332 | // if buttons should be added to the start or end of a collection 1333 | var postfixButtons = conf.postfixButtons; 1334 | if (postfixButtons) { 1335 | if (!conf.buttons) { 1336 | conf.buttons = []; 1337 | } 1338 | 1339 | for (i = 0, ien = postfixButtons.length; i < ien; i++) { 1340 | conf.buttons.push(postfixButtons[i]); 1341 | } 1342 | } 1343 | 1344 | var prefixButtons = conf.prefixButtons; 1345 | if (prefixButtons) { 1346 | if (!conf.buttons) { 1347 | conf.buttons = []; 1348 | } 1349 | 1350 | for (i = 0, ien = prefixButtons.length; i < ien; i++) { 1351 | conf.buttons.splice(i, 0, prefixButtons[i]); 1352 | } 1353 | } 1354 | 1355 | return conf; 1356 | }, 1357 | 1358 | /** 1359 | * Display (and replace if there is an existing one) a popover attached to a button 1360 | * @param {string|node} content Content to show 1361 | * @param {DataTable.Api} hostButton DT API instance of the button 1362 | * @param {object} inOpts Options (see object below for all options) 1363 | */ 1364 | _popover: function (content, hostButton, inOpts) { 1365 | var dt = hostButton; 1366 | var c = this.c; 1367 | var closed = false; 1368 | var options = $.extend( 1369 | { 1370 | align: 'button-left', // button-right, dt-container, split-left, split-right 1371 | autoClose: false, 1372 | background: true, 1373 | backgroundClassName: 'dt-button-background', 1374 | closeButton: true, 1375 | containerClassName: c.dom.collection.container.className, 1376 | contentClassName: c.dom.collection.container.content.className, 1377 | collectionLayout: '', 1378 | collectionTitle: '', 1379 | dropup: false, 1380 | fade: 400, 1381 | popoverTitle: '', 1382 | rightAlignClassName: 'dt-button-right', 1383 | tag: c.dom.collection.container.tag 1384 | }, 1385 | inOpts 1386 | ); 1387 | 1388 | var containerSelector = 1389 | options.tag + '.' + options.containerClassName.replace(/ /g, '.'); 1390 | var hostButtonNode = hostButton.node(); 1391 | var hostNode = options.collectionLayout.includes('fixed') ? $('body') : hostButton.node(); 1392 | 1393 | var close = function () { 1394 | closed = true; 1395 | 1396 | _fadeOut($(containerSelector), options.fade, function () { 1397 | $(this).detach(); 1398 | }); 1399 | 1400 | $( 1401 | dt 1402 | .buttons('[aria-haspopup="dialog"][aria-expanded="true"]') 1403 | .nodes() 1404 | ).attr('aria-expanded', 'false'); 1405 | 1406 | $('div.dt-button-background').off('click.dtb-collection'); 1407 | Buttons.background( 1408 | false, 1409 | options.backgroundClassName, 1410 | options.fade, 1411 | hostNode 1412 | ); 1413 | 1414 | $(window).off('resize.resize.dtb-collection'); 1415 | $('body').off('.dtb-collection'); 1416 | dt.off('buttons-action.b-internal'); 1417 | dt.off('destroy'); 1418 | 1419 | $('body').trigger('buttons-popover-hide.dt'); 1420 | }; 1421 | 1422 | if (content === false) { 1423 | close(); 1424 | return; 1425 | } 1426 | 1427 | var existingExpanded = $( 1428 | dt.buttons('[aria-haspopup="dialog"][aria-expanded="true"]').nodes() 1429 | ); 1430 | if (existingExpanded.length) { 1431 | // Reuse the current position if the button that was triggered is inside an existing collection 1432 | if (hostNode.closest(containerSelector).length) { 1433 | hostNode = existingExpanded.eq(0); 1434 | } 1435 | 1436 | close(); 1437 | } 1438 | 1439 | // Sort buttons if defined 1440 | if (options.sort) { 1441 | var elements = $('button', content) 1442 | .map(function (idx, el) { 1443 | return { 1444 | text: $(el).text(), 1445 | el: el 1446 | }; 1447 | }) 1448 | .toArray(); 1449 | 1450 | elements.sort(function (a, b) { 1451 | return a.text.localeCompare(b.text); 1452 | }); 1453 | 1454 | $(content).append(elements.map(function (v) { 1455 | return v.el; 1456 | })); 1457 | } 1458 | 1459 | // Try to be smart about the layout 1460 | var cnt = $('.dt-button', content).length; 1461 | var mod = ''; 1462 | 1463 | if (cnt === 3) { 1464 | mod = 'dtb-b3'; 1465 | } 1466 | else if (cnt === 2) { 1467 | mod = 'dtb-b2'; 1468 | } 1469 | else if (cnt === 1) { 1470 | mod = 'dtb-b1'; 1471 | } 1472 | 1473 | var display = $('<' + options.tag + '/>') 1474 | .addClass(options.containerClassName) 1475 | .addClass(options.collectionLayout) 1476 | .addClass(options.splitAlignClass) 1477 | .addClass(mod) 1478 | .css('display', 'none') 1479 | .attr({ 1480 | 'aria-modal': true, 1481 | role: 'dialog' 1482 | }); 1483 | 1484 | content = $(content) 1485 | .addClass(options.contentClassName) 1486 | .attr('role', 'menu') 1487 | .appendTo(display); 1488 | 1489 | hostButtonNode.attr('aria-expanded', 'true'); 1490 | 1491 | if (hostNode.parents('body')[0] !== document.body) { 1492 | hostNode = $(document.body).children('div, section, p').last(); 1493 | } 1494 | 1495 | if (options.popoverTitle) { 1496 | display.prepend( 1497 | '
    ' + 1498 | options.popoverTitle + 1499 | '
    ' 1500 | ); 1501 | } 1502 | else if (options.collectionTitle) { 1503 | display.prepend( 1504 | '
    ' + 1505 | options.collectionTitle + 1506 | '
    ' 1507 | ); 1508 | } 1509 | 1510 | if (options.closeButton) { 1511 | display 1512 | .prepend('
    ×
    ') 1513 | .addClass('dtb-collection-closeable'); 1514 | } 1515 | 1516 | _fadeIn(display.insertAfter(hostNode), options.fade); 1517 | 1518 | var tableContainer = $(hostButton.table().container()); 1519 | var position = display.css('position'); 1520 | 1521 | if (options.span === 'container' || options.align === 'dt-container') { 1522 | hostNode = hostNode.parent(); 1523 | display.css('width', tableContainer.width()); 1524 | } 1525 | 1526 | // Align the popover relative to the DataTables container 1527 | // Useful for wide popovers such as SearchPanes 1528 | if (position === 'absolute') { 1529 | // Align relative to the host button 1530 | var offsetParent = $(hostNode[0].offsetParent); 1531 | var buttonPosition = hostNode.position(); 1532 | var buttonOffset = hostNode.offset(); 1533 | var tableSizes = offsetParent.offset(); 1534 | var containerPosition = offsetParent.position(); 1535 | var computed = window.getComputedStyle(offsetParent[0]); 1536 | 1537 | tableSizes.height = offsetParent.outerHeight(); 1538 | tableSizes.width = 1539 | offsetParent.width() + parseFloat(computed.paddingLeft); 1540 | tableSizes.right = tableSizes.left + tableSizes.width; 1541 | tableSizes.bottom = tableSizes.top + tableSizes.height; 1542 | 1543 | // Set the initial position so we can read height / width 1544 | var top = buttonPosition.top + hostNode.outerHeight(); 1545 | var left = buttonPosition.left; 1546 | 1547 | display.css({ 1548 | top: top, 1549 | left: left 1550 | }); 1551 | 1552 | // Get the popover position 1553 | computed = window.getComputedStyle(display[0]); 1554 | var popoverSizes = display.offset(); 1555 | 1556 | popoverSizes.height = display.outerHeight(); 1557 | popoverSizes.width = display.outerWidth(); 1558 | popoverSizes.right = popoverSizes.left + popoverSizes.width; 1559 | popoverSizes.bottom = popoverSizes.top + popoverSizes.height; 1560 | popoverSizes.marginTop = parseFloat(computed.marginTop); 1561 | popoverSizes.marginBottom = parseFloat(computed.marginBottom); 1562 | 1563 | // First position per the class requirements - pop up and right align 1564 | if (options.dropup) { 1565 | top = 1566 | buttonPosition.top - 1567 | popoverSizes.height - 1568 | popoverSizes.marginTop - 1569 | popoverSizes.marginBottom; 1570 | } 1571 | 1572 | if ( 1573 | options.align === 'button-right' || 1574 | display.hasClass(options.rightAlignClassName) 1575 | ) { 1576 | left = 1577 | buttonPosition.left - 1578 | popoverSizes.width + 1579 | hostNode.outerWidth(); 1580 | } 1581 | 1582 | // Container alignment - make sure it doesn't overflow the table container 1583 | if ( 1584 | options.align === 'dt-container' || 1585 | options.align === 'container' 1586 | ) { 1587 | if (left < buttonPosition.left) { 1588 | left = -buttonPosition.left; 1589 | } 1590 | } 1591 | 1592 | // Window adjustment 1593 | if ( 1594 | containerPosition.left + left + popoverSizes.width > 1595 | $(window).width() 1596 | ) { 1597 | // Overflowing the document to the right 1598 | left = 1599 | $(window).width() - 1600 | popoverSizes.width - 1601 | containerPosition.left; 1602 | } 1603 | 1604 | if (buttonOffset.left + left < 0) { 1605 | // Off to the left of the document 1606 | left = -buttonOffset.left; 1607 | } 1608 | 1609 | if ( 1610 | containerPosition.top + top + popoverSizes.height > 1611 | $(window).height() + $(window).scrollTop() 1612 | ) { 1613 | // Pop up if otherwise we'd need the user to scroll down 1614 | top = 1615 | buttonPosition.top - 1616 | popoverSizes.height - 1617 | popoverSizes.marginTop - 1618 | popoverSizes.marginBottom; 1619 | } 1620 | 1621 | if (offsetParent.offset().top + top < $(window).scrollTop()) { 1622 | // Correction for when the top is beyond the top of the page 1623 | top = buttonPosition.top + hostNode.outerHeight(); 1624 | } 1625 | 1626 | // Calculations all done - now set it 1627 | display.css({ 1628 | top: top, 1629 | left: left 1630 | }); 1631 | } 1632 | else { 1633 | // Fix position - centre on screen 1634 | var place = function () { 1635 | var half = $(window).height() / 2; 1636 | 1637 | var top = display.height() / 2; 1638 | if (top > half) { 1639 | top = half; 1640 | } 1641 | 1642 | display.css('marginTop', top * -1); 1643 | }; 1644 | 1645 | place(); 1646 | 1647 | $(window).on('resize.dtb-collection', function () { 1648 | place(); 1649 | }); 1650 | } 1651 | 1652 | if (options.background) { 1653 | Buttons.background( 1654 | true, 1655 | options.backgroundClassName, 1656 | options.fade, 1657 | options.backgroundHost || hostNode 1658 | ); 1659 | } 1660 | 1661 | // This is bonkers, but if we don't have a click listener on the 1662 | // background element, iOS Safari will ignore the body click 1663 | // listener below. An empty function here is all that is 1664 | // required to make it work... 1665 | $('div.dt-button-background').on( 1666 | 'click.dtb-collection', 1667 | function () {} 1668 | ); 1669 | 1670 | if (options.autoClose) { 1671 | setTimeout(function () { 1672 | dt.on('buttons-action.b-internal', function (e, btn, dt, node) { 1673 | if (node[0] === hostNode[0]) { 1674 | return; 1675 | } 1676 | close(); 1677 | }); 1678 | }, 0); 1679 | } 1680 | 1681 | $(display).trigger('buttons-popover.dt'); 1682 | 1683 | dt.on('destroy', close); 1684 | 1685 | setTimeout(function () { 1686 | closed = false; 1687 | $('body') 1688 | .on('click.dtb-collection', function (e) { 1689 | if (closed) { 1690 | return; 1691 | } 1692 | 1693 | // andSelf is deprecated in jQ1.8, but we want 1.7 compat 1694 | var back = $.fn.addBack ? 'addBack' : 'andSelf'; 1695 | var parent = $(e.target).parent()[0]; 1696 | 1697 | if ( 1698 | (!$(e.target).parents()[back]().filter(content) 1699 | .length && 1700 | !$(parent).hasClass('dt-buttons')) || 1701 | $(e.target).hasClass('dt-button-background') 1702 | ) { 1703 | close(); 1704 | } 1705 | }) 1706 | .on('keyup.dtb-collection', function (e) { 1707 | if (e.keyCode === 27) { 1708 | close(); 1709 | } 1710 | }) 1711 | .on('keydown.dtb-collection', function (e) { 1712 | // Focus trap for tab key 1713 | var elements = $('a, button', content); 1714 | var active = document.activeElement; 1715 | 1716 | if (e.keyCode !== 9) { 1717 | // tab 1718 | return; 1719 | } 1720 | 1721 | if (elements.index(active) === -1) { 1722 | // If current focus is not inside the popover 1723 | elements.first().focus(); 1724 | e.preventDefault(); 1725 | } 1726 | else if (e.shiftKey) { 1727 | // Reverse tabbing order when shift key is pressed 1728 | if (active === elements[0]) { 1729 | elements.last().focus(); 1730 | e.preventDefault(); 1731 | } 1732 | } 1733 | else { 1734 | if (active === elements.last()[0]) { 1735 | elements.first().focus(); 1736 | e.preventDefault(); 1737 | } 1738 | } 1739 | }); 1740 | }, 0); 1741 | } 1742 | }); 1743 | 1744 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1745 | * Statics 1746 | */ 1747 | 1748 | /** 1749 | * Show / hide a background layer behind a collection 1750 | * @param {boolean} Flag to indicate if the background should be shown or 1751 | * hidden 1752 | * @param {string} Class to assign to the background 1753 | * @static 1754 | */ 1755 | Buttons.background = function (show, className, fade, insertPoint) { 1756 | if (fade === undefined) { 1757 | fade = 400; 1758 | } 1759 | if (!insertPoint) { 1760 | insertPoint = document.body; 1761 | } 1762 | 1763 | if (show) { 1764 | _fadeIn( 1765 | $('
    ') 1766 | .addClass(className) 1767 | .css('display', 'none') 1768 | .insertAfter(insertPoint), 1769 | fade 1770 | ); 1771 | } 1772 | else { 1773 | _fadeOut($('div.' + className), fade, function () { 1774 | $(this).removeClass(className).remove(); 1775 | }); 1776 | } 1777 | }; 1778 | 1779 | /** 1780 | * Instance selector - select Buttons instances based on an instance selector 1781 | * value from the buttons assigned to a DataTable. This is only useful if 1782 | * multiple instances are attached to a DataTable. 1783 | * @param {string|int|array} Instance selector - see `instance-selector` 1784 | * documentation on the DataTables site 1785 | * @param {array} Button instance array that was attached to the DataTables 1786 | * settings object 1787 | * @return {array} Buttons instances 1788 | * @static 1789 | */ 1790 | Buttons.instanceSelector = function (group, buttons) { 1791 | if (group === undefined || group === null) { 1792 | return $.map(buttons, function (v) { 1793 | return v.inst; 1794 | }); 1795 | } 1796 | 1797 | var ret = []; 1798 | var names = $.map(buttons, function (v) { 1799 | return v.name; 1800 | }); 1801 | 1802 | // Flatten the group selector into an array of single options 1803 | var process = function (input) { 1804 | if (Array.isArray(input)) { 1805 | for (var i = 0, ien = input.length; i < ien; i++) { 1806 | process(input[i]); 1807 | } 1808 | return; 1809 | } 1810 | 1811 | if (typeof input === 'string') { 1812 | if (input.indexOf(',') !== -1) { 1813 | // String selector, list of names 1814 | process(input.split(',')); 1815 | } 1816 | else { 1817 | // String selector individual name 1818 | var idx = $.inArray(input.trim(), names); 1819 | 1820 | if (idx !== -1) { 1821 | ret.push(buttons[idx].inst); 1822 | } 1823 | } 1824 | } 1825 | else if (typeof input === 'number') { 1826 | // Index selector 1827 | ret.push(buttons[input].inst); 1828 | } 1829 | else if (typeof input === 'object' && input.nodeName) { 1830 | // Element selector 1831 | for (var j = 0; j < buttons.length; j++) { 1832 | if (buttons[j].inst.dom.container[0] === input) { 1833 | ret.push(buttons[j].inst); 1834 | } 1835 | } 1836 | } 1837 | else if (typeof input === 'object') { 1838 | // Actual instance selector 1839 | ret.push(input); 1840 | } 1841 | }; 1842 | 1843 | process(group); 1844 | 1845 | return ret; 1846 | }; 1847 | 1848 | /** 1849 | * Button selector - select one or more buttons from a selector input so some 1850 | * operation can be performed on them. 1851 | * @param {array} Button instances array that the selector should operate on 1852 | * @param {string|int|node|jQuery|array} Button selector - see 1853 | * `button-selector` documentation on the DataTables site 1854 | * @return {array} Array of objects containing `inst` and `idx` properties of 1855 | * the selected buttons so you know which instance each button belongs to. 1856 | * @static 1857 | */ 1858 | Buttons.buttonSelector = function (insts, selector) { 1859 | var ret = []; 1860 | var nodeBuilder = function (a, buttons, baseIdx) { 1861 | var button; 1862 | var idx; 1863 | 1864 | for (var i = 0, ien = buttons.length; i < ien; i++) { 1865 | button = buttons[i]; 1866 | 1867 | if (button) { 1868 | idx = baseIdx !== undefined ? baseIdx + i : i + ''; 1869 | 1870 | a.push({ 1871 | node: button.node, 1872 | name: button.conf.name, 1873 | idx: idx 1874 | }); 1875 | 1876 | if (button.buttons) { 1877 | nodeBuilder(a, button.buttons, idx + '-'); 1878 | } 1879 | } 1880 | } 1881 | }; 1882 | 1883 | var run = function (selector, inst) { 1884 | var i, ien; 1885 | var buttons = []; 1886 | nodeBuilder(buttons, inst.s.buttons); 1887 | 1888 | var nodes = $.map(buttons, function (v) { 1889 | return v.node; 1890 | }); 1891 | 1892 | if (Array.isArray(selector) || selector instanceof $) { 1893 | for (i = 0, ien = selector.length; i < ien; i++) { 1894 | run(selector[i], inst); 1895 | } 1896 | return; 1897 | } 1898 | 1899 | if (selector === null || selector === undefined || selector === '*') { 1900 | // Select all 1901 | for (i = 0, ien = buttons.length; i < ien; i++) { 1902 | ret.push({ 1903 | inst: inst, 1904 | node: buttons[i].node 1905 | }); 1906 | } 1907 | } 1908 | else if (typeof selector === 'number') { 1909 | // Main button index selector 1910 | if (inst.s.buttons[selector]) { 1911 | ret.push({ 1912 | inst: inst, 1913 | node: inst.s.buttons[selector].node 1914 | }); 1915 | } 1916 | } 1917 | else if (typeof selector === 'string') { 1918 | if (selector.indexOf(',') !== -1) { 1919 | // Split 1920 | var a = selector.split(','); 1921 | 1922 | for (i = 0, ien = a.length; i < ien; i++) { 1923 | run(a[i].trim(), inst); 1924 | } 1925 | } 1926 | else if (selector.match(/^\d+(\-\d+)*$/)) { 1927 | // Sub-button index selector 1928 | var indexes = $.map(buttons, function (v) { 1929 | return v.idx; 1930 | }); 1931 | 1932 | ret.push({ 1933 | inst: inst, 1934 | node: buttons[$.inArray(selector, indexes)].node 1935 | }); 1936 | } 1937 | else if (selector.indexOf(':name') !== -1) { 1938 | // Button name selector 1939 | var name = selector.replace(':name', ''); 1940 | 1941 | for (i = 0, ien = buttons.length; i < ien; i++) { 1942 | if (buttons[i].name === name) { 1943 | ret.push({ 1944 | inst: inst, 1945 | node: buttons[i].node 1946 | }); 1947 | } 1948 | } 1949 | } 1950 | else { 1951 | // jQuery selector on the nodes 1952 | $(nodes) 1953 | .filter(selector) 1954 | .each(function () { 1955 | ret.push({ 1956 | inst: inst, 1957 | node: this 1958 | }); 1959 | }); 1960 | } 1961 | } 1962 | else if (typeof selector === 'object' && selector.nodeName) { 1963 | // Node selector 1964 | var idx = $.inArray(selector, nodes); 1965 | 1966 | if (idx !== -1) { 1967 | ret.push({ 1968 | inst: inst, 1969 | node: nodes[idx] 1970 | }); 1971 | } 1972 | } 1973 | }; 1974 | 1975 | for (var i = 0, ien = insts.length; i < ien; i++) { 1976 | var inst = insts[i]; 1977 | 1978 | run(selector, inst); 1979 | } 1980 | 1981 | return ret; 1982 | }; 1983 | 1984 | /** 1985 | * Default function used for formatting output data. 1986 | * @param {*} str Data to strip 1987 | */ 1988 | Buttons.stripData = function (str, config) { 1989 | // If the input is an HTML element, we can use the HTML from it (HTML might be stripped below). 1990 | if (str !== null && typeof str === 'object' && str.nodeName && str.nodeType) { 1991 | str = str.innerHTML; 1992 | } 1993 | 1994 | if (typeof str !== 'string') { 1995 | return str; 1996 | } 1997 | 1998 | // Always remove script tags 1999 | str = Buttons.stripHtmlScript(str); 2000 | 2001 | // Always remove comments 2002 | str = Buttons.stripHtmlComments(str); 2003 | 2004 | if (!config || config.stripHtml) { 2005 | str = DataTable.util.stripHtml(str); 2006 | } 2007 | 2008 | if (!config || config.trim) { 2009 | str = str.trim(); 2010 | } 2011 | 2012 | if (!config || config.stripNewlines) { 2013 | str = str.replace(/\n/g, ' '); 2014 | } 2015 | 2016 | if (!config || config.decodeEntities) { 2017 | if (_entityDecoder) { 2018 | str = _entityDecoder(str); 2019 | } 2020 | else { 2021 | _exportTextarea.innerHTML = str; 2022 | str = _exportTextarea.value; 2023 | } 2024 | } 2025 | 2026 | // Prevent Excel from running a formula 2027 | if (!config || config.escapeExcelFormula) { 2028 | if (str.match(/^[=+\-@\t\r]/)) { 2029 | console.log('matching and updateing'); 2030 | str = "'" + str; 2031 | } 2032 | } 2033 | 2034 | return str; 2035 | }; 2036 | 2037 | /** 2038 | * Provide a custom entity decoding function - e.g. a regex one, which can be 2039 | * much faster than the built in DOM option, but also larger code size. 2040 | * @param {function} fn 2041 | */ 2042 | Buttons.entityDecoder = function (fn) { 2043 | _entityDecoder = fn; 2044 | }; 2045 | 2046 | /** 2047 | * Common function for stripping HTML comments 2048 | * 2049 | * @param {*} input 2050 | * @returns 2051 | */ 2052 | Buttons.stripHtmlComments = function (input) { 2053 | var previous; 2054 | 2055 | do { 2056 | previous = input; 2057 | input = input.replace(/(