mirror of
https://github.com/pi-hole/web.git
synced 2026-05-08 09:39:05 +01:00
Pi-hole Web v6.2 (#3444)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
FROM node:21-alpine3.18
|
||||
FROM node:22-alpine3.21
|
||||
RUN apk add --no-cache \
|
||||
git \
|
||||
nano\
|
||||
openssh
|
||||
git \
|
||||
nano \
|
||||
openssh
|
||||
|
||||
USER node
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-syntax
|
||||
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
* @pi-hole/web-maintainers
|
||||
+17
-21
@@ -1,23 +1,19 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "10:00"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: development
|
||||
versioning-strategy: increase
|
||||
reviewers:
|
||||
- "pi-hole/web-maintainers"
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "10:00"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: development
|
||||
reviewers:
|
||||
- "pi-hole/web-maintainers"
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "10:00"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: development
|
||||
versioning-strategy: increase
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: saturday
|
||||
time: "10:00"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: development
|
||||
|
||||
@@ -31,16 +31,16 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.13
|
||||
uses: github/codeql-action/init@v3.28.18
|
||||
with:
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
languages: "javascript"
|
||||
queries: +security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3.28.13
|
||||
uses: github/codeql-action/autobuild@v3.28.18
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.13
|
||||
uses: github/codeql-action/analyze@v3.28.18
|
||||
with:
|
||||
category: "/language:javascript"
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Codespell
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- "**"
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
@@ -12,11 +12,12 @@ jobs:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
-
|
||||
name: Spell-Checking
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Spell-Checking
|
||||
uses: codespell-project/actions-codespell@master
|
||||
with:
|
||||
ignore_words_file: .codespellignore
|
||||
|
||||
@@ -9,6 +9,9 @@ jobs:
|
||||
name: editorconfig-checker
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: editorconfig-checker/action-editorconfig-checker@main
|
||||
- run: editorconfig-checker
|
||||
|
||||
+15
-13
@@ -2,7 +2,7 @@ name: Mark stale issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 8 * * *'
|
||||
- cron: "0 8 * * *"
|
||||
workflow_dispatch:
|
||||
issue_comment:
|
||||
|
||||
@@ -17,17 +17,17 @@ jobs:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v9.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Please comment or update this issue or it will be closed in 5 days.'
|
||||
stale-issue-label: '${{ env.stale_label }}'
|
||||
exempt-issue-labels: 'internal, Fixed In Next Release, Bug, never-stale'
|
||||
exempt-all-issue-assignees: true
|
||||
operations-per-run: 300
|
||||
close-issue-reason: 'not_planned'
|
||||
- uses: actions/stale@v9.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Please comment or update this issue or it will be closed in 5 days."
|
||||
stale-issue-label: "${{ env.stale_label }}"
|
||||
exempt-issue-labels: "internal, Fixed In Next Release, Bug, never-stale, Bug: Confirmed"
|
||||
exempt-all-issue-assignees: true
|
||||
operations-per-run: 300
|
||||
close-issue-reason: "not_planned"
|
||||
|
||||
remove_stale:
|
||||
# trigger "stale" removal immediately when stale issues are commented on
|
||||
@@ -40,8 +40,10 @@ jobs:
|
||||
issues: write # to edit issues label
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Remove 'stale' label
|
||||
run: gh issue edit ${{ github.event.issue.number }} --remove-label ${{ env.stale_label }}
|
||||
env:
|
||||
|
||||
@@ -5,12 +5,11 @@ name: Close stale PR
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 10 * * *'
|
||||
- cron: "0 10 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
@@ -27,9 +26,9 @@ jobs:
|
||||
# Close PRs immediately, after marking them 'stale'
|
||||
days-before-pr-close: 0
|
||||
# only run the action on merge conflict PR
|
||||
any-of-labels: 'Merge Conflicts'
|
||||
exempt-pr-labels: 'internal, never-stale, ON HOLD, WIP'
|
||||
any-of-labels: "Merge Conflicts"
|
||||
exempt-pr-labels: "internal, never-stale, ON HOLD, WIP"
|
||||
exempt-all-pr-assignees: true
|
||||
operations-per-run: 300
|
||||
stale-pr-message: ''
|
||||
close-pr-message: 'Existing merge conflicts have not been addressed. This PR is considered abandoned.'
|
||||
stale-pr-message: ""
|
||||
close-pr-message: "Existing merge conflicts have not been addressed. This PR is considered abandoned."
|
||||
|
||||
@@ -10,8 +10,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Syncing branches
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Opening pull request
|
||||
run: gh pr create -B development -H master --title 'Sync master back into development' --body 'Created by Github action' --label 'internal'
|
||||
env:
|
||||
|
||||
@@ -20,11 +20,13 @@ jobs:
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4.3.0
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version: "20.x"
|
||||
node-version: "22.x"
|
||||
cache: npm
|
||||
|
||||
- name: Install npm dependencies
|
||||
|
||||
@@ -206,7 +206,7 @@ Lists, domains (block or allow) and regex entries can be managed through groups.
|
||||
|
||||
**Local DNS Settings:**
|
||||
- Local DNS records
|
||||
- Local CNAME records records
|
||||
- Local CNAME records
|
||||
|
||||
**All Settings** (only visible in Expert mode):
|
||||
- Advanced settings page, containing all available options
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
|
||||
mg.include('scripts/lua/header.lp','r')
|
||||
?>
|
||||
<body class="hold-transition layout-boxed login-page">
|
||||
<body class="hold-transition layout-boxed login-page page-<?=pihole.format_path(mg.request_info.request_uri)?>">
|
||||
<div class="box login-box">
|
||||
<section style="padding: 15px;">
|
||||
<h2 class="error-headline text-danger">403</h2>
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
|
||||
mg.include('scripts/lua/header.lp','r')
|
||||
?>
|
||||
<body class="hold-transition layout-boxed login-page">
|
||||
<body class="hold-transition layout-boxed login-page page-<?=pihole.format_path(mg.request_info.request_uri)?>">
|
||||
<div class="box login-box">
|
||||
<section style="padding: 15px;">
|
||||
<h2 class="error-headline text-yellow">404</h2>
|
||||
|
||||
+1
-1
@@ -27,5 +27,5 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
<button type="button" id="gravityBtn" class="btn btn-lg btn-primary btn-block">Update</button>
|
||||
<pre id="output" style="width: 100%; height: 100%;" hidden></pre>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/gravity.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/gravity.js')?>"></script>
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+5
-5
@@ -95,10 +95,10 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-select/bootstrap-select.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/ip-address-sorting.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/groups-common.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/groups-clients.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-select/bootstrap-select.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/ip-address-sorting.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/groups-common.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/groups-clients.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+5
-5
@@ -75,7 +75,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<strong>Hint:</strong> Need help to write a proper RegEx rule? Have a look at our online
|
||||
<a href="https://docs.pi-hole.net/ftldns/regex/tutorial" rel="noopener" target="_blank">
|
||||
<a href="https://docs.pi-hole.net/ftldns/regex/tutorial" rel="noopener noreferrer" target="_blank">
|
||||
regular expressions tutorial</a>.
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,9 +156,9 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-select/bootstrap-select.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/groups-common.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/groups-domains.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-select/bootstrap-select.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/groups-common.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/groups-domains.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+4
-4
@@ -94,9 +94,9 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-select/bootstrap-select.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/groups-common.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/groups-lists.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-select/bootstrap-select.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/groups-common.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/groups-lists.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
@@ -79,9 +79,9 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-select/bootstrap-select.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/groups-common.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/groups.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-select/bootstrap-select.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/groups-common.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/groups.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
@@ -16,7 +16,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
<!-- small box -->
|
||||
<div class="small-box bg-aqua no-user-select" id="total_queries" title="Total number of queries">
|
||||
<div class="inner">
|
||||
<p>Total queries</p>
|
||||
<p>Total Queries</p>
|
||||
<h3 class="statistic"><span id="dns_queries">---</span></h3>
|
||||
</div>
|
||||
<div class="icon">
|
||||
@@ -108,7 +108,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="chart" style="width: 100%; height: 180px">
|
||||
<canvas id="clientsChart" class="extratooltipcanvas no-user-select"></canvas>
|
||||
<canvas id="clientsChart" class="no-user-select"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay">
|
||||
@@ -144,7 +144,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div style="width:50%">
|
||||
<canvas id="forwardDestinationPieChart" width="280" height="280" class="extratooltipcanvas no-user-select"></canvas>
|
||||
<canvas id="forwardDestinationPieChart" width="280" height="280" class="no-user-select"></canvas>
|
||||
</div>
|
||||
<div class="chart-legend" style="width:50%" id="forward-destinations-legend"></div>
|
||||
</div>
|
||||
@@ -166,7 +166,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
@@ -195,7 +195,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
@@ -226,7 +226,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
@@ -255,7 +255,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
@@ -279,7 +279,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/charts.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/index.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/charts.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/index.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+1
-3
@@ -30,7 +30,5 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre id="output" style="width: 100%; height: 100%;" hidden></pre>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/interfaces.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/interfaces.js')?>"></script>
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
mg.include('scripts/lua/header.lp','r')
|
||||
?>
|
||||
<body class="hold-transition layout-boxed login-page" data-apiurl="<?=pihole.api_url()?>">
|
||||
<body class="hold-transition layout-boxed login-page page-<?=pihole.format_path(mg.request_info.request_uri)?>" data-apiurl="<?=pihole.api_url()?>" data-webhome="<?=webhome?>">
|
||||
<div class="box login-box" id="login-box">
|
||||
<section style="padding: 15px;">
|
||||
<div class="login-logo">
|
||||
@@ -72,7 +72,7 @@ mg.include('scripts/lua/header.lp','r')
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>Your Pi-hole has two-factor authentication enabled. You have to
|
||||
enter a valid <a href="https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm" rel="noopener" target="_blank">TOTP</a>
|
||||
enter a valid <a href="https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm" rel="noopener noreferrer" target="_blank">TOTP</a>
|
||||
token in addition to your password. You see this message because your
|
||||
token was incorrect or has already expired.</p>
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@ mg.include('scripts/lua/header.lp','r')
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="box-body" style="display: none;">
|
||||
<p>After installing Pi-hole for the first time, a password is generated and displayed
|
||||
to the user. The password cannot be retrieved later on, but it is possible to set
|
||||
a new password (or explicitly disable the password by setting an empty password)
|
||||
@@ -104,9 +104,9 @@ mg.include('scripts/lua/header.lp','r')
|
||||
</div>
|
||||
<!-- /.login-card-body -->
|
||||
<div class="login-footer" style="margin-top: 15px; display: flex; justify-content: space-between;">
|
||||
<a class="btn btn-default btn-sm" role="button" href="https://docs.pi-hole.net/" rel="noopener" target="_blank"><i class="fas fa-question-circle"></i> Documentation</a>
|
||||
<a class="btn btn-default btn-sm" role="button" href="https://github.com/pi-hole/" rel="noopener" target="_blank"><i class="fab fa-github"></i> GitHub</a>
|
||||
<a class="btn btn-default btn-sm" role="button" href="https://discourse.pi-hole.net/" rel="noopener" target="_blank"><i class="fab fa-discourse"></i> Pi-hole Discourse</a>
|
||||
<a class="btn btn-default btn-sm" role="button" href="https://docs.pi-hole.net/" rel="noopener noreferrer" target="_blank"><i class="fas fa-question-circle"></i> Documentation</a>
|
||||
<a class="btn btn-default btn-sm" role="button" href="https://github.com/pi-hole/" rel="noopener noreferrer" target="_blank"><i class="fab fa-github"></i> GitHub</a>
|
||||
<a class="btn btn-default btn-sm" role="button" href="https://discourse.pi-hole.net/" rel="noopener noreferrer" target="_blank"><i class="fab fa-discourse"></i> Pi-hole Discourse</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -114,10 +114,10 @@ mg.include('scripts/lua/header.lp','r')
|
||||
|
||||
<div class="login-donate">
|
||||
<div class="text-center" style="font-size:125%">
|
||||
<strong><a href="https://pi-hole.net/donate/" rel="noopener" target="_blank"><i class="fa fa-heart text-red"></i> Donate</a></strong> if you found this useful.
|
||||
<strong><a href="https://pi-hole.net/donate/" rel="noopener noreferrer" target="_blank"><i class="fa fa-heart text-red"></i> Donate</a></strong> if you found this useful.
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/footer.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/login.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/footer.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/login.js')?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -41,6 +41,6 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/messages.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/messages.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+2
-2
@@ -64,7 +64,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/ip-address-sorting.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/network.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/ip-address-sorting.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/network.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
Generated
+6
-13
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "Pi-hole web interface",
|
||||
"name": "pi-hole-web-interface",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Pi-hole web interface",
|
||||
"name": "pi-hole-web-interface",
|
||||
"version": "1.0.0",
|
||||
"license": "EUPL-1.2",
|
||||
"dependencies": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"bootstrap-select": "1.13.18",
|
||||
"bootstrap-toggle": "2.2.2",
|
||||
"bstreeview": "1.2.0",
|
||||
"chart.js": "4.4.8",
|
||||
"chart.js": "4.4.9",
|
||||
"chartjs-adapter-moment": "1.0.1",
|
||||
"chartjs-plugin-deferred": "2.0.0",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
@@ -27,7 +27,6 @@
|
||||
"daterangepicker": "3.1.0",
|
||||
"hammerjs": "2.0.8",
|
||||
"icheck-bootstrap": "3.0.1",
|
||||
"icheck-material": "1.0.1",
|
||||
"jquery": "3.7.1",
|
||||
"modernized-waitme": "1.0.0",
|
||||
"moment": "2.30.1",
|
||||
@@ -1879,9 +1878,9 @@
|
||||
"license": "MIT/X11"
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.4.8",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
|
||||
"integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
|
||||
"version": "4.4.9",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
|
||||
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
@@ -4865,12 +4864,6 @@
|
||||
"integrity": "sha512-Rj3SybdcMcayhsP4IJ+hmCNgCKclaFcs/5zwCuLXH1WMo468NegjhZVxbSNKhEjJjnwc4gKETogUmPYSQ9lEZQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/icheck-material": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/icheck-material/-/icheck-material-1.0.1.tgz",
|
||||
"integrity": "sha512-uruQ7el8Jq/7mTiW/+TC694g1hsdf7XgjGSbqccuIfMketEqy5OubDnBOUjaRYdNdeVQORnUBzTnVEIdjG7l/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
|
||||
+10
-32
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Pi-hole web interface",
|
||||
"name": "pi-hole-web-interface",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
@@ -25,6 +25,7 @@
|
||||
"testpr": "npm run prefix && npm run prettier:fix && git diff --ws-error-highlight=all --color=always --exit-code && npm run xo:check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.7.2",
|
||||
"admin-lte": "2.4.18",
|
||||
"animate.css": "4.1.1",
|
||||
"bootstrap": "3.4.1",
|
||||
@@ -32,7 +33,7 @@
|
||||
"bootstrap-select": "1.13.18",
|
||||
"bootstrap-toggle": "2.2.2",
|
||||
"bstreeview": "1.2.0",
|
||||
"chart.js": "4.4.8",
|
||||
"chart.js": "4.4.9",
|
||||
"chartjs-adapter-moment": "1.0.1",
|
||||
"chartjs-plugin-deferred": "2.0.0",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
@@ -40,10 +41,8 @@
|
||||
"datatables.net-buttons-bs": "1.7.1",
|
||||
"datatables.net-select-bs": "1.3.1",
|
||||
"daterangepicker": "3.1.0",
|
||||
"@fortawesome/fontawesome-free": "6.7.2",
|
||||
"hammerjs": "2.0.8",
|
||||
"icheck-bootstrap": "3.0.1",
|
||||
"icheck-material": "1.0.1",
|
||||
"jquery": "3.7.1",
|
||||
"modernized-waitme": "1.0.0",
|
||||
"moment": "2.30.1",
|
||||
@@ -76,6 +75,9 @@
|
||||
"trailingComma": "es5"
|
||||
},
|
||||
"xo": {
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
},
|
||||
"envs": [
|
||||
"browser",
|
||||
"jquery"
|
||||
@@ -83,10 +85,6 @@
|
||||
"extends": [
|
||||
"plugin:compat/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "script"
|
||||
},
|
||||
"prettier": true,
|
||||
"space": 2,
|
||||
"ignores": [
|
||||
@@ -108,37 +106,17 @@
|
||||
],
|
||||
"no-alert": "off",
|
||||
"no-console": "error",
|
||||
"no-else-return": "off",
|
||||
"no-negated-condition": "off",
|
||||
"no-var": "off",
|
||||
"object-shorthand": "off",
|
||||
"one-var": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
"prefer-arrow-callback": "error",
|
||||
"spaced-comment": "off",
|
||||
"unicorn/explicit-length-check": [
|
||||
"error",
|
||||
{
|
||||
"non-zero": "greater-than"
|
||||
}
|
||||
],
|
||||
"unicorn/filename-case": "off",
|
||||
"strict": "error",
|
||||
"unicorn/no-anonymous-default-export": "off",
|
||||
"unicorn/no-array-for-each": "off",
|
||||
"unicorn/no-for-loop": "off",
|
||||
"unicorn/no-document-cookie": "off",
|
||||
"unicorn/numeric-separators-style": "off",
|
||||
"unicorn/prefer-includes": "off",
|
||||
"unicorn/no-negated-condition": "off",
|
||||
"unicorn/prefer-module": "off",
|
||||
"unicorn/prefer-node-append": "off",
|
||||
"unicorn/prefer-number-properties": "off",
|
||||
"unicorn/prefer-query-selector": "off",
|
||||
"unicorn/prefer-string-slice": "off",
|
||||
"unicorn/prevent-abbreviations": "off",
|
||||
"unicorn/prefer-logical-operator-over-ternary": "off",
|
||||
"unicorn/switch-case-braces": "off",
|
||||
"unicorn/no-negated-condition": "off",
|
||||
"logical-assignment-operators": "off",
|
||||
"prefer-object-has-own": "off"
|
||||
"unicorn/switch-case-braces": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-16
@@ -45,17 +45,17 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box box-warning collapsed-box box-solid">
|
||||
<div class="box-header with-border">
|
||||
<button type="button" class="btn btn-box-tool btn-block" data-widget="collapse">
|
||||
<h3 class="box-title pull-left">Advanced filtering</h3>
|
||||
<div class="pull-right">
|
||||
<div class="box box-info collapsed-box box-solid">
|
||||
<div class="box-header with-border pointer no-user-select p-4" data-widget="collapse">
|
||||
<h3 class="box-title">Advanced filtering</h3>
|
||||
<div class="box-tools pull-right">
|
||||
<button type="button" class="btn btn-box-tool">
|
||||
<i class="fa fa-plus"></i>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="box-body" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<div class="input-group">
|
||||
@@ -72,7 +72,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12">
|
||||
<div class="form-group">
|
||||
<label>Domain</label>
|
||||
<label>Domain*</label>
|
||||
<select id="domain_filter" class="form-control" placeholder="">
|
||||
<option disabled selected>Loading...</option>
|
||||
</select>
|
||||
@@ -80,7 +80,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12">
|
||||
<div class="form-group">
|
||||
<label>Client (by IP)</label>
|
||||
<label>Client (by IP)*</label>
|
||||
<select id="client_ip_filter" class="form-control" placeholder="">
|
||||
<option disabled selected>Loading...</option>
|
||||
</select>
|
||||
@@ -88,7 +88,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12">
|
||||
<div class="form-group">
|
||||
<label>Client (by name)</label>
|
||||
<label>Client (by name)*</label>
|
||||
<select id="client_name_filter" class="form-control" placeholder="">
|
||||
<option disabled selected>Loading...</option>
|
||||
</select>
|
||||
@@ -96,7 +96,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12">
|
||||
<div class="form-group">
|
||||
<label>Upstream</label>
|
||||
<label>Upstream*</label>
|
||||
<select id="upstream_filter" class="form-control" placeholder="">
|
||||
<option disabled selected>Loading...</option>
|
||||
</select>
|
||||
@@ -138,6 +138,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
<div class="box-footer clearfix">
|
||||
<span class="pull-left">* These input fields allow manual input as well. Use <code>*</code> for wildcard search.</span>
|
||||
<span class="pull-right">Click "Refresh" below to apply.</span>
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
@@ -194,9 +195,9 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-select/bootstrap-select.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/daterangepicker/daterangepicker.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/queries.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/ip-address-sorting.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-select/bootstrap-select.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/daterangepicker/daterangepicker.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/queries.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/ip-address-sorting.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+62
-56
@@ -5,10 +5,12 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global upstreams */
|
||||
/* global upstreams:false */
|
||||
|
||||
"use strict";
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var THEME_COLORS = [
|
||||
const THEME_COLORS = [
|
||||
"#f56954",
|
||||
"#3c8dbc",
|
||||
"#00a65a",
|
||||
@@ -57,7 +59,7 @@ const htmlLegendPlugin = {
|
||||
ul.firstChild.remove();
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
for (const item of items) {
|
||||
const li = document.createElement("li");
|
||||
li.style.alignItems = "center";
|
||||
li.style.cursor = "pointer";
|
||||
@@ -110,18 +112,18 @@ const htmlLegendPlugin = {
|
||||
textLink.style.padding = 0;
|
||||
textLink.style.textDecoration = item.hidden ? "line-through" : "";
|
||||
textLink.className = "legend-label-text";
|
||||
textLink.append(item.text);
|
||||
textLink.textContent = item.text;
|
||||
|
||||
li.append(boxSpan, textLink);
|
||||
ul.append(li);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var customTooltips = function (context) {
|
||||
var tooltip = context.tooltip;
|
||||
var tooltipEl = document.getElementById(this.chart.canvas.id + "-customTooltip");
|
||||
const customTooltips = function (context) {
|
||||
const tooltip = context.tooltip;
|
||||
let tooltipEl = document.getElementById(this.chart.canvas.id + "-customTooltip");
|
||||
if (!tooltipEl) {
|
||||
// Create Tooltip Element once per chart
|
||||
tooltipEl = document.createElement("div");
|
||||
@@ -130,7 +132,7 @@ var customTooltips = function (context) {
|
||||
tooltipEl.innerHTML = "<div class='arrow'></div> <table></table>";
|
||||
// avoid browser's font-zoom since we know that <body>'s
|
||||
// font-size was set to 14px by bootstrap's css
|
||||
var fontZoom = parseFloat($("body").css("font-size")) / 14;
|
||||
const fontZoom = Number.parseFloat($("body").css("font-size")) / 14;
|
||||
// set styles and font
|
||||
tooltipEl.style.padding = tooltip.options.padding + "px " + tooltip.options.padding + "px";
|
||||
tooltipEl.style.borderRadius = tooltip.options.cornerRadius + "px";
|
||||
@@ -155,56 +157,58 @@ var customTooltips = function (context) {
|
||||
|
||||
// Set Text
|
||||
if (tooltip.body) {
|
||||
var titleLines = tooltip.title || [];
|
||||
var bodyLines = tooltip.body.map(function (bodyItem) {
|
||||
return bodyItem.lines;
|
||||
});
|
||||
var innerHtml = "<thead>";
|
||||
const titleLines = tooltip.title || [];
|
||||
const bodyLines = tooltip.body.map(bodyItem => bodyItem.lines);
|
||||
let innerHtml = "<thead>";
|
||||
|
||||
titleLines.forEach(function (title) {
|
||||
for (const title of titleLines) {
|
||||
innerHtml += "<tr><th>" + title + "</th></tr>";
|
||||
});
|
||||
innerHtml += "</thead><tbody>";
|
||||
var printed = 0;
|
||||
}
|
||||
|
||||
var devicePixel = (1 / window.devicePixelRatio).toFixed(1);
|
||||
bodyLines.forEach(function (body, i) {
|
||||
var labelColors = tooltip.labelColors[i];
|
||||
var style = "background-color: " + labelColors.backgroundColor;
|
||||
innerHtml += "</thead><tbody>";
|
||||
let printed = 0;
|
||||
|
||||
const devicePixel = (1 / window.devicePixelRatio).toFixed(1);
|
||||
for (const [i, body] of bodyLines.entries()) {
|
||||
const labelColors = tooltip.labelColors[i];
|
||||
let style = "background-color: " + labelColors.backgroundColor;
|
||||
style += "; outline: 1px solid " + labelColors.backgroundColor;
|
||||
style += "; border: " + devicePixel + "px solid #fff";
|
||||
var span = "<span class='chartjs-tooltip-key' style='" + style + "'></span>";
|
||||
const span = "<span class='chartjs-tooltip-key' style='" + style + "'></span>";
|
||||
|
||||
var num = body[0].split(": ");
|
||||
const num = body[0].split(": ");
|
||||
// do not display entries with value of 0 (in bar chart),
|
||||
// but pass through entries with "0.0% (in pie charts)
|
||||
if (num[1] !== "0") {
|
||||
innerHtml += "<tr><td>" + span + body + "</td></tr>";
|
||||
printed++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (printed < 1) {
|
||||
innerHtml += "<tr><td>No activity recorded</td></tr>";
|
||||
}
|
||||
|
||||
innerHtml += "</tbody>";
|
||||
|
||||
var tableRoot = tooltipEl.querySelector("table");
|
||||
const tableRoot = tooltipEl.querySelector("table");
|
||||
tableRoot.innerHTML = innerHtml;
|
||||
}
|
||||
|
||||
var canvasPos = this.chart.canvas.getBoundingClientRect();
|
||||
var boxPos = tooltipEl.ancestor.getBoundingClientRect();
|
||||
var offsetX = canvasPos.left - boxPos.left;
|
||||
var offsetY = canvasPos.top - boxPos.top;
|
||||
var tooltipWidth = tooltipEl.offsetWidth;
|
||||
var tooltipHeight = tooltipEl.offsetHeight;
|
||||
var caretX = tooltip.caretX;
|
||||
var caretY = tooltip.caretY;
|
||||
var caretPadding = tooltip.options.caretPadding;
|
||||
var tooltipX, tooltipY, arrowX;
|
||||
var arrowMinIndent = 2 * tooltip.options.cornerRadius;
|
||||
var arrowSize = 5;
|
||||
const canvasPos = this.chart.canvas.getBoundingClientRect();
|
||||
const boxPos = tooltipEl.ancestor.getBoundingClientRect();
|
||||
const offsetX = canvasPos.left - boxPos.left;
|
||||
const offsetY = canvasPos.top - boxPos.top;
|
||||
const tooltipWidth = tooltipEl.offsetWidth;
|
||||
const tooltipHeight = tooltipEl.offsetHeight;
|
||||
const caretX = tooltip.caretX;
|
||||
const caretY = tooltip.caretY;
|
||||
const caretPadding = tooltip.options.caretPadding;
|
||||
let tooltipX;
|
||||
let tooltipY;
|
||||
let arrowX;
|
||||
const arrowMinIndent = 2 * tooltip.options.cornerRadius;
|
||||
const arrowSize = 5;
|
||||
|
||||
// Compute X position
|
||||
if ($(document).width() > 2 * tooltip.width || tooltip.xAlign !== "center") {
|
||||
@@ -212,10 +216,10 @@ var customTooltips = function (context) {
|
||||
tooltipX = offsetX + caretX;
|
||||
if (tooltip.yAlign === "top" || tooltip.yAlign === "bottom") {
|
||||
switch (tooltip.xAlign) {
|
||||
case "center":
|
||||
case "center": {
|
||||
// set a minimal X position to 5px to prevent
|
||||
// the tooltip to stick out left of the viewport
|
||||
var minX = 5;
|
||||
const minX = 5;
|
||||
if (2 * tooltipX < tooltipWidth + minX) {
|
||||
arrowX = tooltipX - minX;
|
||||
tooltipX = minX;
|
||||
@@ -224,6 +228,8 @@ var customTooltips = function (context) {
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "left":
|
||||
tooltipX -= arrowMinIndent;
|
||||
arrowX = arrowMinIndent;
|
||||
@@ -295,7 +301,7 @@ var customTooltips = function (context) {
|
||||
} else {
|
||||
// Calculate percentage X value depending on the tooltip's
|
||||
// width to avoid hanging arrow out on tooltip width changes
|
||||
var arrowXpercent = ((100 / tooltipWidth) * arrowX).toFixed(1);
|
||||
const arrowXpercent = ((100 / tooltipWidth) * arrowX).toFixed(1);
|
||||
tooltipEl.querySelector(".arrow").style.left = arrowXpercent + "%";
|
||||
}
|
||||
|
||||
@@ -304,12 +310,12 @@ var customTooltips = function (context) {
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function doughnutTooltip(tooltipLabel) {
|
||||
var percentageTotalShown = tooltipLabel.chart._metasets[0].total.toFixed(1);
|
||||
let percentageTotalShown = tooltipLabel.chart._metasets[0].total.toFixed(1);
|
||||
// tooltipLabel.chart._metasets[0].total returns the total percentage of the shown slices
|
||||
// to compensate rounding errors we round to one decimal
|
||||
|
||||
var label = " " + tooltipLabel.label;
|
||||
var itemPercentage;
|
||||
const label = " " + tooltipLabel.label;
|
||||
let itemPercentage;
|
||||
|
||||
// if we only show < 1% percent of all, show each item with two decimals
|
||||
if (percentageTotalShown < 1) {
|
||||
@@ -326,19 +332,19 @@ function doughnutTooltip(tooltipLabel) {
|
||||
if (percentageTotalShown > 99.9) {
|
||||
// All items shown
|
||||
return label + ": " + itemPercentage + "%";
|
||||
} else {
|
||||
// set percentageTotalShown again without rounding to account
|
||||
// for cases where the total shown percentage would be <0.1% of all
|
||||
percentageTotalShown = tooltipLabel.chart._metasets[0].total;
|
||||
return (
|
||||
label +
|
||||
":<br>• " +
|
||||
itemPercentage +
|
||||
"% of all data<br>• " +
|
||||
((tooltipLabel.parsed * 100) / percentageTotalShown).toFixed(1) +
|
||||
"% of shown items"
|
||||
);
|
||||
}
|
||||
|
||||
// set percentageTotalShown again without rounding to account
|
||||
// for cases where the total shown percentage would be <0.1% of all
|
||||
percentageTotalShown = tooltipLabel.chart._metasets[0].total;
|
||||
return (
|
||||
label +
|
||||
":<br>• " +
|
||||
itemPercentage +
|
||||
"% of all data<br>• " +
|
||||
((tooltipLabel.parsed * 100) / percentageTotalShown).toFixed(1) +
|
||||
"% of shown items"
|
||||
);
|
||||
}
|
||||
|
||||
// chartjs plugin used by the custom doughnut legend
|
||||
|
||||
+114
-148
@@ -7,31 +7,32 @@
|
||||
|
||||
/* global utils:false, moment:false */
|
||||
|
||||
var _isLoginPage = false;
|
||||
const apiUrl = document.body.dataset.apiurl;
|
||||
"use strict";
|
||||
|
||||
globalThis._isLoginPage = false;
|
||||
|
||||
const REFRESH_INTERVAL = {
|
||||
logs: 500, // 0.5 sec (logs page)
|
||||
summary: 1000, // 1 sec (dashboard)
|
||||
query_log: 2000, // 2 sec (Query Log)
|
||||
blocking: 10000, // 10 sec (all pages, sidebar)
|
||||
metrics: 10000, // 10 sec (settings page)
|
||||
system: 20000, // 20 sec (all pages, sidebar)
|
||||
query_types: 60000, // 1 min (dashboard)
|
||||
upstreams: 60000, // 1 min (dashboard)
|
||||
top_lists: 60000, // 1 min (dashboard)
|
||||
messages: 60000, // 1 min (all pages)
|
||||
version: 120000, // 2 min (all pages, footer)
|
||||
ftl: 120000, // 2 min (all pages, sidebar)
|
||||
hosts: 120000, // 2 min (settings page)
|
||||
history: 600000, // 10 min (dashboard)
|
||||
clients: 600000, // 10 min (dashboard)
|
||||
blocking: 10_000, // 10 sec (all pages, sidebar)
|
||||
metrics: 10_000, // 10 sec (settings page)
|
||||
system: 20_000, // 20 sec (all pages, sidebar)
|
||||
query_types: 60_000, // 1 min (dashboard)
|
||||
upstreams: 60_000, // 1 min (dashboard)
|
||||
top_lists: 60_000, // 1 min (dashboard)
|
||||
messages: 60_000, // 1 min (all pages)
|
||||
version: 120_000, // 2 min (all pages, footer)
|
||||
ftl: 120_000, // 2 min (all pages, sidebar)
|
||||
hosts: 120_000, // 2 min (settings page)
|
||||
history: 600_000, // 10 min (dashboard)
|
||||
clients: 600_000, // 10 min (dashboard)
|
||||
};
|
||||
|
||||
function secondsTimeSpanToHMS(s) {
|
||||
var h = Math.floor(s / 3600); //Get whole hours
|
||||
const h = Math.floor(s / 3600); //Get whole hours
|
||||
s -= h * 3600;
|
||||
var m = Math.floor(s / 60); //Get remaining minutes
|
||||
const m = Math.floor(s / 60); //Get remaining minutes
|
||||
s -= m * 60;
|
||||
return h + ":" + (m < 10 ? "0" + m : m) + ":" + (s < 10 ? "0" + s : s); //zero padding on minutes and seconds
|
||||
}
|
||||
@@ -42,8 +43,8 @@ function piholeChanged(blocking, timer = null) {
|
||||
const dis = $("#pihole-disable");
|
||||
const enaT = $("#enableTimer");
|
||||
|
||||
if (timer !== null && parseFloat(timer) > 0) {
|
||||
enaT.html(Date.now() + parseFloat(timer) * 1000);
|
||||
if (timer !== null && Number.parseFloat(timer) > 0) {
|
||||
enaT.text(Date.now() + Number.parseFloat(timer) * 1000);
|
||||
setTimeout(countDown, 100);
|
||||
}
|
||||
|
||||
@@ -84,10 +85,10 @@ function piholeChanged(blocking, timer = null) {
|
||||
}
|
||||
|
||||
function countDown() {
|
||||
var ena = $("#enableLabel");
|
||||
var enaT = $("#enableTimer");
|
||||
var target = new Date(parseInt(enaT.text(), 10));
|
||||
var seconds = Math.round((target.getTime() - Date.now()) / 1000);
|
||||
const ena = $("#enableLabel");
|
||||
const enaT = $("#enableTimer");
|
||||
const target = new Date(Number.parseInt(enaT.text(), 10));
|
||||
const seconds = Math.round((target.getTime() - Date.now()) / 1000);
|
||||
|
||||
//Stop and remove timer when user enabled early
|
||||
if ($("#pihole-enable").is(":hidden")) {
|
||||
@@ -115,14 +116,14 @@ function checkBlocking() {
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl + "/dns/blocking",
|
||||
url: document.body.dataset.apiurl + "/dns/blocking",
|
||||
method: "GET",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
piholeChanged(data.blocking, data.timer);
|
||||
utils.setTimer(checkBlocking, REFRESH_INTERVAL.blocking);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
utils.setTimer(checkBlocking, 3 * REFRESH_INTERVAL.blocking);
|
||||
});
|
||||
@@ -144,23 +145,23 @@ function piholeChange(action, duration) {
|
||||
|
||||
btnStatus.html("<i class='fa fa-spinner fa-spin'> </i>");
|
||||
$.ajax({
|
||||
url: apiUrl + "/dns/blocking",
|
||||
url: document.body.dataset.apiurl + "/dns/blocking",
|
||||
method: "POST",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
blocking: action === "enable",
|
||||
timer: parseInt(duration, 10) > 0 ? parseInt(duration, 10) : null,
|
||||
timer: Number.parseInt(duration, 10) > 0 ? Number.parseInt(duration, 10) : null,
|
||||
}),
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
if (data.blocking === action + "d") {
|
||||
btnStatus.html("");
|
||||
piholeChanged(data.blocking, data.timer);
|
||||
}
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
@@ -172,7 +173,7 @@ function testCookies() {
|
||||
|
||||
// set and read cookie
|
||||
document.cookie = "cookietest=1";
|
||||
var ret = document.cookie.indexOf("cookietest=") !== -1;
|
||||
const ret = document.cookie.includes("cookietest=");
|
||||
|
||||
// delete cookie
|
||||
document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
|
||||
@@ -180,52 +181,21 @@ function testCookies() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
var iCheckStyle = "primary";
|
||||
function applyCheckboxRadioStyle() {
|
||||
// Get all radio/checkboxes for theming, with the exception of the two radio buttons on the custom disable timer,
|
||||
// as well as every element with an id that starts with "status_"
|
||||
var sel = $("input[type='radio'],input[type='checkbox']")
|
||||
const sel = $("input[type='radio'],input[type='checkbox']")
|
||||
.not("#selSec")
|
||||
.not("#selMin")
|
||||
.not("#expert-settings")
|
||||
.not("#only-changed")
|
||||
.not("[id^=status_]");
|
||||
sel.parent().removeClass();
|
||||
sel.parent().addClass("icheck-" + iCheckStyle);
|
||||
sel.parent().addClass("icheck-primary");
|
||||
}
|
||||
|
||||
function initCheckboxRadioStyle() {
|
||||
function getCheckboxURL(style) {
|
||||
var extra = style.startsWith("material-") ? "material" : "bootstrap";
|
||||
return "/admin/vendor/icheck/icheck-" + extra + ".min.css";
|
||||
}
|
||||
|
||||
// Read from local storage, initialize if needed
|
||||
var chkboxStyle = localStorage ? localStorage.getItem("theme_icheck") : null;
|
||||
if (chkboxStyle === null) {
|
||||
chkboxStyle = "primary";
|
||||
}
|
||||
|
||||
var boxsheet = $('<link href="' + getCheckboxURL(chkboxStyle) + '" rel="stylesheet">');
|
||||
// Only add the stylesheet if it's not already present
|
||||
if ($("link[href='" + boxsheet.attr("href") + "']").length === 0) boxsheet.appendTo("head");
|
||||
|
||||
iCheckStyle = chkboxStyle;
|
||||
applyCheckboxRadioStyle();
|
||||
|
||||
// Add handler when on settings page
|
||||
var iCheckStyle = $("#iCheckStyle");
|
||||
if (iCheckStyle !== null) {
|
||||
iCheckStyle.val(chkboxStyle);
|
||||
iCheckStyle.on("change", function () {
|
||||
var themename = $(this).val();
|
||||
localStorage.setItem("theme_icheck", themename);
|
||||
applyCheckboxRadioStyle();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var systemTimer, versionTimer;
|
||||
let systemTimer;
|
||||
let versionTimer;
|
||||
function updateInfo() {
|
||||
updateSystemInfo();
|
||||
updateVersionInfo();
|
||||
@@ -234,7 +204,7 @@ function updateInfo() {
|
||||
}
|
||||
|
||||
function updateQueryFrequency(intl, frequency) {
|
||||
let freq = parseFloat(frequency) * 60;
|
||||
let freq = Number.parseFloat(frequency) * 60;
|
||||
let unit = "q/min";
|
||||
let title = "Queries per minute";
|
||||
if (freq > 100) {
|
||||
@@ -264,15 +234,15 @@ function updateQueryFrequency(intl, frequency) {
|
||||
.attr("title", title);
|
||||
}
|
||||
|
||||
var ftlinfoTimer = null;
|
||||
let ftlinfoTimer = null;
|
||||
function updateFtlInfo() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/info/ftl",
|
||||
url: document.body.dataset.apiurl + "/info/ftl",
|
||||
})
|
||||
.done(function (data) {
|
||||
var ftl = data.ftl;
|
||||
var database = ftl.database;
|
||||
var intl = new Intl.NumberFormat();
|
||||
.done(data => {
|
||||
const ftl = data.ftl;
|
||||
const database = ftl.database;
|
||||
const intl = new Intl.NumberFormat();
|
||||
$("#num_groups").text(intl.format(database.groups));
|
||||
$("#num_clients").text(intl.format(database.clients));
|
||||
$("#num_lists").text(intl.format(database.lists));
|
||||
@@ -301,12 +271,10 @@ function updateFtlInfo() {
|
||||
$("#sysinfo-cpu-ftl").text("(" + ftl["%cpu"].toFixed(1) + "% used by FTL)");
|
||||
$("#sysinfo-ram-ftl").text("(" + ftl["%mem"].toFixed(1) + "% used by FTL)");
|
||||
$("#sysinfo-pid-ftl").text(ftl.pid);
|
||||
var startdate = moment()
|
||||
const startdate = moment()
|
||||
.subtract(ftl.uptime, "milliseconds")
|
||||
.format("dddd, MMMM Do YYYY, HH:mm:ss");
|
||||
$("#sysinfo-uptime-ftl").text(startdate);
|
||||
$("#sysinfo-privacy_level").text(ftl.privacy_level);
|
||||
$("#sysinfo-ftl-overlay").hide();
|
||||
|
||||
$(".destructive_action").prop("disabled", !ftl.allow_destructive);
|
||||
$(".destructive_action").prop(
|
||||
@@ -317,19 +285,19 @@ function updateFtlInfo() {
|
||||
clearTimeout(ftlinfoTimer);
|
||||
ftlinfoTimer = utils.setTimer(updateFtlInfo, REFRESH_INTERVAL.ftl);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
function updateSystemInfo() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/info/system",
|
||||
url: document.body.dataset.apiurl + "/info/system",
|
||||
})
|
||||
.done(function (data) {
|
||||
var system = data.system;
|
||||
var percentRAM = system.memory.ram["%used"];
|
||||
var percentSwap = system.memory.swap["%used"];
|
||||
.done(data => {
|
||||
const system = data.system;
|
||||
const percentRAM = system.memory.ram["%used"];
|
||||
const percentSwap = system.memory.swap["%used"];
|
||||
let totalRAM = system.memory.ram.total / 1024;
|
||||
let totalRAMUnit = "MB";
|
||||
if (totalRAM > 1024) {
|
||||
@@ -344,11 +312,11 @@ function updateSystemInfo() {
|
||||
totalSwapUnit = "GB";
|
||||
}
|
||||
|
||||
var swap =
|
||||
const swap =
|
||||
system.memory.swap.total > 0
|
||||
? ((1e2 * system.memory.swap.used) / system.memory.swap.total).toFixed(1) + " %"
|
||||
: "N/A";
|
||||
var color;
|
||||
let color;
|
||||
color = percentRAM > 75 ? "text-red" : "text-green-light";
|
||||
$("#memory").html(
|
||||
'<i class="fa fa-fw fa-memory ' +
|
||||
@@ -407,7 +375,7 @@ function updateSystemInfo() {
|
||||
" processes"
|
||||
);
|
||||
|
||||
var startdate = moment()
|
||||
const startdate = moment()
|
||||
.subtract(system.uptime, "seconds")
|
||||
.format("dddd, MMMM Do YYYY, HH:mm:ss");
|
||||
$("#status").prop(
|
||||
@@ -426,7 +394,7 @@ function updateSystemInfo() {
|
||||
clearTimeout(systemTimer);
|
||||
systemTimer = utils.setTimer(updateSystemInfo, REFRESH_INTERVAL.system);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
@@ -443,8 +411,8 @@ function apiFailure(data) {
|
||||
// Credits: https://www.geeksforgeeks.org/compare-two-version-numbers/
|
||||
function versionCompare(v1, v2) {
|
||||
// vnum stores each numeric part of version
|
||||
var vnum1 = 0,
|
||||
vnum2 = 0;
|
||||
let vnum1 = 0;
|
||||
let vnum2 = 0;
|
||||
|
||||
// Remove possible leading "v" in v1 and v2
|
||||
if (v1[0] === "v") {
|
||||
@@ -456,7 +424,7 @@ function versionCompare(v1, v2) {
|
||||
}
|
||||
|
||||
// loop until both string are processed
|
||||
for (var i = 0, j = 0; i < v1.length || j < v2.length; ) {
|
||||
for (let i = 0, j = 0; i < v1.length || j < v2.length; ) {
|
||||
// storing numeric part of version 1 in vnum1
|
||||
while (i < v1.length && v1[i] !== ".") {
|
||||
vnum1 = vnum1 * 10 + (v1[i] - "0");
|
||||
@@ -484,15 +452,16 @@ function versionCompare(v1, v2) {
|
||||
|
||||
function updateVersionInfo() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/info/version",
|
||||
}).done(function (data) {
|
||||
var version = data.version;
|
||||
var updateAvailable = false;
|
||||
var dockerUpdate = false;
|
||||
var isDocker = false;
|
||||
url: document.body.dataset.apiurl + "/info/version",
|
||||
}).done(data => {
|
||||
const version = data.version;
|
||||
let updateAvailable = false;
|
||||
let dockerUpdate = false;
|
||||
let isDocker = false;
|
||||
$("#versions").empty();
|
||||
$("#update-hint").empty();
|
||||
|
||||
var versions = [
|
||||
const versions = [
|
||||
{
|
||||
name: "Docker Tag",
|
||||
local: version.docker.local,
|
||||
@@ -535,11 +504,11 @@ function updateVersionInfo() {
|
||||
isDocker = true;
|
||||
}
|
||||
|
||||
versions.forEach(function (v) {
|
||||
for (const v of versions) {
|
||||
if (v.local !== null) {
|
||||
// reset update status for each component
|
||||
var updateComponentAvailable = false;
|
||||
var localVersion = v.local;
|
||||
let updateComponentAvailable = false;
|
||||
let localVersion = v.local;
|
||||
if (v.branch !== null && v.hash !== null) {
|
||||
if (v.branch === "master") {
|
||||
localVersion = v.local.split("-")[0];
|
||||
@@ -548,7 +517,7 @@ function updateVersionInfo() {
|
||||
v.url +
|
||||
"/" +
|
||||
localVersion +
|
||||
'" rel="noopener" target="_blank">' +
|
||||
'" rel="noopener noreferrer" target="_blank">' +
|
||||
localVersion +
|
||||
"</a>";
|
||||
if (versionCompare(v.local, v.remote) === -1) {
|
||||
@@ -579,7 +548,7 @@ function updateVersionInfo() {
|
||||
v.url +
|
||||
"/" +
|
||||
localVersion +
|
||||
'" rel="noopener" target="_blank">' +
|
||||
'" rel="noopener noreferrer" target="_blank">' +
|
||||
localVersion +
|
||||
"</a>";
|
||||
}
|
||||
@@ -594,7 +563,7 @@ function updateVersionInfo() {
|
||||
localVersion +
|
||||
' · <a class="lookatme" lookatme-text="Update available!" href="' +
|
||||
v.url +
|
||||
'" rel="noopener" target="_blank">Update available!</a></li>'
|
||||
'" rel="noopener noreferrer" target="_blank">Update available!</a></li>'
|
||||
);
|
||||
// if at least one component can be updated, display the update-hint footer
|
||||
updateAvailable = true;
|
||||
@@ -602,15 +571,15 @@ function updateVersionInfo() {
|
||||
$("#versions").append("<li><strong>" + v.name + "</strong> " + localVersion + "</li>");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (dockerUpdate)
|
||||
$("#update-hint").html(
|
||||
'To install updates, <a href="https://github.com/pi-hole/docker-pi-hole#upgrading-persistence-and-customizations" rel="noopener" target="_blank">replace this old container with a fresh upgraded image</a>.'
|
||||
'To install updates, <a href="https://github.com/pi-hole/docker-pi-hole#upgrading-persistence-and-customizations" rel="noopener noreferrer" target="_blank">replace this old container with a fresh upgraded image</a>.'
|
||||
);
|
||||
else if (updateAvailable)
|
||||
$("#update-hint").html(
|
||||
'To install updates, run <code><a href="https://docs.pi-hole.net/main/update/" rel="noopener" target="_blank">pihole -up</a></code>.'
|
||||
'To install updates, run <code><a href="https://docs.pi-hole.net/main/update/" rel="noopener noreferrer" target="_blank">pihole -up</a></code>.'
|
||||
);
|
||||
|
||||
clearTimeout(versionTimer);
|
||||
@@ -618,11 +587,11 @@ function updateVersionInfo() {
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
if (!_isLoginPage) updateInfo();
|
||||
var enaT = $("#enableTimer");
|
||||
var target = new Date(parseInt(enaT.html(), 10));
|
||||
var seconds = Math.round((target.getTime() - Date.now()) / 1000);
|
||||
$(() => {
|
||||
if (!globalThis._isLoginPage) updateInfo();
|
||||
const enaT = $("#enableTimer");
|
||||
const target = new Date(Number.parseInt(enaT.text(), 10));
|
||||
const seconds = Math.round((target.getTime() - Date.now()) / 1000);
|
||||
if (seconds > 0) {
|
||||
setTimeout(countDown, 100);
|
||||
}
|
||||
@@ -631,10 +600,10 @@ $(function () {
|
||||
$("#cookieInfo").show();
|
||||
}
|
||||
|
||||
// Apply per-browser styling settings
|
||||
initCheckboxRadioStyle();
|
||||
// Apply icheckbox/iradio style
|
||||
applyCheckboxRadioStyle();
|
||||
|
||||
if (!_isLoginPage) {
|
||||
if (!globalThis._isLoginPage) {
|
||||
// Run check immediately after page loading ...
|
||||
utils.checkMessages();
|
||||
// ... and then periodically
|
||||
@@ -643,52 +612,53 @@ $(function () {
|
||||
});
|
||||
|
||||
// Handle Enable/Disable
|
||||
$("#pihole-enable").on("click", function (e) {
|
||||
$("#pihole-enable").on("click", e => {
|
||||
e.preventDefault();
|
||||
localStorage.removeItem("countDownTarget");
|
||||
piholeChange("enable", "");
|
||||
});
|
||||
$("#pihole-disable-indefinitely").on("click", function (e) {
|
||||
$("#pihole-disable-indefinitely").on("click", e => {
|
||||
e.preventDefault();
|
||||
piholeChange("disable", "0");
|
||||
});
|
||||
$("#pihole-disable-10s").on("click", function (e) {
|
||||
$("#pihole-disable-10s").on("click", e => {
|
||||
e.preventDefault();
|
||||
piholeChange("disable", "10");
|
||||
});
|
||||
$("#pihole-disable-30s").on("click", function (e) {
|
||||
$("#pihole-disable-30s").on("click", e => {
|
||||
e.preventDefault();
|
||||
piholeChange("disable", "30");
|
||||
});
|
||||
$("#pihole-disable-5m").on("click", function (e) {
|
||||
$("#pihole-disable-5m").on("click", e => {
|
||||
e.preventDefault();
|
||||
piholeChange("disable", "300");
|
||||
});
|
||||
$("#pihole-disable-custom").on("click", function (e) {
|
||||
$("#pihole-disable-custom").on("click", e => {
|
||||
e.preventDefault();
|
||||
var custVal = $("#customTimeout").val();
|
||||
let custVal = $("#customTimeout").val();
|
||||
custVal = $("#btnMins").hasClass("active") ? custVal * 60 : custVal;
|
||||
piholeChange("disable", custVal);
|
||||
});
|
||||
|
||||
function initSettingsLevel() {
|
||||
const elem = $("#expert-settings");
|
||||
// Apply expert settings level
|
||||
applyExpertSettings();
|
||||
|
||||
const expertSettingsElement = document.getElementById("expert-settings");
|
||||
|
||||
// Skip init if element is not present (e.g. on login page)
|
||||
if (elem.length === 0) {
|
||||
applyExpertSettings();
|
||||
return;
|
||||
}
|
||||
if (!expertSettingsElement) return;
|
||||
|
||||
// Restore settings level from local storage (if available) or default to "false"
|
||||
if (localStorage.getItem("expert_settings") === null) {
|
||||
const storedExpertSettings = localStorage.getItem("expert_settings");
|
||||
if (storedExpertSettings === null) {
|
||||
localStorage.setItem("expert_settings", "false");
|
||||
}
|
||||
|
||||
elem.prop("checked", localStorage.getItem("expert_settings") === "true");
|
||||
expertSettingsElement.checked = storedExpertSettings === "true";
|
||||
|
||||
// Init the settings level toggle
|
||||
elem.bootstrapToggle({
|
||||
$(expertSettingsElement).bootstrapToggle({
|
||||
on: "Expert",
|
||||
off: "Basic",
|
||||
size: "small",
|
||||
@@ -698,27 +668,23 @@ function initSettingsLevel() {
|
||||
});
|
||||
|
||||
// Add handler for settings level toggle
|
||||
elem.on("change", function () {
|
||||
localStorage.setItem("expert_settings", $(this).prop("checked") ? "true" : "false");
|
||||
$(expertSettingsElement).on("change", event => {
|
||||
localStorage.setItem("expert_settings", event.currentTarget.checked ? "true" : "false");
|
||||
applyExpertSettings();
|
||||
addAdvancedInfo();
|
||||
});
|
||||
|
||||
// Apply settings level
|
||||
applyExpertSettings();
|
||||
}
|
||||
|
||||
// Apply expert settings level, this will hide/show elements with the class
|
||||
// "settings-level-expert" depending on the current settings level
|
||||
// If "expert_settings" is not set, we default to !"true"
|
||||
function applyExpertSettings() {
|
||||
// Apply settings level, this will hide/show elements with the class
|
||||
// "settings-level-basic" or "settings-level-expert" depending on the
|
||||
// current settings level
|
||||
// If "expert_settings" is not set, we default to !"true" (basic settings)
|
||||
const expertSettingsNodes = document.querySelectorAll(".settings-level-expert");
|
||||
if (expertSettingsNodes.length === 0) return;
|
||||
|
||||
if (localStorage.getItem("expert_settings") === "true") {
|
||||
$(".settings-level-basic").show();
|
||||
$(".settings-level-expert").show();
|
||||
for (const element of expertSettingsNodes) element.classList.remove("d-none");
|
||||
} else {
|
||||
$(".settings-level-basic").show();
|
||||
$(".settings-level-expert").hide();
|
||||
for (const element of expertSettingsNodes) element.classList.toggle("d-none", true);
|
||||
|
||||
// If we left with an empty page (no visible boxes) after switching from
|
||||
// Expert to Basic settings, redirect to admin/settings/system instead
|
||||
@@ -727,8 +693,8 @@ function applyExpertSettings() {
|
||||
// settings page as well, even when the button has "only modified"
|
||||
// functionality there), and
|
||||
// - there are no visible boxes (the page is empty)
|
||||
if ($(".settings-selector").length > 0 && $(".box:visible").length === 0) {
|
||||
globalThis.location.href = "/admin/settings/system";
|
||||
if (document.querySelector(".settings-selector") && $(".box:visible").length === 0) {
|
||||
globalThis.location.href = `${document.body.dataset.webhome}settings/system`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -738,9 +704,9 @@ function addAdvancedInfo() {
|
||||
const advancedInfoTarget = $("#advanced-info");
|
||||
const isTLS = location.protocol === "https:";
|
||||
const clientIP = advancedInfoSource.data("client-ip");
|
||||
const XForwardedFor = globalThis.atob(advancedInfoSource.data("xff") ?? "");
|
||||
const starttime = parseFloat(advancedInfoSource.data("starttime"));
|
||||
const endtime = parseFloat(advancedInfoSource.data("endtime"));
|
||||
const XForwardedFor = globalThis.atob(advancedInfoSource.data("xff") || "") || null;
|
||||
const starttime = Number.parseFloat(advancedInfoSource.data("starttime"));
|
||||
const endtime = Number.parseFloat(advancedInfoSource.data("endtime"));
|
||||
const totaltime = 1e3 * (endtime - starttime);
|
||||
|
||||
// Show advanced info
|
||||
@@ -756,7 +722,7 @@ function addAdvancedInfo() {
|
||||
);
|
||||
|
||||
// Add client IP info
|
||||
$("#client-id").text(XForwardedFor ? XForwardedFor : clientIP);
|
||||
$("#client-id").text(XForwardedFor ?? clientIP);
|
||||
if (XForwardedFor) {
|
||||
// If X-Forwarded-For is set, show the X-Forwarded-For in italics and add
|
||||
// the real client IP as tooltip
|
||||
@@ -773,7 +739,7 @@ function addAdvancedInfo() {
|
||||
advancedInfoTarget.show();
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
initSettingsLevel();
|
||||
addAdvancedInfo();
|
||||
});
|
||||
|
||||
+15
-22
@@ -4,19 +4,20 @@
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
/* global apiUrl: false */
|
||||
|
||||
"use strict";
|
||||
|
||||
function eventsource() {
|
||||
var alInfo = $("#alInfo");
|
||||
var alSuccess = $("#alSuccess");
|
||||
var ta = $("#output");
|
||||
const alInfo = $("#alInfo");
|
||||
const alSuccess = $("#alSuccess");
|
||||
const ta = $("#output");
|
||||
|
||||
ta.html("");
|
||||
ta.show();
|
||||
alInfo.show();
|
||||
alSuccess.hide();
|
||||
|
||||
fetch(apiUrl + "/action/gravity", {
|
||||
fetch(document.body.dataset.apiurl + "/action/gravity", {
|
||||
method: "POST",
|
||||
headers: { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content") },
|
||||
})
|
||||
@@ -38,10 +39,10 @@ function eventsource() {
|
||||
|
||||
// Enqueue the next data chunk into our target stream
|
||||
controller.enqueue(value);
|
||||
var string = new TextDecoder().decode(value);
|
||||
const string = new TextDecoder().decode(value);
|
||||
parseLines(ta, string);
|
||||
|
||||
if (string.indexOf("Done.") !== -1) {
|
||||
if (string.includes("Done.")) {
|
||||
alSuccess.show();
|
||||
}
|
||||
|
||||
@@ -54,26 +55,18 @@ function eventsource() {
|
||||
.catch(error => console.error(error)); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
$("#gravityBtn").on("click", function () {
|
||||
$("#gravityBtn").on("click", () => {
|
||||
$("#gravityBtn").prop("disabled", true);
|
||||
eventsource();
|
||||
});
|
||||
|
||||
// Handle hiding of alerts
|
||||
$(function () {
|
||||
$(() => {
|
||||
$("[data-hide]").on("click", function () {
|
||||
$(this)
|
||||
.closest("." + $(this).attr("data-hide"))
|
||||
.hide();
|
||||
});
|
||||
|
||||
// Do we want to start updating immediately?
|
||||
// gravity?go
|
||||
var searchString = globalThis.location.search.substring(1);
|
||||
if (searchString.indexOf("go") !== -1) {
|
||||
$("#gravityBtn").prop("disabled", true);
|
||||
eventsource();
|
||||
}
|
||||
});
|
||||
|
||||
function parseLines(ta, str) {
|
||||
@@ -81,18 +74,18 @@ function parseLines(ta, str) {
|
||||
// We want to split the text before an "OVER" escape sequence to allow overwriting previous line when needed
|
||||
|
||||
// Splitting the text on "\r"
|
||||
var lines = str.split(/(?=\r)/g);
|
||||
const lines = str.split(/(?=\r)/g);
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i][0] === "\r") {
|
||||
for (let line of lines) {
|
||||
if (line[0] === "\r") {
|
||||
// This line starts with the "OVER" sequence. Replace them with "\n" before print
|
||||
lines[i] = lines[i].replaceAll("\r[K", "\n").replaceAll("\r", "\n");
|
||||
line = line.replaceAll("\r[K", "\n").replaceAll("\r", "\n");
|
||||
|
||||
// Last line from the textarea will be overwritten, so we remove it
|
||||
ta.text(ta.text().substring(0, ta.text().lastIndexOf("\n")));
|
||||
}
|
||||
|
||||
// Append the new text to the end of the output
|
||||
ta.append(lines[i]);
|
||||
ta.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,20 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, groups:false,, apiFailure:false, updateFtlInfo:false, getGroups:false, processGroupResult:false, delGroupItems:false */
|
||||
/* global utils:false, groups:false, apiFailure:false, updateFtlInfo:false, getGroups:false, processGroupResult:false, delGroupItems:false */
|
||||
/* exported initTable */
|
||||
|
||||
var table;
|
||||
"use strict";
|
||||
|
||||
let table;
|
||||
|
||||
function reloadClientSuggestions() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/clients/_suggestions",
|
||||
url: document.body.dataset.apiurl + "/clients/_suggestions",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
var sel = $("#select");
|
||||
success(data) {
|
||||
const sel = $("#select");
|
||||
sel.empty();
|
||||
|
||||
// In order for the placeholder value to appear, we have to have a blank
|
||||
@@ -27,11 +29,10 @@ function reloadClientSuggestions() {
|
||||
sel.append($("<option />"));
|
||||
|
||||
// Add data obtained from API
|
||||
for (var i = 0; i < data.clients.length; i++) {
|
||||
const client = data.clients[i];
|
||||
for (const client of data.clients) {
|
||||
let mockDevice = false;
|
||||
var text = client.hwaddr.toUpperCase();
|
||||
var key = text;
|
||||
let text = client.hwaddr.toUpperCase();
|
||||
let key = text;
|
||||
if (key.startsWith("IP-")) {
|
||||
// Mock MAC address for address-only devices
|
||||
mockDevice = true;
|
||||
@@ -40,10 +41,10 @@ function reloadClientSuggestions() {
|
||||
}
|
||||
|
||||
// Append additional infos if available
|
||||
var extraInfo = "";
|
||||
let extraInfo = "";
|
||||
if (client.names !== null && client.names.length > 0) {
|
||||
// Count number of "," in client.names to determine number of hostnames
|
||||
var numHostnames = client.names.split(",").length;
|
||||
const numHostnames = client.names.split(",").length;
|
||||
const pluralHostnames = numHostnames > 1 ? "s" : "";
|
||||
extraInfo =
|
||||
numHostnames + " hostname" + pluralHostnames + ": " + utils.escapeHtml(client.names);
|
||||
@@ -59,7 +60,7 @@ function reloadClientSuggestions() {
|
||||
if (client.addresses !== null && client.addresses.length > 0 && !mockDevice) {
|
||||
if (extraInfo.length > 0) extraInfo += "; ";
|
||||
// Count number of "," in client.addresses to determine number of addresses
|
||||
var numAddresses = client.addresses.split(",").length;
|
||||
const numAddresses = client.addresses.split(",").length;
|
||||
const pluralAddresses = numAddresses > 1 ? "es" : "";
|
||||
extraInfo +=
|
||||
numAddresses + " address" + pluralAddresses + ": " + utils.escapeHtml(client.addresses);
|
||||
@@ -73,7 +74,7 @@ function reloadClientSuggestions() {
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
$("#btnAdd").on("click", addClient);
|
||||
$("#select").select2({
|
||||
tags: true,
|
||||
@@ -85,7 +86,7 @@ $(function () {
|
||||
utils.setBsSelectDefaults();
|
||||
getGroups();
|
||||
|
||||
$("#select").on("change", function () {
|
||||
$("#select").on("change", () => {
|
||||
$("#ip-custom").val("");
|
||||
$("#ip-custom").prop("disabled", $("#select option:selected").val() !== "custom");
|
||||
});
|
||||
@@ -96,7 +97,7 @@ function initTable() {
|
||||
table = $("#clientsTable").DataTable({
|
||||
processing: true,
|
||||
ajax: {
|
||||
url: apiUrl + "/clients",
|
||||
url: document.body.dataset.apiurl + "/clients",
|
||||
dataSrc: "clients",
|
||||
type: "GET",
|
||||
},
|
||||
@@ -113,7 +114,7 @@ function initTable() {
|
||||
{
|
||||
targets: 1,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
render() {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
@@ -122,26 +123,26 @@ function initTable() {
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
drawCallback() {
|
||||
// Hide buttons if all clients were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
const hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
$('button[id^="deleteClient_"]').on("click", deleteClient);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var dataId = utils.hexEncode(data.client);
|
||||
rowCallback(row, data) {
|
||||
const dataId = utils.hexEncode(data.client);
|
||||
$(row).attr("data-id", dataId);
|
||||
var tooltip =
|
||||
const tooltip =
|
||||
"Added: " +
|
||||
utils.datetime(data.date_added, false) +
|
||||
"\nLast modified: " +
|
||||
utils.datetime(data.date_modified, false) +
|
||||
"\nDatabase ID: " +
|
||||
data.id;
|
||||
var ipName =
|
||||
let ipName =
|
||||
'<code id="ip_' +
|
||||
dataId +
|
||||
'" title="' +
|
||||
@@ -161,7 +162,7 @@ function initTable() {
|
||||
$("td:eq(1)", row).html(ipName);
|
||||
|
||||
$("td:eq(2)", row).html('<input id="comment_' + dataId + '" class="form-control">');
|
||||
var commentEl = $("#comment_" + dataId, row);
|
||||
const commentEl = $("#comment_" + dataId, row);
|
||||
commentEl.val(data.comment);
|
||||
commentEl.on("change", editClient);
|
||||
|
||||
@@ -169,45 +170,44 @@ function initTable() {
|
||||
$("td:eq(3)", row).append(
|
||||
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
|
||||
);
|
||||
var selectEl = $("#multiselect_" + dataId, row);
|
||||
const selectEl = $("#multiselect_" + dataId, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var dataSub = "";
|
||||
if (!groups[i].enabled) {
|
||||
dataSub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
for (const group of groups) {
|
||||
const dataSub = group.enabled ? "" : 'data-subtext="(disabled)"';
|
||||
|
||||
selectEl.append(
|
||||
$("<option " + dataSub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name)
|
||||
.val(group.id)
|
||||
.text(group.name)
|
||||
);
|
||||
}
|
||||
|
||||
const applyBtn = "#btn_apply_" + dataId;
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// Initialize bootstrap-select
|
||||
selectEl
|
||||
// fix dropdown if it would stick out right of the viewport
|
||||
.on("show.bs.select", function () {
|
||||
var winWidth = $(globalThis).width();
|
||||
var dropdownEl = $("body > .bootstrap-select.dropdown");
|
||||
.on("show.bs.select", () => {
|
||||
const winWidth = $(globalThis).width();
|
||||
const dropdownEl = $("body > .bootstrap-select.dropdown");
|
||||
if (dropdownEl.length > 0) {
|
||||
dropdownEl.removeClass("align-right");
|
||||
var width = dropdownEl.width();
|
||||
var left = dropdownEl.offset().left;
|
||||
const width = dropdownEl.width();
|
||||
const left = dropdownEl.offset().left;
|
||||
if (left + width > winWidth) {
|
||||
dropdownEl.addClass("align-right");
|
||||
}
|
||||
}
|
||||
})
|
||||
.on("changed.bs.select", function () {
|
||||
.on("changed.bs.select", () => {
|
||||
// enable Apply button
|
||||
if ($(applyBtn).prop("disabled")) {
|
||||
$(applyBtn)
|
||||
.addClass("btn-success")
|
||||
.prop("disabled", false)
|
||||
.on("click", function () {
|
||||
.on("click", () => {
|
||||
editClient.call(selectEl);
|
||||
});
|
||||
}
|
||||
@@ -228,9 +228,7 @@ function initTable() {
|
||||
' class="btn btn-block btn-sm" disabled>Apply</button>'
|
||||
);
|
||||
|
||||
var applyBtn = "#btn_apply_" + dataId;
|
||||
|
||||
var button =
|
||||
const button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteClient_' +
|
||||
dataId +
|
||||
'" data-id="' +
|
||||
@@ -250,7 +248,7 @@ function initTable() {
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
action() {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -258,7 +256,7 @@ function initTable() {
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
action() {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -272,9 +270,9 @@ function initTable() {
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
action() {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
const ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push({ item: $(this).attr("data-id") });
|
||||
@@ -296,11 +294,11 @@ function initTable() {
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
stateSaveCallback(settings, data) {
|
||||
utils.stateSaveCallback("groups-clients-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("groups-clients-table");
|
||||
stateLoadCallback() {
|
||||
const data = utils.stateLoadCallback("groups-clients-table");
|
||||
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
@@ -315,7 +313,7 @@ function initTable() {
|
||||
});
|
||||
|
||||
// Disable autocorrect in the search box
|
||||
var input = document.querySelector("input[type=search]");
|
||||
const input = document.querySelector("input[type=search]");
|
||||
if (input !== null) {
|
||||
input.setAttribute("autocomplete", "off");
|
||||
input.setAttribute("autocorrect", "off");
|
||||
@@ -323,12 +321,12 @@ function initTable() {
|
||||
input.setAttribute("spellcheck", false);
|
||||
}
|
||||
|
||||
table.on("init select deselect", function () {
|
||||
table.on("init select deselect", () => {
|
||||
utils.changeBulkDeleteStates(table);
|
||||
});
|
||||
|
||||
table.on("order.dt", function () {
|
||||
var order = table.order();
|
||||
table.on("order.dt", () => {
|
||||
const order = table.order();
|
||||
if (order[0][0] !== 0 || order[0][1] !== "asc") {
|
||||
$("#resetButton").removeClass("hidden");
|
||||
} else {
|
||||
@@ -336,7 +334,7 @@ function initTable() {
|
||||
}
|
||||
});
|
||||
|
||||
$("#resetButton").on("click", function () {
|
||||
$("#resetButton").on("click", () => {
|
||||
table.order([[0, "asc"]]).draw();
|
||||
$("#resetButton").addClass("hidden");
|
||||
});
|
||||
@@ -358,14 +356,12 @@ function addClient() {
|
||||
|
||||
// Check if the user wants to add multiple IPs (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
var ips = $("#select")
|
||||
const ips = $("#select")
|
||||
.val()
|
||||
.trim()
|
||||
.split(/[\s,]+/);
|
||||
// Remove empty elements
|
||||
ips = ips.filter(function (el) {
|
||||
return el !== "";
|
||||
});
|
||||
.split(/[\s,]+/)
|
||||
// Remove empty elements
|
||||
.filter(el => el !== "");
|
||||
const ipStr = JSON.stringify(ips);
|
||||
|
||||
// Validate input, can be:
|
||||
@@ -373,15 +369,11 @@ function addClient() {
|
||||
// - IPv6 address (with and without CIDR)
|
||||
// - MAC address (in the form AA:BB:CC:DD:EE:FF)
|
||||
// - host name (arbitrary form, we're only checking against some reserved characters)
|
||||
for (var i = 0; i < ips.length; i++) {
|
||||
if (
|
||||
utils.validateIPv4CIDR(ips[i]) ||
|
||||
utils.validateIPv6CIDR(ips[i]) ||
|
||||
utils.validateMAC(ips[i])
|
||||
) {
|
||||
for (const [index, ip] of ips.entries()) {
|
||||
if (utils.validateIPv4CIDR(ip) || utils.validateIPv6CIDR(ip) || utils.validateMAC(ip)) {
|
||||
// Convert input to upper case (important for MAC addresses)
|
||||
ips[i] = ips[i].toUpperCase();
|
||||
} else if (!utils.validateHostname(ips[i])) {
|
||||
ips[index] = ip.toUpperCase();
|
||||
} else if (!utils.validateHostname(ip)) {
|
||||
utils.showAlert(
|
||||
"warning",
|
||||
"",
|
||||
@@ -402,13 +394,13 @@ function addClient() {
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl + "/clients",
|
||||
url: document.body.dataset.apiurl + "/clients",
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ client: ips, comment: comment, groups: group }),
|
||||
success: function (data) {
|
||||
data: JSON.stringify({ client: ips, comment, groups: group }),
|
||||
success(data) {
|
||||
utils.enableAll();
|
||||
utils.listsAlert("client", ips, data);
|
||||
reloadClientSuggestions();
|
||||
@@ -419,7 +411,7 @@ function addClient() {
|
||||
// Update number of groups in the sidebar
|
||||
updateFtlInfo();
|
||||
},
|
||||
error: function (data, exception) {
|
||||
error(data, exception) {
|
||||
apiFailure(data);
|
||||
utils.enableAll();
|
||||
utils.showAlert("error", "", "Error while adding new client", data.responseText);
|
||||
@@ -439,8 +431,8 @@ function editClient() {
|
||||
.map(Number);
|
||||
const comment = tr.find("#comment_" + client).val();
|
||||
|
||||
var done = "edited";
|
||||
var notDone = "editing";
|
||||
let done = "edited";
|
||||
let notDone = "editing";
|
||||
switch (elem) {
|
||||
case "multiselect_" + client:
|
||||
done = "edited groups of";
|
||||
@@ -459,21 +451,21 @@ function editClient() {
|
||||
const clientDecoded = utils.hexDecode(client);
|
||||
utils.showAlert("info", "", "Editing client...", clientDecoded);
|
||||
$.ajax({
|
||||
url: apiUrl + "/clients/" + encodeURIComponent(clientDecoded),
|
||||
url: document.body.dataset.apiurl + "/clients/" + encodeURIComponent(clientDecoded),
|
||||
method: "put",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
groups: groups,
|
||||
comment: comment,
|
||||
groups,
|
||||
comment,
|
||||
}),
|
||||
success: function (data) {
|
||||
success(data) {
|
||||
utils.enableAll();
|
||||
processGroupResult(data, "client", done, notDone);
|
||||
table.ajax.reload(null, false);
|
||||
},
|
||||
error: function (data, exception) {
|
||||
error(data, exception) {
|
||||
apiFailure(data);
|
||||
utils.enableAll();
|
||||
utils.showAlert(
|
||||
|
||||
+26
-26
@@ -5,9 +5,11 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global apiFailure:false, utils:false, apiUrl:false, initTable:false, updateFtlInfo:false */
|
||||
/* global apiFailure:false, utils:false, initTable:false, updateFtlInfo:false */
|
||||
|
||||
var groups = [];
|
||||
"use strict";
|
||||
|
||||
let groups = [];
|
||||
|
||||
function populateGroupSelect(selectEl) {
|
||||
if (selectEl.length === 0) {
|
||||
@@ -16,16 +18,13 @@ function populateGroupSelect(selectEl) {
|
||||
}
|
||||
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var dataSub = "";
|
||||
if (!groups[i].enabled) {
|
||||
dataSub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
for (const group of groups) {
|
||||
const dataSub = group.enabled ? "" : 'data-subtext="(disabled)"';
|
||||
|
||||
selectEl.append(
|
||||
$("<option " + dataSub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name)
|
||||
.val(group.id)
|
||||
.text(group.name)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,23 +38,23 @@ function populateGroupSelect(selectEl) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function getGroups(groupSelector) {
|
||||
$.ajax({
|
||||
url: apiUrl + "/groups",
|
||||
url: document.body.dataset.apiurl + "/groups",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
success(data) {
|
||||
groups = data.groups;
|
||||
|
||||
// Get all all <select> elements with the class "selectpicker"
|
||||
var groupSelector = $(".selectpicker");
|
||||
const groupSelector = $(".selectpicker");
|
||||
// Populate the groupSelector with the groups
|
||||
for (var i = 0; i < groupSelector.length; i++) {
|
||||
populateGroupSelect($(groupSelector[i]));
|
||||
for (const element of groupSelector) {
|
||||
populateGroupSelect($(element));
|
||||
}
|
||||
|
||||
// Actually load table contents
|
||||
initTable();
|
||||
},
|
||||
error: function (data) {
|
||||
error(data) {
|
||||
apiFailure(data);
|
||||
},
|
||||
});
|
||||
@@ -64,14 +63,15 @@ function getGroups(groupSelector) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function processGroupResult(data, type, done, notDone) {
|
||||
// Loop over data.processed.success and show toasts
|
||||
data.processed.success.forEach(function (item) {
|
||||
for (const item of data.processed.success) {
|
||||
utils.showAlert("success", "fas fa-pencil-alt", `Successfully ${done} ${type}`, item);
|
||||
});
|
||||
}
|
||||
|
||||
// Loop over errors and display them
|
||||
data.processed.errors.forEach(function (error) {
|
||||
for (const error of data.processed.errors) {
|
||||
console.log(error); // eslint-disable-line no-console
|
||||
utils.showAlert("error", "", `Error while ${notDone} ${type} ${error.item}`, error.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -79,13 +79,13 @@ function delGroupItems(type, ids, table, listType = undefined) {
|
||||
// Check input validity
|
||||
if (!Array.isArray(ids)) return;
|
||||
|
||||
const url = apiUrl + "/" + type + "s:batchDelete";
|
||||
const url = document.body.dataset.apiurl + "/" + type + "s:batchDelete";
|
||||
|
||||
// use utils.hexDecode() to decode all clients
|
||||
let idstring = "";
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
ids[i].item = utils.hexDecode(ids[i].item);
|
||||
idstring += ids[i].item + ", ";
|
||||
for (const id of ids) {
|
||||
id.item = utils.hexDecode(id.item);
|
||||
idstring += id.item + ", ";
|
||||
}
|
||||
|
||||
// Remove last comma and space from idstring
|
||||
@@ -103,12 +103,12 @@ function delGroupItems(type, ids, table, listType = undefined) {
|
||||
utils.showAlert("info", "", "Deleting " + ids.length + " " + type + "...", idstring);
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
url,
|
||||
data: JSON.stringify(ids),
|
||||
contentType: "application/json",
|
||||
method: "POST",
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "far fa-trash-alt", "Successfully deleted " + type, idstring);
|
||||
table.ajax.reload(null, false);
|
||||
@@ -120,7 +120,7 @@ function delGroupItems(type, ids, table, listType = undefined) {
|
||||
// Update number of <type> items in the sidebar
|
||||
updateFtlInfo();
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
.fail((data, exception) => {
|
||||
apiFailure(data);
|
||||
utils.enableAll();
|
||||
utils.showAlert("error", "", "Error while deleting " + type, data.responseText);
|
||||
|
||||
+115
-99
@@ -5,20 +5,22 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, groups:false,, getGroups:false, updateFtlInfo:false, apiFailure:false, processGroupResult:false, delGroupItems:false */
|
||||
/* global utils:false, groups:false, getGroups:false, updateFtlInfo:false, apiFailure:false, processGroupResult:false, delGroupItems:false */
|
||||
/* exported initTable */
|
||||
|
||||
var table;
|
||||
var GETDict = {};
|
||||
"use strict";
|
||||
|
||||
$(function () {
|
||||
let table;
|
||||
let GETDict = {};
|
||||
|
||||
$(() => {
|
||||
GETDict = utils.parseQueryString();
|
||||
|
||||
// Tabs: Domain/Regex handling
|
||||
// sync description fields, reset inactive inputs on tab change
|
||||
$('a[data-toggle="tab"]').on("shown.bs.tab", function () {
|
||||
var tabHref = $(this).attr("href");
|
||||
var val;
|
||||
const tabHref = $(this).attr("href");
|
||||
let val;
|
||||
if (tabHref === "#tab_domain") {
|
||||
val = $("#new_regex_comment").val();
|
||||
$("#new_domain_comment").val(val);
|
||||
@@ -37,8 +39,8 @@ $(function () {
|
||||
$("#add_deny, #add_allow").on("click", addDomain);
|
||||
|
||||
// Domain suggestion handling
|
||||
var suggestTimeout;
|
||||
$("#new_domain").on("input", function (e) {
|
||||
let suggestTimeout;
|
||||
$("#new_domain").on("input", e => {
|
||||
hideSuggestDomains();
|
||||
clearTimeout(suggestTimeout);
|
||||
suggestTimeout = setTimeout(showSuggestDomains, 1000, e.target.value);
|
||||
@@ -50,36 +52,47 @@ $(function () {
|
||||
|
||||
// Show a list of suggested domains based on the user's input
|
||||
function showSuggestDomains(value) {
|
||||
const newDomainEl = $("#new_domain");
|
||||
const suggestDomainEl = $("#suggest_domains");
|
||||
|
||||
function createButton(hostname) {
|
||||
// Purposefully omit 'btn' class to save space on padding
|
||||
return $('<button type="button" class="btn-link btn-block text-right">')
|
||||
.append($("<i>").text(hostname))
|
||||
.on("click", function () {
|
||||
.append($("<em>").text(hostname))
|
||||
.on("click", () => {
|
||||
hideSuggestDomains();
|
||||
newDomainEl.val(hostname);
|
||||
});
|
||||
}
|
||||
|
||||
var newDomainEl = $("#new_domain");
|
||||
var suggestDomainEl = $("#suggest_domains");
|
||||
try {
|
||||
const parts = new URL(value).hostname.split(".");
|
||||
const table = $("<table>");
|
||||
|
||||
var parts = new URL(value).hostname.split(".");
|
||||
var table = $("<table>");
|
||||
for (let i = 0; i < parts.length - 1; ++i) {
|
||||
const hostname = parts.slice(i).join(".");
|
||||
|
||||
for (var i = 0; i < parts.length - 1; ++i) {
|
||||
var hostname = parts.slice(i).join(".");
|
||||
table.append(
|
||||
$("<tr>")
|
||||
.append($('<td class="text-nowrap text-right">').text(i === 0 ? "Did you mean" : "or"))
|
||||
.append($("<td>").append(createButton(hostname)))
|
||||
);
|
||||
}
|
||||
|
||||
table.append(
|
||||
$("<tr>")
|
||||
.append($('<td class="text-nowrap text-right">').text(i === 0 ? "Did you mean" : "or"))
|
||||
.append($("<td>").append(createButton(hostname)))
|
||||
);
|
||||
suggestDomainEl.slideUp("fast", () => {
|
||||
suggestDomainEl.html(table);
|
||||
suggestDomainEl.slideDown("fast");
|
||||
});
|
||||
} catch (error) {
|
||||
const { message } = error;
|
||||
const isValidUrlError =
|
||||
error instanceof TypeError &&
|
||||
(message.includes("Invalid URL") || message.includes("is not a valid URL"));
|
||||
|
||||
if (!isValidUrlError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
suggestDomainEl.slideUp("fast", function () {
|
||||
suggestDomainEl.html(table);
|
||||
suggestDomainEl.slideDown("fast");
|
||||
});
|
||||
}
|
||||
|
||||
function hideSuggestDomains() {
|
||||
@@ -91,7 +104,7 @@ function initTable() {
|
||||
table = $("#domainsTable").DataTable({
|
||||
processing: true,
|
||||
ajax: {
|
||||
url: apiUrl + "/domains",
|
||||
url: document.body.dataset.apiurl + "/domains",
|
||||
dataSrc: "domains",
|
||||
type: "GET",
|
||||
},
|
||||
@@ -110,13 +123,13 @@ function initTable() {
|
||||
{
|
||||
targets: 1,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
render() {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
{
|
||||
targets: 3,
|
||||
render: function (data) {
|
||||
render(data) {
|
||||
return data.kind + "_" + data.type;
|
||||
},
|
||||
},
|
||||
@@ -125,20 +138,20 @@ function initTable() {
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
drawCallback() {
|
||||
// Hide buttons if all domains were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
const hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
$('button[id^="deleteDomain_"]').on("click", deleteDomain);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var dataId = utils.hexEncode(data.domain) + "_" + data.type + "_" + data.kind;
|
||||
rowCallback(row, data) {
|
||||
const dataId = utils.hexEncode(data.domain) + "_" + data.type + "_" + data.kind;
|
||||
$(row).attr("data-id", dataId);
|
||||
// Tooltip for domain
|
||||
var tooltip =
|
||||
const tooltip =
|
||||
"Added: " +
|
||||
utils.datetime(data.date_added, false) +
|
||||
"\nLast modified: " +
|
||||
@@ -182,7 +195,7 @@ function initTable() {
|
||||
data.kind +
|
||||
"'>"
|
||||
);
|
||||
var typeEl = $("#type_" + dataId, row);
|
||||
const typeEl = $("#type_" + dataId, row);
|
||||
typeEl.on("change", editDomain);
|
||||
|
||||
// Initialize bootstrap-toggle for status field (enabled/disabled)
|
||||
@@ -193,7 +206,7 @@ function initTable() {
|
||||
(data.enabled ? " checked" : "") +
|
||||
">"
|
||||
);
|
||||
var statusEl = $("#enabled_" + dataId, row);
|
||||
const statusEl = $("#enabled_" + dataId, row);
|
||||
statusEl.bootstrapToggle({
|
||||
on: "Enabled",
|
||||
off: "Disabled",
|
||||
@@ -205,7 +218,7 @@ function initTable() {
|
||||
|
||||
// Comment field
|
||||
$("td:eq(4)", row).html('<input id="comment_' + dataId + '" class="form-control">');
|
||||
var commentEl = $("#comment_" + dataId, row);
|
||||
const commentEl = $("#comment_" + dataId, row);
|
||||
commentEl.val(data.comment);
|
||||
commentEl.on("change", editDomain);
|
||||
|
||||
@@ -214,18 +227,15 @@ function initTable() {
|
||||
$("td:eq(5)", row).append(
|
||||
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
|
||||
);
|
||||
var selectEl = $("#multiselect_" + dataId, row);
|
||||
const selectEl = $("#multiselect_" + dataId, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var dataSub = "";
|
||||
if (!groups[i].enabled) {
|
||||
dataSub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
for (const group of groups) {
|
||||
const dataSub = group.enabled ? "" : 'data-subtext="(disabled)"';
|
||||
|
||||
selectEl.append(
|
||||
$("<option " + dataSub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name)
|
||||
.val(group.id)
|
||||
.text(group.name)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -235,26 +245,26 @@ function initTable() {
|
||||
const applyBtn = "#btn_apply_" + dataId;
|
||||
selectEl
|
||||
// fix dropdown if it would stick out right of the viewport
|
||||
.on("show.bs.select", function () {
|
||||
var winWidth = $(globalThis).width();
|
||||
var dropdownEl = $("body > .bootstrap-select.dropdown");
|
||||
.on("show.bs.select", () => {
|
||||
const winWidth = $(globalThis).width();
|
||||
const dropdownEl = $("body > .bootstrap-select.dropdown");
|
||||
if (dropdownEl.length > 0) {
|
||||
dropdownEl.removeClass("align-right");
|
||||
var width = dropdownEl.width();
|
||||
var left = dropdownEl.offset().left;
|
||||
const width = dropdownEl.width();
|
||||
const left = dropdownEl.offset().left;
|
||||
if (left + width > winWidth) {
|
||||
dropdownEl.addClass("align-right");
|
||||
}
|
||||
}
|
||||
})
|
||||
.on("changed.bs.select", function () {
|
||||
.on("changed.bs.select", () => {
|
||||
// enable Apply button if changes were made to the drop-down menu
|
||||
// and have it call editDomain() on click
|
||||
if ($(applyBtn).prop("disabled")) {
|
||||
$(applyBtn)
|
||||
.addClass("btn-success")
|
||||
.prop("disabled", false)
|
||||
.on("click", function () {
|
||||
.on("click", () => {
|
||||
editDomain.call(selectEl);
|
||||
});
|
||||
}
|
||||
@@ -278,12 +288,12 @@ function initTable() {
|
||||
);
|
||||
|
||||
// Highlight row (if url parameter "domainid=" is used)
|
||||
if ("domainid" in GETDict && data.id === parseInt(GETDict.domainid, 10)) {
|
||||
if ("domainid" in GETDict && data.id === Number.parseInt(GETDict.domainid, 10)) {
|
||||
$(row).find("td").addClass("highlight");
|
||||
}
|
||||
|
||||
// Add delete domain button
|
||||
var button =
|
||||
const button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteDomain_' +
|
||||
dataId +
|
||||
'" data-id="' +
|
||||
@@ -303,7 +313,7 @@ function initTable() {
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
action() {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -311,7 +321,7 @@ function initTable() {
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
action() {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -325,9 +335,9 @@ function initTable() {
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
action() {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
const ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push($(this).attr("data-id"));
|
||||
@@ -349,11 +359,11 @@ function initTable() {
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
stateSaveCallback(settings, data) {
|
||||
utils.stateSaveCallback("groups-domains-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("groups-domains-table");
|
||||
stateLoadCallback() {
|
||||
const data = utils.stateLoadCallback("groups-domains-table");
|
||||
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
@@ -365,21 +375,21 @@ function initTable() {
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
},
|
||||
initComplete: function () {
|
||||
initComplete() {
|
||||
if ("domainid" in GETDict) {
|
||||
var pos = table
|
||||
const pos = table
|
||||
.column(0, { order: "current" })
|
||||
.data()
|
||||
.indexOf(parseInt(GETDict.domainid, 10));
|
||||
if (pos >= 0) {
|
||||
var page = Math.floor(pos / table.page.info().length);
|
||||
.indexOf(Number.parseInt(GETDict.domainid, 10));
|
||||
if (pos !== -1) {
|
||||
const page = Math.floor(pos / table.page.info().length);
|
||||
table.page(page).draw(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
// Disable autocorrect in the search box
|
||||
var input = document.querySelector("input[type=search]");
|
||||
const input = document.querySelector("input[type=search]");
|
||||
if (input !== null) {
|
||||
input.setAttribute("autocomplete", "off");
|
||||
input.setAttribute("autocorrect", "off");
|
||||
@@ -387,12 +397,12 @@ function initTable() {
|
||||
input.setAttribute("spellcheck", false);
|
||||
}
|
||||
|
||||
table.on("init select deselect", function () {
|
||||
table.on("init select deselect", () => {
|
||||
utils.changeBulkDeleteStates(table);
|
||||
});
|
||||
|
||||
table.on("order.dt", function () {
|
||||
var order = table.order();
|
||||
table.on("order.dt", () => {
|
||||
const order = table.order();
|
||||
if (order[0][0] !== 0 || order[0][1] !== "asc") {
|
||||
$("#resetButton").removeClass("hidden");
|
||||
} else {
|
||||
@@ -400,28 +410,28 @@ function initTable() {
|
||||
}
|
||||
});
|
||||
|
||||
$("#resetButton").on("click", function () {
|
||||
$("#resetButton").on("click", () => {
|
||||
table.order([[0, "asc"]]).draw();
|
||||
$("#resetButton").addClass("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
// Enable "filter by type" functionality, using checkboxes
|
||||
$.fn.dataTable.ext.search.push(function (settings, searchData, index, rowData) {
|
||||
var types = $(".filter_types input:checkbox:checked")
|
||||
$.fn.dataTable.ext.search.push((settings, searchData, index, rowData) => {
|
||||
const types = $(".filter_types input:checkbox:checked")
|
||||
.map(function () {
|
||||
return this.value;
|
||||
})
|
||||
.get();
|
||||
|
||||
const typeStr = rowData.type + "/" + rowData.kind;
|
||||
if (types.indexOf(typeStr) !== -1) {
|
||||
if (types.includes(typeStr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
$(".filter_types input:checkbox").on("change", function () {
|
||||
$(".filter_types input:checkbox").on("change", () => {
|
||||
table.draw();
|
||||
});
|
||||
|
||||
@@ -435,9 +445,9 @@ function deleteDomain() {
|
||||
|
||||
function deleteDomains(encodedIds) {
|
||||
const decodedIds = [];
|
||||
for (let i = 0; i < encodedIds.length; i++) {
|
||||
for (const [i, encodedId] of encodedIds.entries()) {
|
||||
// Decode domain, type, and kind and add to array
|
||||
const parts = encodedIds[i].split("_");
|
||||
const parts = encodedId.split("_");
|
||||
decodedIds[i] = {
|
||||
item: parts[0],
|
||||
type: parts[1],
|
||||
@@ -455,7 +465,10 @@ function addDomain() {
|
||||
const wildcardChecked = wildcardEl.prop("checked");
|
||||
|
||||
// current tab's inputs
|
||||
var kind, domainEl, commentEl, groupEl;
|
||||
let kind;
|
||||
let domainEl;
|
||||
let commentEl;
|
||||
let groupEl;
|
||||
if (tabHref === "#tab_domain") {
|
||||
kind = "exact";
|
||||
domainEl = $("#new_domain");
|
||||
@@ -474,11 +487,9 @@ function addDomain() {
|
||||
|
||||
// Check if the user wants to add multiple domains (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
var domains = domainEl.val().split(/\s+/);
|
||||
let domains = domainEl.val().split(/\s+/);
|
||||
// Remove empty elements
|
||||
domains = domains.filter(function (el) {
|
||||
return el !== "";
|
||||
});
|
||||
domains = domains.filter(el => el !== "");
|
||||
const domainStr = JSON.stringify(domains);
|
||||
|
||||
utils.disableAll();
|
||||
@@ -492,12 +503,12 @@ function addDomain() {
|
||||
|
||||
// Check if the wildcard checkbox was marked and transform the domains into regex
|
||||
if (kind === "exact" && wildcardChecked) {
|
||||
for (var i = 0; i < domains.length; i++) {
|
||||
for (const [index, domain] of domains.entries()) {
|
||||
// Strip leading "*." if specified by user in wildcard mode
|
||||
if (domains[i].startsWith("*.")) domains[i] = domains[i].substr(2);
|
||||
if (domain.startsWith("*.")) domains[index] = domain.substr(2);
|
||||
|
||||
// Transform domain into a wildcard regex
|
||||
domains[i] = "(\\.|^)" + domains[i].replaceAll(".", "\\.") + "$";
|
||||
domains[index] = "(\\.|^)" + domains[index].replaceAll(".", "\\.") + "$";
|
||||
}
|
||||
|
||||
kind = "regex";
|
||||
@@ -507,19 +518,19 @@ function addDomain() {
|
||||
const type = action === "add_deny" ? "deny" : "allow";
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl + "/domains/" + type + "/" + kind,
|
||||
url: document.body.dataset.apiurl + "/domains/" + type + "/" + kind,
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
domain: domains,
|
||||
comment: comment,
|
||||
type: type,
|
||||
kind: kind,
|
||||
comment,
|
||||
type,
|
||||
kind,
|
||||
groups: group,
|
||||
}),
|
||||
success: function (data) {
|
||||
success(data) {
|
||||
utils.enableAll();
|
||||
utils.listsAlert("domain", domains, data);
|
||||
$("#new_domain").val("");
|
||||
@@ -532,7 +543,7 @@ function addDomain() {
|
||||
// Update number of groups in the sidebar
|
||||
updateFtlInfo();
|
||||
},
|
||||
error: function (data, exception) {
|
||||
error(data, exception) {
|
||||
apiFailure(data);
|
||||
utils.enableAll();
|
||||
utils.showAlert("error", "", "Error while adding new domain", data.responseText);
|
||||
@@ -558,8 +569,8 @@ function editDomain() {
|
||||
const oldType = oldTypeStr.split("/")[0];
|
||||
const oldKind = oldTypeStr.split("/")[1];
|
||||
|
||||
var done = "edited";
|
||||
var notDone = "editing";
|
||||
let done = "edited";
|
||||
let notDone = "editing";
|
||||
switch (elem) {
|
||||
case "enabled_" + domain:
|
||||
if (!enabled) {
|
||||
@@ -596,24 +607,29 @@ function editDomain() {
|
||||
const domainDecoded = utils.hexDecode(domain.split("_")[0]);
|
||||
utils.showAlert("info", "", "Editing domain...", domainDecoded);
|
||||
$.ajax({
|
||||
url: apiUrl + "/domains/" + newTypestr + "/" + encodeURIComponent(domainDecoded),
|
||||
url:
|
||||
document.body.dataset.apiurl +
|
||||
"/domains/" +
|
||||
newTypestr +
|
||||
"/" +
|
||||
encodeURIComponent(domainDecoded),
|
||||
method: "put",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
groups: groups,
|
||||
comment: comment,
|
||||
enabled: enabled,
|
||||
groups,
|
||||
comment,
|
||||
enabled,
|
||||
type: oldType,
|
||||
kind: oldKind,
|
||||
}),
|
||||
success: function (data) {
|
||||
success(data) {
|
||||
utils.enableAll();
|
||||
processGroupResult(data, "domain", done, notDone);
|
||||
table.ajax.reload(null, false);
|
||||
},
|
||||
error: function (data, exception) {
|
||||
error(data, exception) {
|
||||
apiFailure(data);
|
||||
utils.enableAll();
|
||||
utils.showAlert(
|
||||
|
||||
+100
-100
@@ -5,13 +5,15 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, groups:false, apiFailure:false, updateFtlInfo:false, getGroups:false, processGroupResult:false, delGroupItems:false */
|
||||
/* global utils:false, groups:false, apiFailure:false, updateFtlInfo:false, getGroups:false, processGroupResult:false, delGroupItems:false */
|
||||
/* exported initTable */
|
||||
|
||||
var table;
|
||||
var GETDict = {};
|
||||
"use strict";
|
||||
|
||||
$(function () {
|
||||
let table;
|
||||
let GETDict = {};
|
||||
|
||||
$(() => {
|
||||
GETDict = utils.parseQueryString();
|
||||
|
||||
$("#btnAddAllow").on("click", { type: "allow" }, addList);
|
||||
@@ -23,33 +25,35 @@ $(function () {
|
||||
|
||||
function format(data) {
|
||||
// Generate human-friendly status string
|
||||
var statusText = setStatusText(data, true);
|
||||
var numbers = true;
|
||||
const statusText = setStatusText(data, true);
|
||||
let numbers = true;
|
||||
if (data.status === 0 || data.status === 4) {
|
||||
numbers = false;
|
||||
}
|
||||
|
||||
// Compile extra info for displaying
|
||||
var dateAddedISO = utils.datetime(data.date_added, false),
|
||||
dateModifiedISO = utils.datetime(data.date_modified, false),
|
||||
dateUpdated =
|
||||
data.date_updated > 0
|
||||
? utils.datetimeRelative(data.date_updated) +
|
||||
" (" +
|
||||
utils.datetime(data.date_updated, false) +
|
||||
")"
|
||||
: "N/A",
|
||||
numberOfEntries =
|
||||
(data.number !== null && numbers === true
|
||||
? parseInt(data.number, 10).toLocaleString()
|
||||
: "N/A") +
|
||||
(data.abp_entries !== null && parseInt(data.abp_entries, 10) > 0 && numbers === true
|
||||
? " (out of which " + parseInt(data.abp_entries, 10).toLocaleString() + " are in ABP-style)"
|
||||
: ""),
|
||||
nonDomains =
|
||||
data.invalid_domains !== null && numbers === true
|
||||
? parseInt(data.invalid_domains, 10).toLocaleString()
|
||||
: "N/A";
|
||||
const dateAddedISO = utils.datetime(data.date_added, false);
|
||||
const dateModifiedISO = utils.datetime(data.date_modified, false);
|
||||
const dateUpdated =
|
||||
data.date_updated > 0
|
||||
? utils.datetimeRelative(data.date_updated) +
|
||||
" (" +
|
||||
utils.datetime(data.date_updated, false) +
|
||||
")"
|
||||
: "N/A";
|
||||
const numberOfEntries =
|
||||
(data.number !== null && numbers === true
|
||||
? Number.parseInt(data.number, 10).toLocaleString()
|
||||
: "N/A") +
|
||||
(data.abp_entries !== null && Number.parseInt(data.abp_entries, 10) > 0 && numbers === true
|
||||
? " (out of which " +
|
||||
Number.parseInt(data.abp_entries, 10).toLocaleString() +
|
||||
" are in ABP-style)"
|
||||
: "");
|
||||
const nonDomains =
|
||||
data.invalid_domains !== null && numbers === true
|
||||
? Number.parseInt(data.invalid_domains, 10).toLocaleString()
|
||||
: "N/A";
|
||||
|
||||
return `<table>
|
||||
<tr class="dataTables-child">
|
||||
@@ -83,9 +87,9 @@ function format(data) {
|
||||
|
||||
// Define the status icon element
|
||||
function setStatusIcon(data) {
|
||||
var statusCode = parseInt(data.status, 10),
|
||||
statusTitle = setStatusText(data) + "\nClick for details about this list",
|
||||
statusIcon;
|
||||
const statusCode = Number.parseInt(data.status, 10);
|
||||
const statusTitle = setStatusText(data) + "\nClick for details about this list";
|
||||
let statusIcon;
|
||||
|
||||
switch (statusCode) {
|
||||
case 1:
|
||||
@@ -110,10 +114,10 @@ function setStatusIcon(data) {
|
||||
|
||||
// Define human-friendly status string
|
||||
function setStatusText(data, showdetails = false) {
|
||||
var statusText = "Unknown",
|
||||
statusDetails = "";
|
||||
let statusText = "Unknown";
|
||||
let statusDetails = "";
|
||||
if (data.status !== null) {
|
||||
switch (parseInt(data.status, 10)) {
|
||||
switch (Number.parseInt(data.status, 10)) {
|
||||
case 0:
|
||||
statusText =
|
||||
data.enabled === 0
|
||||
@@ -140,7 +144,8 @@ function setStatusText(data, showdetails = false) {
|
||||
|
||||
default:
|
||||
statusText = "Unknown";
|
||||
statusDetails = ' (<span class="list-status-0">' + parseInt(data.status, 10) + "</span>)";
|
||||
statusDetails =
|
||||
' (<span class="list-status-0">' + Number.parseInt(data.status, 10) + "</span>)";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -152,8 +157,8 @@ function setStatusText(data, showdetails = false) {
|
||||
function setTypeIcon(type) {
|
||||
//Add red ban icon if data["type"] is "block"
|
||||
//Add green check icon if data["type"] is "allow"
|
||||
let iconClass = "fa-question text-orange",
|
||||
title = "This list is of unknown type";
|
||||
let iconClass = "fa-question text-orange";
|
||||
let title = "This list is of unknown type";
|
||||
if (type === "block") {
|
||||
iconClass = "fa-ban text-red";
|
||||
title = "This is a blocklist";
|
||||
@@ -170,7 +175,7 @@ function initTable() {
|
||||
table = $("#listsTable").DataTable({
|
||||
processing: true,
|
||||
ajax: {
|
||||
url: apiUrl + "/lists",
|
||||
url: document.body.dataset.apiurl + "/lists",
|
||||
dataSrc: "lists",
|
||||
type: "GET",
|
||||
},
|
||||
@@ -190,7 +195,7 @@ function initTable() {
|
||||
{
|
||||
targets: 1,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
render() {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
@@ -199,26 +204,26 @@ function initTable() {
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
drawCallback() {
|
||||
// Hide buttons if all lists were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
const hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
$('button[id^="deleteList_"]').on("click", deleteList);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var dataId = utils.hexEncode(data.address + "_" + data.type);
|
||||
rowCallback(row, data) {
|
||||
const dataId = utils.hexEncode(data.address + "_" + data.type);
|
||||
$(row).attr("data-id", dataId);
|
||||
$(row).attr("data-address", utils.hexEncode(data.address));
|
||||
$(row).attr("data-type", data.type);
|
||||
|
||||
var statusCode = 0;
|
||||
let statusCode = 0;
|
||||
// If there is no status or the list is disabled, we keep
|
||||
// status 0 (== unknown)
|
||||
if (data.status !== null && data.enabled) {
|
||||
statusCode = parseInt(data.status, 10);
|
||||
statusCode = Number.parseInt(data.status, 10);
|
||||
}
|
||||
|
||||
$("td:eq(1)", row).addClass("list-status-" + statusCode);
|
||||
@@ -256,7 +261,7 @@ function initTable() {
|
||||
(data.enabled ? " checked" : "") +
|
||||
">"
|
||||
);
|
||||
var statusEl = $("#enabled_" + dataId, row);
|
||||
const statusEl = $("#enabled_" + dataId, row);
|
||||
statusEl.bootstrapToggle({
|
||||
on: "Enabled",
|
||||
off: "Disabled",
|
||||
@@ -267,7 +272,7 @@ function initTable() {
|
||||
statusEl.on("change", editList);
|
||||
|
||||
$("td:eq(5)", row).html('<input id="comment_' + dataId + '" class="form-control">');
|
||||
var commentEl = $("#comment_" + dataId, row);
|
||||
const commentEl = $("#comment_" + dataId, row);
|
||||
commentEl.val(data.comment);
|
||||
commentEl.on("change", editList);
|
||||
|
||||
@@ -275,47 +280,44 @@ function initTable() {
|
||||
$("td:eq(6)", row).append(
|
||||
'<select class="selectpicker" id="multiselect_' + dataId + '" multiple></select>'
|
||||
);
|
||||
var selectEl = $("#multiselect_" + dataId, row);
|
||||
const selectEl = $("#multiselect_" + dataId, row);
|
||||
// Add all known groups
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var dataSub = "";
|
||||
if (!groups[i].enabled) {
|
||||
dataSub = 'data-subtext="(disabled)"';
|
||||
}
|
||||
for (const group of groups) {
|
||||
const dataSub = group.enabled ? "" : 'data-subtext="(disabled)"';
|
||||
|
||||
selectEl.append(
|
||||
$("<option " + dataSub + "/>")
|
||||
.val(groups[i].id)
|
||||
.text(groups[i].name)
|
||||
.val(group.id)
|
||||
.text(group.name)
|
||||
);
|
||||
}
|
||||
|
||||
var applyBtn = "#btn_apply_" + dataId;
|
||||
const applyBtn = "#btn_apply_" + dataId;
|
||||
|
||||
// Select assigned groups
|
||||
selectEl.val(data.groups);
|
||||
// Initialize bootstrap-select
|
||||
selectEl
|
||||
// fix dropdown if it would stick out right of the viewport
|
||||
.on("show.bs.select", function () {
|
||||
var winWidth = $(globalThis).width();
|
||||
var dropdownEl = $("body > .bootstrap-select.dropdown");
|
||||
.on("show.bs.select", () => {
|
||||
const winWidth = $(globalThis).width();
|
||||
const dropdownEl = $("body > .bootstrap-select.dropdown");
|
||||
if (dropdownEl.length > 0) {
|
||||
dropdownEl.removeClass("align-right");
|
||||
var width = dropdownEl.width();
|
||||
var left = dropdownEl.offset().left;
|
||||
const width = dropdownEl.width();
|
||||
const left = dropdownEl.offset().left;
|
||||
if (left + width > winWidth) {
|
||||
dropdownEl.addClass("align-right");
|
||||
}
|
||||
}
|
||||
})
|
||||
.on("changed.bs.select", function () {
|
||||
.on("changed.bs.select", () => {
|
||||
// enable Apply button
|
||||
if ($(applyBtn).prop("disabled")) {
|
||||
$(applyBtn)
|
||||
.addClass("btn-success")
|
||||
.prop("disabled", false)
|
||||
.on("click", function () {
|
||||
.on("click", () => {
|
||||
editList.call(selectEl);
|
||||
});
|
||||
}
|
||||
@@ -337,11 +339,11 @@ function initTable() {
|
||||
);
|
||||
|
||||
// Highlight row (if url parameter "listid=" is used)
|
||||
if ("listid" in GETDict && data.id === parseInt(GETDict.listid, 10)) {
|
||||
if ("listid" in GETDict && data.id === Number.parseInt(GETDict.listid, 10)) {
|
||||
$(row).find("td").addClass("highlight");
|
||||
}
|
||||
|
||||
var button =
|
||||
const button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteList_' +
|
||||
dataId +
|
||||
'" data-id="' +
|
||||
@@ -371,7 +373,7 @@ function initTable() {
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
action() {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -379,7 +381,7 @@ function initTable() {
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
action() {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -393,9 +395,9 @@ function initTable() {
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
action() {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
const ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push({ item: $(this).attr("data-address"), type: $(this).attr("data-type") });
|
||||
@@ -407,11 +409,11 @@ function initTable() {
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
stateSaveCallback(settings, data) {
|
||||
utils.stateSaveCallback("groups-lists-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("groups-lists-table");
|
||||
stateLoadCallback() {
|
||||
const data = utils.stateLoadCallback("groups-lists-table");
|
||||
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
@@ -423,26 +425,26 @@ function initTable() {
|
||||
// Apply loaded state to table
|
||||
return data;
|
||||
},
|
||||
initComplete: function () {
|
||||
initComplete() {
|
||||
if ("listid" in GETDict) {
|
||||
var pos = table
|
||||
const pos = table
|
||||
.column(0, { order: "current" })
|
||||
.data()
|
||||
.indexOf(parseInt(GETDict.listid, 10));
|
||||
if (pos >= 0) {
|
||||
var page = Math.floor(pos / table.page.info().length);
|
||||
.indexOf(Number.parseInt(GETDict.listid, 10));
|
||||
if (pos !== -1) {
|
||||
const page = Math.floor(pos / table.page.info().length);
|
||||
table.page(page).draw(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
table.on("init select deselect", function () {
|
||||
table.on("init select deselect", () => {
|
||||
utils.changeBulkDeleteStates(table);
|
||||
});
|
||||
|
||||
table.on("order.dt", function () {
|
||||
var order = table.order();
|
||||
table.on("order.dt", () => {
|
||||
const order = table.order();
|
||||
if (order[0][0] !== 0 || order[0][1] !== "asc") {
|
||||
$("#resetButton").removeClass("hidden");
|
||||
} else {
|
||||
@@ -450,15 +452,15 @@ function initTable() {
|
||||
}
|
||||
});
|
||||
|
||||
$("#resetButton").on("click", function () {
|
||||
$("#resetButton").on("click", () => {
|
||||
table.order([[0, "asc"]]).draw();
|
||||
$("#resetButton").addClass("hidden");
|
||||
});
|
||||
|
||||
// Add event listener for opening and closing details
|
||||
$("#listsTable tbody").on("click", "td.details-control", function () {
|
||||
var tr = $(this).closest("tr");
|
||||
var row = table.row(tr);
|
||||
const tr = $(this).closest("tr");
|
||||
const row = table.row(tr);
|
||||
|
||||
if (row.child.isShown()) {
|
||||
// This row is already open - close it
|
||||
@@ -472,7 +474,7 @@ function initTable() {
|
||||
});
|
||||
|
||||
// Disable autocorrect in the search box
|
||||
var input = document.querySelector("input[type=search]");
|
||||
const input = document.querySelector("input[type=search]");
|
||||
if (input !== null) {
|
||||
input.setAttribute("autocomplete", "off");
|
||||
input.setAttribute("autocorrect", "off");
|
||||
@@ -499,13 +501,11 @@ function addList(event) {
|
||||
|
||||
// Check if the user wants to add multiple domains (space or newline separated)
|
||||
// If so, split the input and store it in an array
|
||||
var addresses = $("#new_address")
|
||||
let addresses = $("#new_address")
|
||||
.val()
|
||||
.split(/[\s,]+/);
|
||||
// Remove empty elements
|
||||
addresses = addresses.filter(function (el) {
|
||||
return el !== "";
|
||||
});
|
||||
addresses = addresses.filter(el => el !== "");
|
||||
const addressestr = JSON.stringify(addresses);
|
||||
|
||||
utils.disableAll();
|
||||
@@ -519,13 +519,13 @@ function addList(event) {
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl + "/lists",
|
||||
url: document.body.dataset.apiurl + "/lists",
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ address: addresses, comment: comment, type: type, groups: group }),
|
||||
success: function (data) {
|
||||
data: JSON.stringify({ address: addresses, comment, type, groups: group }),
|
||||
success(data) {
|
||||
utils.enableAll();
|
||||
utils.listsAlert(type + "list", addresses, data);
|
||||
$("#new_address").val("");
|
||||
@@ -536,7 +536,7 @@ function addList(event) {
|
||||
// Update number of groups in the sidebar
|
||||
updateFtlInfo();
|
||||
},
|
||||
error: function (data, exception) {
|
||||
error(data, exception) {
|
||||
apiFailure(data);
|
||||
utils.enableAll();
|
||||
utils.showAlert("error", "", "Error while adding new " + type + "list", data.responseText);
|
||||
@@ -552,15 +552,15 @@ function editList() {
|
||||
const dataId = tr.attr("data-id");
|
||||
const address = utils.hexDecode(tr.attr("data-address"));
|
||||
const enabled = tr.find("#enabled_" + dataId).is(":checked");
|
||||
const comment = utils.escapeHtml(tr.find("#comment_" + dataId).val());
|
||||
const comment = tr.find("#comment_" + dataId).val();
|
||||
// Convert list of string integers to list of integers using map(Number)
|
||||
const groups = tr
|
||||
.find("#multiselect_" + dataId)
|
||||
.val()
|
||||
.map(Number);
|
||||
|
||||
var done = "edited";
|
||||
var notDone = "editing";
|
||||
let done = "edited";
|
||||
let notDone = "editing";
|
||||
switch (elem) {
|
||||
case "enabled_" + dataId:
|
||||
if (!enabled) {
|
||||
@@ -588,23 +588,23 @@ function editList() {
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Editing address...", address);
|
||||
$.ajax({
|
||||
url: apiUrl + "/lists/" + encodeURIComponent(address) + "?type=" + type,
|
||||
url: document.body.dataset.apiurl + "/lists/" + encodeURIComponent(address) + "?type=" + type,
|
||||
method: "put",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
groups: groups,
|
||||
comment: comment,
|
||||
enabled: enabled,
|
||||
type: type,
|
||||
groups,
|
||||
comment,
|
||||
enabled,
|
||||
type,
|
||||
}),
|
||||
success: function (data) {
|
||||
success(data) {
|
||||
utils.enableAll();
|
||||
processGroupResult(data, type + "list", done, notDone);
|
||||
table.ajax.reload(null, false);
|
||||
},
|
||||
error: function (data, exception) {
|
||||
error(data, exception) {
|
||||
apiFailure(data);
|
||||
utils.enableAll();
|
||||
utils.showAlert(
|
||||
|
||||
+45
-46
@@ -5,9 +5,11 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, apiFailure:false, updateFtlInfo:false, processGroupResult:false, delGroupItems:false */
|
||||
/* global utils:false, apiFailure:false, updateFtlInfo:false, processGroupResult:false, delGroupItems:false */
|
||||
|
||||
var table;
|
||||
"use strict";
|
||||
|
||||
let table;
|
||||
|
||||
function handleAjaxError(xhr, textStatus) {
|
||||
if (textStatus === "timeout") {
|
||||
@@ -20,13 +22,13 @@ function handleAjaxError(xhr, textStatus) {
|
||||
table.draw();
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
$("#btnAdd").on("click", addGroup);
|
||||
|
||||
table = $("#groupsTable").DataTable({
|
||||
processing: true,
|
||||
ajax: {
|
||||
url: apiUrl + "/groups",
|
||||
url: document.body.dataset.apiurl + "/groups",
|
||||
error: handleAjaxError,
|
||||
dataSrc: "groups",
|
||||
type: "GET",
|
||||
@@ -44,7 +46,7 @@ $(function () {
|
||||
{
|
||||
targets: 1,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
render() {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
@@ -53,18 +55,18 @@ $(function () {
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
drawCallback() {
|
||||
// Hide buttons if all groups were deleted
|
||||
// if there is one row, it's the default group
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 1;
|
||||
const hasRows = this.api().rows({ filter: "applied" }).data().length > 1;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
$('button[id^="deleteGroup_"]').on("click", deleteGroup);
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var dataId = utils.hexEncode(data.name);
|
||||
rowCallback(row, data) {
|
||||
const dataId = utils.hexEncode(data.name);
|
||||
$(row).attr("data-id", dataId);
|
||||
var tooltip =
|
||||
const tooltip =
|
||||
"Added: " +
|
||||
utils.datetime(data.date_added, false) +
|
||||
"\nLast modified: " +
|
||||
@@ -74,7 +76,7 @@ $(function () {
|
||||
$("td:eq(1)", row).html(
|
||||
'<input id="name_' + dataId + '" title="' + tooltip + '" class="form-control">'
|
||||
);
|
||||
var nameEl = $("#name_" + dataId, row);
|
||||
const nameEl = $("#name_" + dataId, row);
|
||||
nameEl.val(data.name);
|
||||
nameEl.on("change", editGroup);
|
||||
|
||||
@@ -85,7 +87,7 @@ $(function () {
|
||||
(data.enabled ? " checked" : "") +
|
||||
">"
|
||||
);
|
||||
var enabledEl = $("#enabled_" + dataId, row);
|
||||
const enabledEl = $("#enabled_" + dataId, row);
|
||||
enabledEl.bootstrapToggle({
|
||||
on: "Enabled",
|
||||
off: "Disabled",
|
||||
@@ -96,15 +98,15 @@ $(function () {
|
||||
enabledEl.on("change", editGroup);
|
||||
|
||||
$("td:eq(3)", row).html('<input id="comment_' + dataId + '" class="form-control">');
|
||||
var comment = data.comment !== null ? data.comment : "";
|
||||
var commentEl = $("#comment_" + dataId, row);
|
||||
const comment = data.comment !== null ? data.comment : "";
|
||||
const commentEl = $("#comment_" + dataId, row);
|
||||
commentEl.val(comment);
|
||||
commentEl.on("change", editGroup);
|
||||
|
||||
$("td:eq(4)", row).empty();
|
||||
// Show delete button for all but the default group
|
||||
if (data.id !== 0) {
|
||||
var button =
|
||||
const button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteGroup_' +
|
||||
dataId +
|
||||
'" data-id="' +
|
||||
@@ -125,7 +127,7 @@ $(function () {
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
action() {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -133,7 +135,7 @@ $(function () {
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
action() {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -147,9 +149,9 @@ $(function () {
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
action() {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
const ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push({ item: $(this).attr("data-id") });
|
||||
@@ -171,11 +173,11 @@ $(function () {
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
stateSaveCallback(settings, data) {
|
||||
utils.stateSaveCallback("groups-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("groups-table");
|
||||
stateLoadCallback() {
|
||||
const data = utils.stateLoadCallback("groups-table");
|
||||
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
@@ -190,7 +192,7 @@ $(function () {
|
||||
});
|
||||
|
||||
// Disable autocorrect in the search box
|
||||
var input = document.querySelector("input[type=search]");
|
||||
const input = document.querySelector("input[type=search]");
|
||||
if (input !== null) {
|
||||
input.setAttribute("autocomplete", "off");
|
||||
input.setAttribute("autocorrect", "off");
|
||||
@@ -198,8 +200,9 @@ $(function () {
|
||||
input.setAttribute("spellcheck", false);
|
||||
}
|
||||
|
||||
table.on("init select deselect", function () {
|
||||
table.on("init select deselect", () => {
|
||||
// if the Default group is selected, undo the selection of it
|
||||
// eslint-disable-next-line unicorn/prefer-includes
|
||||
if (table.rows({ selected: true }).data().pluck("id").indexOf(0) !== -1) {
|
||||
table.rows(0).deselect();
|
||||
}
|
||||
@@ -207,8 +210,8 @@ $(function () {
|
||||
utils.changeBulkDeleteStates(table);
|
||||
});
|
||||
|
||||
table.on("order.dt", function () {
|
||||
var order = table.order();
|
||||
table.on("order.dt", () => {
|
||||
const order = table.order();
|
||||
if (order[0][0] !== 0 || order[0][1] !== "asc") {
|
||||
$("#resetButton").removeClass("hidden");
|
||||
} else {
|
||||
@@ -216,7 +219,7 @@ $(function () {
|
||||
}
|
||||
});
|
||||
|
||||
$("#resetButton").on("click", function () {
|
||||
$("#resetButton").on("click", () => {
|
||||
table.order([[0, "asc"]]).draw();
|
||||
$("#resetButton").addClass("hidden");
|
||||
});
|
||||
@@ -237,17 +240,13 @@ function addGroup() {
|
||||
// Check if the user wants to add multiple groups (space or newline separated)
|
||||
// If so, split the input and store it in an array, however, do not split
|
||||
// group names enclosed in quotes
|
||||
var names = utils
|
||||
let names = utils
|
||||
.escapeHtml($("#new_name"))
|
||||
.val()
|
||||
.match(/(?:[^\s"]+|"[^"]*")+/g)
|
||||
.map(function (name) {
|
||||
return name.replaceAll(/(^"|"$)/g, "");
|
||||
});
|
||||
.map(name => name.replaceAll(/(^"|"$)/g, ""));
|
||||
// Remove empty elements
|
||||
names = names.filter(function (el) {
|
||||
return el !== "";
|
||||
});
|
||||
names = names.filter(el => el !== "");
|
||||
const groupStr = JSON.stringify(names);
|
||||
|
||||
utils.disableAll();
|
||||
@@ -261,17 +260,17 @@ function addGroup() {
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl + "/groups",
|
||||
url: document.body.dataset.apiurl + "/groups",
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
name: names,
|
||||
comment: comment,
|
||||
comment,
|
||||
enabled: true,
|
||||
}),
|
||||
success: function (data) {
|
||||
success(data) {
|
||||
utils.enableAll();
|
||||
utils.listsAlert("group", names, data);
|
||||
$("#new_name").val("");
|
||||
@@ -282,7 +281,7 @@ function addGroup() {
|
||||
// Update number of groups in the sidebar
|
||||
updateFtlInfo();
|
||||
},
|
||||
error: function (data, exception) {
|
||||
error(data, exception) {
|
||||
apiFailure(data);
|
||||
utils.enableAll();
|
||||
utils.showAlert("error", "", "Error while adding new group", data.responseText);
|
||||
@@ -300,8 +299,8 @@ function editGroup() {
|
||||
const enabled = tr.find("#enabled_" + id).is(":checked");
|
||||
const comment = tr.find("#comment_" + id).val();
|
||||
|
||||
var done = "edited";
|
||||
var notDone = "editing";
|
||||
let done = "edited";
|
||||
let notDone = "editing";
|
||||
switch (elem) {
|
||||
case "enabled_" + id:
|
||||
if (!enabled) {
|
||||
@@ -329,22 +328,22 @@ function editGroup() {
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Editing group...", oldName);
|
||||
$.ajax({
|
||||
url: apiUrl + "/groups/" + oldName,
|
||||
url: document.body.dataset.apiurl + "/groups/" + oldName,
|
||||
method: "put",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
name: name,
|
||||
comment: comment,
|
||||
enabled: enabled,
|
||||
name,
|
||||
comment,
|
||||
enabled,
|
||||
}),
|
||||
success: function (data) {
|
||||
success(data) {
|
||||
utils.enableAll();
|
||||
processGroupResult(data, "group", done, notDone);
|
||||
table.ajax.reload(null, false);
|
||||
},
|
||||
error: function (data, exception) {
|
||||
error(data, exception) {
|
||||
apiFailure(data);
|
||||
utils.enableAll();
|
||||
utils.showAlert(
|
||||
|
||||
+214
-162
@@ -5,11 +5,16 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, Chart:false, apiFailure:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false, REFRESH_INTERVAL: false, updateQueryFrequency: false */
|
||||
/* global utils:false, Chart:false, apiFailure:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false, REFRESH_INTERVAL: false, updateQueryFrequency: false */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Define global variables
|
||||
var timeLineChart, clientsChart;
|
||||
var queryTypePieChart, forwardDestinationPieChart;
|
||||
let timeLineChart;
|
||||
let clientsChart;
|
||||
let queryTypePieChart;
|
||||
let forwardDestinationPieChart;
|
||||
let privacyLevel = 0;
|
||||
|
||||
// Register the ChartDeferred plugin to all charts:
|
||||
Chart.register(ChartDeferred);
|
||||
@@ -18,11 +23,26 @@ Chart.defaults.set("plugins.deferred", {
|
||||
delay: 300,
|
||||
});
|
||||
|
||||
// Set the privacy level
|
||||
function initPrivacyLevel() {
|
||||
return $.ajax({
|
||||
url: document.body.dataset.apiurl + "/info/ftl",
|
||||
})
|
||||
.done(data => {
|
||||
privacyLevel = data.ftl.privacy_level;
|
||||
})
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
// Set privacy level to 0 by default if the request fails
|
||||
privacyLevel = 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Functions to update data in page
|
||||
|
||||
var failures = 0;
|
||||
let failures = 0;
|
||||
function updateQueriesOverTime() {
|
||||
$.getJSON(apiUrl + "/history", function (data) {
|
||||
$.getJSON(document.body.dataset.apiurl + "/history", data => {
|
||||
// Remove graph if there are no results (e.g. new
|
||||
// installation or privacy mode enabled)
|
||||
if (jQuery.isEmptyObject(data.history)) {
|
||||
@@ -34,20 +54,20 @@ function updateQueriesOverTime() {
|
||||
timeLineChart.data.labels = [];
|
||||
timeLineChart.data.datasets = [];
|
||||
|
||||
var labels = [
|
||||
const labels = [
|
||||
"Other DNS Queries",
|
||||
"Blocked DNS Queries",
|
||||
"Cached DNS Queries",
|
||||
"Forwarded DNS Queries",
|
||||
];
|
||||
var cachedColor = utils.getCSSval("queries-cached", "background-color");
|
||||
var blockedColor = utils.getCSSval("queries-blocked", "background-color");
|
||||
var permittedColor = utils.getCSSval("queries-permitted", "background-color");
|
||||
var otherColor = utils.getCSSval("queries-other", "background-color");
|
||||
var colors = [otherColor, blockedColor, cachedColor, permittedColor];
|
||||
const cachedColor = utils.getCSSval("queries-cached", "background-color");
|
||||
const blockedColor = utils.getCSSval("queries-blocked", "background-color");
|
||||
const permittedColor = utils.getCSSval("queries-permitted", "background-color");
|
||||
const otherColor = utils.getCSSval("queries-other", "background-color");
|
||||
const colors = [otherColor, blockedColor, cachedColor, permittedColor];
|
||||
|
||||
// Collect values and colors, and labels
|
||||
for (var i = 0; i < labels.length; i++) {
|
||||
for (const [i, label] of labels.entries()) {
|
||||
timeLineChart.data.datasets.push({
|
||||
data: [],
|
||||
// If we ran out of colors, make a random one
|
||||
@@ -55,68 +75,68 @@ function updateQueriesOverTime() {
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 5,
|
||||
pointHoverRadius: 5,
|
||||
label: labels[i],
|
||||
label,
|
||||
cubicInterpolationMode: "monotone",
|
||||
});
|
||||
}
|
||||
|
||||
// Add data for each dataset that is available
|
||||
data.history.forEach(function (item) {
|
||||
var timestamp = new Date(1000 * parseInt(item.timestamp, 10));
|
||||
for (const item of data.history) {
|
||||
const timestamp = new Date(1000 * Number.parseInt(item.timestamp, 10));
|
||||
|
||||
timeLineChart.data.labels.push(timestamp);
|
||||
var other = item.total - (item.blocked + item.cached + item.forwarded);
|
||||
const other = item.total - (item.blocked + item.cached + item.forwarded);
|
||||
timeLineChart.data.datasets[0].data.push(other);
|
||||
timeLineChart.data.datasets[1].data.push(item.blocked);
|
||||
timeLineChart.data.datasets[2].data.push(item.cached);
|
||||
timeLineChart.data.datasets[3].data.push(item.forwarded);
|
||||
});
|
||||
}
|
||||
|
||||
$("#queries-over-time .overlay").hide();
|
||||
timeLineChart.update();
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
failures = 0;
|
||||
utils.setTimer(updateQueriesOverTime, REFRESH_INTERVAL.history);
|
||||
})
|
||||
.fail(function () {
|
||||
.fail(() => {
|
||||
failures++;
|
||||
if (failures < 5) {
|
||||
// Try again ´only if this has not failed more than five times in a row
|
||||
utils.setTimer(updateQueriesOverTime, 0.1 * REFRESH_INTERVAL.history);
|
||||
}
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
function updateQueryTypesPie() {
|
||||
$.getJSON(apiUrl + "/stats/query_types", function (data) {
|
||||
var v = [],
|
||||
c = [],
|
||||
k = [],
|
||||
i = 0,
|
||||
sum = 0;
|
||||
$.getJSON(document.body.dataset.apiurl + "/stats/query_types", data => {
|
||||
const v = [];
|
||||
const c = [];
|
||||
const k = [];
|
||||
let i = 0;
|
||||
let sum = 0;
|
||||
|
||||
// Compute total number of queries
|
||||
Object.keys(data.types).forEach(function (item) {
|
||||
sum += data.types[item];
|
||||
});
|
||||
for (const value of Object.values(data.types)) {
|
||||
sum += value;
|
||||
}
|
||||
|
||||
// Fill chart with data (only include query types which appeared recently)
|
||||
Object.keys(data.types).forEach(function (item) {
|
||||
if (data.types[item] > 0) {
|
||||
v.push((100 * data.types[item]) / sum);
|
||||
for (const [item, value] of Object.entries(data.types)) {
|
||||
if (value > 0) {
|
||||
v.push((100 * value) / sum);
|
||||
c.push(THEME_COLORS[i % THEME_COLORS.length]);
|
||||
k.push(item);
|
||||
}
|
||||
|
||||
i++;
|
||||
});
|
||||
}
|
||||
|
||||
// Build a single dataset with the data to be pushed
|
||||
var dd = { data: v, backgroundColor: c };
|
||||
const dd = { data: v, backgroundColor: c };
|
||||
// and push it at once
|
||||
queryTypePieChart.data.datasets[0] = dd;
|
||||
queryTypePieChart.data.labels = k;
|
||||
@@ -125,16 +145,16 @@ function updateQueryTypesPie() {
|
||||
//https://www.chartjs.org/docs/latest/developers/updates.html#preventing-animations
|
||||
queryTypePieChart.update("none");
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
utils.setTimer(updateQueryTypesPie, REFRESH_INTERVAL.query_types);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
function updateClientsOverTime() {
|
||||
$.getJSON(apiUrl + "/history/clients", function (data) {
|
||||
$.getJSON(document.body.dataset.apiurl + "/history/clients", data => {
|
||||
// Remove graph if there are no results (e.g. new
|
||||
// installation or privacy mode enabled)
|
||||
if (jQuery.isEmptyObject(data.history)) {
|
||||
@@ -143,12 +163,12 @@ function updateClientsOverTime() {
|
||||
}
|
||||
|
||||
let numClients = 0;
|
||||
const labels = [],
|
||||
clients = {};
|
||||
Object.keys(data.clients).forEach(function (ip) {
|
||||
const labels = [];
|
||||
const clients = {};
|
||||
for (const [ip, clientData] of Object.entries(data.clients)) {
|
||||
clients[ip] = numClients++;
|
||||
labels.push(data.clients[ip].name !== null ? data.clients[ip].name : ip);
|
||||
});
|
||||
labels.push(clientData.name !== null ? clientData.name : ip);
|
||||
}
|
||||
|
||||
// Remove possibly already existing data
|
||||
clientsChart.data.labels = [];
|
||||
@@ -161,7 +181,7 @@ function updateClientsOverTime() {
|
||||
backgroundColor:
|
||||
i < THEME_COLORS.length
|
||||
? THEME_COLORS[i]
|
||||
: "#" + (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6),
|
||||
: "#" + (0x1_00_00_00 + Math.random() * 0xff_ff_ff).toString(16).substr(1, 6),
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 5,
|
||||
pointHoverRadius: 5,
|
||||
@@ -172,62 +192,58 @@ function updateClientsOverTime() {
|
||||
|
||||
// Add data for each dataset that is available
|
||||
// We need to iterate over all time slots and fill in the data for each client
|
||||
Object.keys(data.history).forEach(function (item) {
|
||||
Object.keys(clients).forEach(function (client) {
|
||||
if (data.history[item].data[client] === undefined) {
|
||||
// If there is no data for this client in this timeslot, we push 0
|
||||
clientsChart.data.datasets[clients[client]].data.push(0);
|
||||
} else {
|
||||
// Otherwise, we push the data
|
||||
clientsChart.data.datasets[clients[client]].data.push(data.history[item].data[client]);
|
||||
}
|
||||
});
|
||||
});
|
||||
for (const item of Object.values(data.history)) {
|
||||
for (const [client, index] of Object.entries(clients)) {
|
||||
const clientData = item.data[client];
|
||||
// If there is no data for this client in this timeslot, we push 0, otherwise the data
|
||||
clientsChart.data.datasets[index].data.push(clientData === undefined ? 0 : clientData);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract data timestamps
|
||||
data.history.forEach(function (item) {
|
||||
var d = new Date(1000 * parseInt(item.timestamp, 10));
|
||||
for (const item of data.history) {
|
||||
const d = new Date(1000 * Number.parseInt(item.timestamp, 10));
|
||||
clientsChart.data.labels.push(d);
|
||||
});
|
||||
}
|
||||
|
||||
$("#clients .overlay").hide();
|
||||
clientsChart.update();
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
// Reload graph after 10 minutes
|
||||
failures = 0;
|
||||
utils.setTimer(updateClientsOverTime, REFRESH_INTERVAL.clients);
|
||||
})
|
||||
.fail(function () {
|
||||
.fail(() => {
|
||||
failures++;
|
||||
if (failures < 5) {
|
||||
// Try again only if this has not failed more than five times in a row
|
||||
utils.setTimer(updateClientsOverTime, 0.1 * REFRESH_INTERVAL.clients);
|
||||
}
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
var upstreams = {};
|
||||
const upstreams = {};
|
||||
function updateForwardDestinationsPie() {
|
||||
$.getJSON(apiUrl + "/stats/upstreams", function (data) {
|
||||
var v = [],
|
||||
c = [],
|
||||
k = [],
|
||||
i = 0,
|
||||
sum = 0,
|
||||
values = [];
|
||||
$.getJSON(document.body.dataset.apiurl + "/stats/upstreams", data => {
|
||||
const v = [];
|
||||
const c = [];
|
||||
const k = [];
|
||||
let i = 0;
|
||||
let sum = 0;
|
||||
const values = [];
|
||||
|
||||
// Compute total number of queries
|
||||
data.upstreams.forEach(function (item) {
|
||||
for (const item of data.upstreams) {
|
||||
sum += item.count;
|
||||
});
|
||||
}
|
||||
|
||||
// Collect values and colors
|
||||
data.upstreams.forEach(function (item) {
|
||||
var label = item.name !== null && item.name.length > 0 ? item.name : item.ip;
|
||||
for (const item of data.upstreams) {
|
||||
let label = item.name !== null && item.name.length > 0 ? item.name : item.ip;
|
||||
if (item.port > 0) {
|
||||
label += "#" + item.port;
|
||||
}
|
||||
@@ -238,19 +254,19 @@ function updateForwardDestinationsPie() {
|
||||
upstreams[label] += "#" + item.port;
|
||||
}
|
||||
|
||||
var percent = (100 * item.count) / sum;
|
||||
const percent = (100 * item.count) / sum;
|
||||
values.push([label, percent, THEME_COLORS[i++ % THEME_COLORS.length]]);
|
||||
});
|
||||
}
|
||||
|
||||
// Split data into individual arrays for the graphs
|
||||
values.forEach(function (value) {
|
||||
for (const value of values) {
|
||||
k.push(value[0]);
|
||||
v.push(value[1]);
|
||||
c.push(value[2]);
|
||||
});
|
||||
}
|
||||
|
||||
// Build a single dataset with the data to be pushed
|
||||
var dd = { data: v, backgroundColor: c };
|
||||
const dd = { data: v, backgroundColor: c };
|
||||
// and push it at once
|
||||
forwardDestinationPieChart.data.labels = k;
|
||||
forwardDestinationPieChart.data.datasets[0] = dd;
|
||||
@@ -262,45 +278,60 @@ function updateForwardDestinationsPie() {
|
||||
queryTypePieChart.update("none");
|
||||
forwardDestinationPieChart.update("none");
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
utils.setTimer(updateForwardDestinationsPie, REFRESH_INTERVAL.upstreams);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTopClientsTable(blocked) {
|
||||
let api, style, tablecontent, overlay, clienttable;
|
||||
let api;
|
||||
let style;
|
||||
let table;
|
||||
let tablecontent;
|
||||
let overlay;
|
||||
let clienttable;
|
||||
if (blocked) {
|
||||
api = apiUrl + "/stats/top_clients?blocked=true";
|
||||
api = document.body.dataset.apiurl + "/stats/top_clients?blocked=true";
|
||||
style = "queries-blocked";
|
||||
table = $("#client-frequency-blocked");
|
||||
tablecontent = $("#client-frequency-blocked td").parent();
|
||||
overlay = $("#client-frequency-blocked .overlay");
|
||||
clienttable = $("#client-frequency-blocked").find("tbody:last");
|
||||
} else {
|
||||
api = apiUrl + "/stats/top_clients";
|
||||
api = document.body.dataset.apiurl + "/stats/top_clients";
|
||||
style = "queries-permitted";
|
||||
table = $("#client-frequency");
|
||||
tablecontent = $("#client-frequency td").parent();
|
||||
overlay = $("#client-frequency .overlay");
|
||||
clienttable = $("#client-frequency").find("tbody:last");
|
||||
}
|
||||
|
||||
$.getJSON(api, function (data) {
|
||||
$.getJSON(api, data => {
|
||||
// Clear tables before filling them with data
|
||||
tablecontent.remove();
|
||||
let url, percentage;
|
||||
let url;
|
||||
let percentage;
|
||||
const sum = blocked ? data.blocked_queries : data.total_queries;
|
||||
|
||||
// Add note if there are no results (e.g. privacy mode enabled)
|
||||
// When there is no data...
|
||||
// a) remove table if there are no results (privacy mode enabled) or
|
||||
// b) add note if there are no results (e.g. new installation)
|
||||
if (jQuery.isEmptyObject(data.clients)) {
|
||||
clienttable.append('<tr><td colspan="3"><center>- No data -</center></td></tr>');
|
||||
overlay.hide();
|
||||
if (privacyLevel > 1) {
|
||||
table.remove();
|
||||
} else {
|
||||
clienttable.append('<tr><td colspan="3" class="text-center">- No data -</td></tr>');
|
||||
overlay.hide();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate table with content
|
||||
data.clients.forEach(function (client) {
|
||||
for (const client of data.clients) {
|
||||
// Sanitize client
|
||||
let clientname = client.name;
|
||||
if (clientname.length === 0) clientname = client.ip;
|
||||
@@ -321,46 +352,63 @@ function updateTopClientsTable(blocked) {
|
||||
utils.addTD(utils.colorBar(percentage, sum, style)) +
|
||||
"</tr> "
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Hide overlay
|
||||
overlay.hide();
|
||||
}).fail(function (data) {
|
||||
}).fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTopDomainsTable(blocked) {
|
||||
let api, style, tablecontent, overlay, domaintable;
|
||||
let api;
|
||||
let style;
|
||||
let table;
|
||||
let tablecontent;
|
||||
let overlay;
|
||||
let domaintable;
|
||||
if (blocked) {
|
||||
api = apiUrl + "/stats/top_domains?blocked=true";
|
||||
api = document.body.dataset.apiurl + "/stats/top_domains?blocked=true";
|
||||
style = "queries-blocked";
|
||||
table = $("#ad-frequency");
|
||||
tablecontent = $("#ad-frequency td").parent();
|
||||
overlay = $("#ad-frequency .overlay");
|
||||
domaintable = $("#ad-frequency").find("tbody:last");
|
||||
} else {
|
||||
api = apiUrl + "/stats/top_domains";
|
||||
api = document.body.dataset.apiurl + "/stats/top_domains";
|
||||
style = "queries-permitted";
|
||||
table = $("#domain-frequency");
|
||||
tablecontent = $("#domain-frequency td").parent();
|
||||
overlay = $("#domain-frequency .overlay");
|
||||
domaintable = $("#domain-frequency").find("tbody:last");
|
||||
}
|
||||
|
||||
$.getJSON(api, function (data) {
|
||||
$.getJSON(api, data => {
|
||||
// Clear tables before filling them with data
|
||||
tablecontent.remove();
|
||||
let url, domain, percentage, urlText;
|
||||
let url;
|
||||
let domain;
|
||||
let percentage;
|
||||
let urlText;
|
||||
const sum = blocked ? data.blocked_queries : data.total_queries;
|
||||
|
||||
// Add note if there are no results (e.g. privacy mode enabled)
|
||||
// When there is no data...
|
||||
// a) remove table if there are no results (privacy mode enabled) or
|
||||
// b) add note if there are no results (e.g. new installation)
|
||||
if (jQuery.isEmptyObject(data.domains)) {
|
||||
domaintable.append('<tr><td colspan="3"><center>- No data -</center></td></tr>');
|
||||
overlay.hide();
|
||||
if (privacyLevel > 0) {
|
||||
table.remove();
|
||||
} else {
|
||||
domaintable.append('<tr><td colspan="3" class="text-center">- No data -</td></tr>');
|
||||
overlay.hide();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate table with content
|
||||
data.domains.forEach(function (item) {
|
||||
for (const item of data.domains) {
|
||||
// Sanitize domain
|
||||
domain = encodeURIComponent(item.domain);
|
||||
// Substitute "." for empty domain lookups
|
||||
@@ -380,10 +428,10 @@ function updateTopDomainsTable(blocked) {
|
||||
utils.addTD(utils.colorBar(percentage, sum, style)) +
|
||||
"</tr> "
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
overlay.hide();
|
||||
}).fail(function (data) {
|
||||
}).fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
@@ -405,26 +453,26 @@ function updateTopLists() {
|
||||
utils.setTimer(updateTopLists, REFRESH_INTERVAL.top_lists);
|
||||
}
|
||||
|
||||
var previousCount = 0;
|
||||
var firstSummaryUpdate = true;
|
||||
let previousCount = 0;
|
||||
let firstSummaryUpdate = true;
|
||||
function updateSummaryData(runOnce = false) {
|
||||
$.getJSON(apiUrl + "/stats/summary", function (data) {
|
||||
var intl = new Intl.NumberFormat();
|
||||
const newCount = parseInt(data.queries.total, 10);
|
||||
$.getJSON(document.body.dataset.apiurl + "/stats/summary", data => {
|
||||
const intl = new Intl.NumberFormat();
|
||||
const newCount = Number.parseInt(data.queries.total, 10);
|
||||
|
||||
$("span#dns_queries").text(intl.format(newCount));
|
||||
$("span#active_clients").text(intl.format(parseInt(data.clients.active, 10)));
|
||||
$("span#active_clients").text(intl.format(Number.parseInt(data.clients.active, 10)));
|
||||
$("a#total_clients").attr(
|
||||
"title",
|
||||
intl.format(parseInt(data.clients.total, 10)) + " total clients"
|
||||
intl.format(Number.parseInt(data.clients.total, 10)) + " total clients"
|
||||
);
|
||||
$("span#blocked_queries").text(intl.format(parseFloat(data.queries.blocked)));
|
||||
var formattedPercentage = utils.toPercent(data.queries.percent_blocked, 1);
|
||||
$("span#blocked_queries").text(intl.format(Number.parseFloat(data.queries.blocked)));
|
||||
const formattedPercentage = utils.toPercent(data.queries.percent_blocked, 1);
|
||||
$("span#percent_blocked").text(formattedPercentage);
|
||||
updateQueryFrequency(intl, data.queries.frequency);
|
||||
|
||||
const lastupdate = parseInt(data.gravity.last_update, 10);
|
||||
var updatetxt = "Lists were never updated";
|
||||
const lastupdate = Number.parseInt(data.gravity.last_update, 10);
|
||||
let updatetxt = "Lists were never updated";
|
||||
if (lastupdate > 0) {
|
||||
updatetxt =
|
||||
"Lists updated " +
|
||||
@@ -434,7 +482,7 @@ function updateSummaryData(runOnce = false) {
|
||||
")";
|
||||
}
|
||||
|
||||
const gravityCount = parseInt(data.gravity.domains_being_blocked, 10);
|
||||
const gravityCount = Number.parseInt(data.gravity.domains_being_blocked, 10);
|
||||
if (gravityCount < 0) {
|
||||
// Error. Change the title text and show the error code in parentheses
|
||||
updatetxt = "Error! Update gravity to reset this value.";
|
||||
@@ -460,10 +508,10 @@ function updateSummaryData(runOnce = false) {
|
||||
previousCount = newCount;
|
||||
firstSummaryUpdate = false;
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
if (!runOnce) utils.setTimer(updateSummaryData, REFRESH_INTERVAL.summary);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
utils.setTimer(updateSummaryData, 3 * REFRESH_INTERVAL.summary);
|
||||
apiFailure(data);
|
||||
});
|
||||
@@ -473,14 +521,14 @@ function labelWithPercentage(tooltipLabel, skipZero = false) {
|
||||
// Sum all queries for the current time by iterating over all keys in the
|
||||
// current dataset
|
||||
let sum = 0;
|
||||
const keys = Object.keys(tooltipLabel.parsed._stacks.y);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (tooltipLabel.parsed._stacks.y[i] === undefined) continue;
|
||||
sum += parseInt(tooltipLabel.parsed._stacks.y[i], 10);
|
||||
for (const value of Object.values(tooltipLabel.parsed._stacks.y)) {
|
||||
if (value === undefined) continue;
|
||||
const num = Number.parseInt(value, 10);
|
||||
if (num) sum += num;
|
||||
}
|
||||
|
||||
let percentage = 0;
|
||||
const data = parseInt(tooltipLabel.parsed._stacks.y[tooltipLabel.datasetIndex], 10);
|
||||
const data = Number.parseInt(tooltipLabel.parsed._stacks.y[tooltipLabel.datasetIndex], 10);
|
||||
if (sum > 0) {
|
||||
percentage = (100 * data) / sum;
|
||||
}
|
||||
@@ -496,7 +544,7 @@ function labelWithPercentage(tooltipLabel, skipZero = false) {
|
||||
);
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
// Pull in data via AJAX
|
||||
updateSummaryData();
|
||||
|
||||
@@ -522,14 +570,14 @@ $(function () {
|
||||
enabled: true,
|
||||
},
|
||||
mode: "y",
|
||||
onZoom: function ({ chart, trigger }) {
|
||||
onZoom({ chart, trigger }) {
|
||||
if (trigger === "api") {
|
||||
// Ignore onZoom triggered by the chart.zoomScale api call below
|
||||
return;
|
||||
}
|
||||
|
||||
// The first time the chart is zoomed, save the maximum initial scale bound
|
||||
if (!chart.absMax) chart.absMax = chart.getInitialScaleBounds().y.max;
|
||||
chart.absMax ||= chart.getInitialScaleBounds().y.max;
|
||||
// Calculate the maximum value to be shown for the current zoom level
|
||||
const zoomMax = chart.absMax / chart.getZoomLevel();
|
||||
// Update the y axis scale
|
||||
@@ -570,9 +618,9 @@ $(function () {
|
||||
},
|
||||
};
|
||||
|
||||
var gridColor = utils.getCSSval("graphs-grid", "background-color");
|
||||
var ticksColor = utils.getCSSval("graphs-ticks", "color");
|
||||
var ctx = document.getElementById("queryOverTimeChart").getContext("2d");
|
||||
const gridColor = utils.getCSSval("graphs-grid", "background-color");
|
||||
const ticksColor = utils.getCSSval("graphs-ticks", "color");
|
||||
let ctx = document.getElementById("queryOverTimeChart").getContext("2d");
|
||||
timeLineChart = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
@@ -594,20 +642,20 @@ $(function () {
|
||||
enabled: true,
|
||||
intersect: false,
|
||||
yAlign: "bottom",
|
||||
itemSort: function (a, b) {
|
||||
itemSort(a, b) {
|
||||
return b.datasetIndex - a.datasetIndex;
|
||||
},
|
||||
callbacks: {
|
||||
title: function (tooltipTitle) {
|
||||
var label = tooltipTitle[0].label;
|
||||
var time = label.match(/(\d?\d):?(\d?\d?)/);
|
||||
var h = parseInt(time[1], 10);
|
||||
var m = parseInt(time[2], 10) || 0;
|
||||
var from = utils.padNumber(h) + ":" + utils.padNumber(m - 5) + ":00";
|
||||
var to = utils.padNumber(h) + ":" + utils.padNumber(m + 4) + ":59";
|
||||
title(tooltipTitle) {
|
||||
const label = tooltipTitle[0].label;
|
||||
const time = label.match(/(\d?\d):?(\d?\d?)/);
|
||||
const h = Number.parseInt(time[1], 10);
|
||||
const m = Number.parseInt(time[2], 10) || 0;
|
||||
const from = utils.padNumber(h) + ":" + utils.padNumber(m - 5) + ":00";
|
||||
const to = utils.padNumber(h) + ":" + utils.padNumber(m + 4) + ":59";
|
||||
return "Queries from " + from + " to " + to;
|
||||
},
|
||||
label: function (tooltipLabel) {
|
||||
label(tooltipLabel) {
|
||||
return labelWithPercentage(tooltipLabel);
|
||||
},
|
||||
},
|
||||
@@ -672,7 +720,7 @@ $(function () {
|
||||
updateQueriesOverTime();
|
||||
|
||||
// Create / load "Top Clients over Time" only if authorized
|
||||
var clientsChartEl = document.getElementById("clientsChart");
|
||||
const clientsChartEl = document.getElementById("clientsChart");
|
||||
if (clientsChartEl) {
|
||||
ctx = clientsChartEl.getContext("2d");
|
||||
clientsChart = new Chart(ctx, {
|
||||
@@ -698,20 +746,20 @@ $(function () {
|
||||
intersect: false,
|
||||
external: customTooltips,
|
||||
yAlign: "top",
|
||||
itemSort: function (a, b) {
|
||||
itemSort(a, b) {
|
||||
return b.raw - a.raw;
|
||||
},
|
||||
callbacks: {
|
||||
title: function (tooltipTitle) {
|
||||
var label = tooltipTitle[0].label;
|
||||
var time = label.match(/(\d?\d):?(\d?\d?)/);
|
||||
var h = parseInt(time[1], 10);
|
||||
var m = parseInt(time[2], 10) || 0;
|
||||
var from = utils.padNumber(h) + ":" + utils.padNumber(m - 5) + ":00";
|
||||
var to = utils.padNumber(h) + ":" + utils.padNumber(m + 4) + ":59";
|
||||
title(tooltipTitle) {
|
||||
const label = tooltipTitle[0].label;
|
||||
const time = label.match(/(\d?\d):?(\d?\d?)/);
|
||||
const h = Number.parseInt(time[1], 10);
|
||||
const m = Number.parseInt(time[2], 10) || 0;
|
||||
const from = utils.padNumber(h) + ":" + utils.padNumber(m - 5) + ":00";
|
||||
const to = utils.padNumber(h) + ":" + utils.padNumber(m + 4) + ":59";
|
||||
return "Client activity from " + from + " to " + to;
|
||||
},
|
||||
label: function (tooltipLabel) {
|
||||
label(tooltipLabel) {
|
||||
return labelWithPercentage(tooltipLabel, true);
|
||||
},
|
||||
},
|
||||
@@ -779,10 +827,14 @@ $(function () {
|
||||
updateClientsOverTime();
|
||||
}
|
||||
|
||||
updateTopLists();
|
||||
// Initialize privacy level before loading any data that depends on it
|
||||
initPrivacyLevel().then(() => {
|
||||
// After privacy level is initialized, load the top lists
|
||||
updateTopLists();
|
||||
});
|
||||
|
||||
$("#queryOverTimeChart").on("click", function (evt) {
|
||||
var activePoints = timeLineChart.getElementsAtEventForMode(
|
||||
$("#queryOverTimeChart").on("click", evt => {
|
||||
const activePoints = timeLineChart.getElementsAtEventForMode(
|
||||
evt,
|
||||
"nearest",
|
||||
{ intersect: true },
|
||||
@@ -790,21 +842,21 @@ $(function () {
|
||||
);
|
||||
if (activePoints.length > 0) {
|
||||
//get the internal index
|
||||
var clickedElementindex = activePoints[0].index;
|
||||
const clickedElementindex = activePoints[0].index;
|
||||
//get specific label by index
|
||||
var label = timeLineChart.data.labels[clickedElementindex];
|
||||
const label = timeLineChart.data.labels[clickedElementindex];
|
||||
|
||||
//get value by index
|
||||
var from = label / 1000 - 300;
|
||||
var until = label / 1000 + 300;
|
||||
const from = label / 1000 - 300;
|
||||
const until = label / 1000 + 300;
|
||||
globalThis.location.href = "queries?from=" + from + "&until=" + until;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#clientsChart").on("click", function (evt) {
|
||||
var activePoints = clientsChart.getElementsAtEventForMode(
|
||||
$("#clientsChart").on("click", evt => {
|
||||
const activePoints = clientsChart.getElementsAtEventForMode(
|
||||
evt,
|
||||
"nearest",
|
||||
{ intersect: true },
|
||||
@@ -812,14 +864,14 @@ $(function () {
|
||||
);
|
||||
if (activePoints.length > 0) {
|
||||
//get the internal index
|
||||
var clickedElementindex = activePoints[0].index;
|
||||
const clickedElementindex = activePoints[0].index;
|
||||
|
||||
//get specific label by index
|
||||
var label = clientsChart.data.labels[clickedElementindex];
|
||||
const label = clientsChart.data.labels[clickedElementindex];
|
||||
|
||||
//get value by index
|
||||
var from = label / 1000 - 300;
|
||||
var until = label / 1000 + 300;
|
||||
const from = label / 1000 - 300;
|
||||
const until = label / 1000 + 300;
|
||||
globalThis.location.href = "queries?from=" + from + "&until=" + until;
|
||||
}
|
||||
|
||||
@@ -855,7 +907,7 @@ $(function () {
|
||||
enabled: false,
|
||||
external: customTooltips,
|
||||
callbacks: {
|
||||
title: function () {
|
||||
title() {
|
||||
return "Query type";
|
||||
},
|
||||
label: doughnutTooltip,
|
||||
@@ -901,7 +953,7 @@ $(function () {
|
||||
enabled: false,
|
||||
external: customTooltips,
|
||||
callbacks: {
|
||||
title: function () {
|
||||
title() {
|
||||
return "Upstream server";
|
||||
},
|
||||
label: doughnutTooltip,
|
||||
@@ -920,11 +972,11 @@ $(function () {
|
||||
});
|
||||
|
||||
//destroy all chartjs customTooltips on window resize
|
||||
window.addEventListener("resize", function () {
|
||||
window.addEventListener("resize", () => {
|
||||
$(".chartjs-tooltip").remove();
|
||||
});
|
||||
|
||||
// Tooltips
|
||||
$(function () {
|
||||
$(() => {
|
||||
$('[data-toggle="tooltip"]').tooltip({ html: true, container: "body" });
|
||||
});
|
||||
|
||||
+88
-90
@@ -5,14 +5,16 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils: false, apiUrl: false */
|
||||
/* global utils: false */
|
||||
|
||||
$(function () {
|
||||
"use strict";
|
||||
|
||||
$(() => {
|
||||
$.ajax({
|
||||
url: apiUrl + "/network/gateway",
|
||||
url: document.body.dataset.apiurl + "/network/gateway",
|
||||
data: { detailed: true },
|
||||
}).done(function (data) {
|
||||
var intl = new Intl.NumberFormat();
|
||||
}).done(data => {
|
||||
const intl = new Intl.NumberFormat();
|
||||
const gateway = data.gateway;
|
||||
// Get all objects in gateway that has family == "inet"
|
||||
const inet = gateway.find(obj => obj.family === "inet");
|
||||
@@ -28,18 +30,18 @@ $(function () {
|
||||
gateways.add(inet6.gateway);
|
||||
}
|
||||
|
||||
var interfaces = {};
|
||||
var masterInterfaces = {};
|
||||
const interfaces = {};
|
||||
const masterInterfaces = {};
|
||||
|
||||
// For each interface in data.interface, create a new object and push it to json
|
||||
data.interfaces.forEach(function (interface) {
|
||||
const carrierColor = interface.carrier ? "text-green" : "text-red";
|
||||
let stateText = interface.state.toUpperCase();
|
||||
if (stateText === "UNKNOWN" && interface.flags !== undefined && interface.flags.length > 0) {
|
||||
if (interface.flags.includes("pointopoint")) {
|
||||
for (const iface of data.interfaces) {
|
||||
const carrierColor = iface.carrier ? "text-green" : "text-red";
|
||||
let stateText = iface.state.toUpperCase();
|
||||
if (stateText === "UNKNOWN" && iface.flags !== undefined && iface.flags.length > 0) {
|
||||
if (iface.flags.includes("pointopoint")) {
|
||||
// WireGuards, etc. -> the typo is intentional
|
||||
stateText = "P2P";
|
||||
} else if (interface.flags.includes("loopback")) {
|
||||
} else if (iface.flags.includes("loopback")) {
|
||||
// Loopback interfaces
|
||||
stateText = "LOOPBACK";
|
||||
}
|
||||
@@ -48,18 +50,18 @@ $(function () {
|
||||
const status = `<span class="${carrierColor}">${stateText}</span>`;
|
||||
|
||||
let master = null;
|
||||
if (interface.master !== undefined) {
|
||||
if (iface.master !== undefined) {
|
||||
// Find interface.master in data.interfaces
|
||||
master = data.interfaces.find(obj => obj.index === interface.master).name;
|
||||
master = data.interfaces.find(obj => obj.index === iface.master).name;
|
||||
}
|
||||
|
||||
// Show an icon for indenting slave interfaces
|
||||
const indentIcon =
|
||||
master === null ? "" : "<span class='child-interface-icon'> ⤷</span> ";
|
||||
|
||||
var obj = {
|
||||
text: indentIcon + interface.name + " - " + status,
|
||||
class: gateways.has(interface.name) ? "text-bold" : null,
|
||||
const obj = {
|
||||
text: indentIcon + iface.name + " - " + status,
|
||||
class: gateways.has(iface.name) ? "text-bold" : null,
|
||||
icon: master === null ? "fa fa-network-wired fa-fw" : "",
|
||||
nodes: [],
|
||||
};
|
||||
@@ -71,57 +73,56 @@ $(function () {
|
||||
});
|
||||
|
||||
if (master in masterInterfaces) {
|
||||
masterInterfaces[master].push(interface.name);
|
||||
masterInterfaces[master].push(iface.name);
|
||||
} else {
|
||||
masterInterfaces[master] = [interface.name];
|
||||
masterInterfaces[master] = [iface.name];
|
||||
}
|
||||
}
|
||||
|
||||
if (interface.speed) {
|
||||
if (iface.speed) {
|
||||
obj.nodes.push({
|
||||
text: "Speed: " + intl.format(interface.speed) + " Mbit/s",
|
||||
text: "Speed: " + intl.format(iface.speed) + " Mbit/s",
|
||||
icon: "fa fa-tachometer-alt fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.type !== undefined) {
|
||||
if (iface.type !== undefined) {
|
||||
obj.nodes.push({
|
||||
text: "Type: " + utils.escapeHtml(interface.type),
|
||||
text: "Type: " + utils.escapeHtml(iface.type),
|
||||
icon: "fa fa-network-wired fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.flags !== undefined && interface.flags.length > 0) {
|
||||
if (iface.flags !== undefined && iface.flags.length > 0) {
|
||||
obj.nodes.push({
|
||||
text: "Flags: " + utils.escapeHtml(interface.flags.join(", ")),
|
||||
text: "Flags: " + utils.escapeHtml(iface.flags.join(", ")),
|
||||
icon: "fa fa-flag fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.address !== undefined) {
|
||||
if (iface.address !== undefined) {
|
||||
let extra = "";
|
||||
if (interface.perm_address !== undefined && interface.perm_address !== interface.address) {
|
||||
extra = " (permanent: <code>" + utils.escapeHtml(interface.perm_address) + "</code>)";
|
||||
if (iface.perm_address !== undefined && iface.perm_address !== iface.address) {
|
||||
extra = " (permanent: <code>" + utils.escapeHtml(iface.perm_address) + "</code>)";
|
||||
}
|
||||
|
||||
obj.nodes.push({
|
||||
text:
|
||||
"Hardware address: <code>" + utils.escapeHtml(interface.address) + "</code>" + extra,
|
||||
text: "Hardware address: <code>" + utils.escapeHtml(iface.address) + "</code>" + extra,
|
||||
icon: "fa fa-map-marker-alt fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.addresses !== undefined) {
|
||||
if (iface.addresses !== undefined) {
|
||||
const addrs = {
|
||||
text:
|
||||
interface.addresses.length +
|
||||
(interface.addresses.length === 1 ? " address" : " addresses") +
|
||||
iface.addresses.length +
|
||||
(iface.addresses.length === 1 ? " address" : " addresses") +
|
||||
" connected to interface",
|
||||
icon: "fa fa-map-marker-alt fa-fw",
|
||||
nodes: [],
|
||||
};
|
||||
|
||||
for (const addr of interface.addresses) {
|
||||
for (const addr of iface.addresses) {
|
||||
let extraaddr = "";
|
||||
if (addr.prefixlen !== undefined) {
|
||||
extraaddr += " / <code>" + addr.prefixlen + "</code>";
|
||||
@@ -179,7 +180,7 @@ $(function () {
|
||||
|
||||
if (addr.prefered !== undefined) {
|
||||
const pref =
|
||||
addr.prefered === 4294967295 ? "forever" : intl.format(addr.prefered) + " s";
|
||||
addr.prefered === 4_294_967_295 ? "forever" : intl.format(addr.prefered) + " s";
|
||||
jaddr.nodes.push({
|
||||
text: "Preferred lifetime: " + pref,
|
||||
icon: "fa fa-clock fa-fw",
|
||||
@@ -187,7 +188,7 @@ $(function () {
|
||||
}
|
||||
|
||||
if (addr.valid !== undefined) {
|
||||
const valid = addr.valid === 4294967295 ? "forever" : intl.format(addr.valid) + " s";
|
||||
const valid = addr.valid === 4_294_967_295 ? "forever" : intl.format(addr.valid) + " s";
|
||||
jaddr.nodes.push({
|
||||
text: "Valid lifetime: " + valid,
|
||||
icon: "fa fa-clock fa-fw",
|
||||
@@ -214,107 +215,107 @@ $(function () {
|
||||
obj.nodes.push(addrs);
|
||||
}
|
||||
|
||||
if (interface.stats !== undefined) {
|
||||
if (iface.stats !== undefined) {
|
||||
const stats = {
|
||||
text: "Statistics",
|
||||
icon: "fa fa-chart-line fa-fw",
|
||||
expanded: false,
|
||||
nodes: [],
|
||||
};
|
||||
if (interface.stats.rx_bytes !== undefined) {
|
||||
if (iface.stats.rx_bytes !== undefined) {
|
||||
stats.nodes.push({
|
||||
text:
|
||||
"RX bytes: " +
|
||||
intl.format(interface.stats.rx_bytes.value) +
|
||||
intl.format(iface.stats.rx_bytes.value) +
|
||||
" " +
|
||||
interface.stats.rx_bytes.unit,
|
||||
iface.stats.rx_bytes.unit,
|
||||
icon: "fa fa-download fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.stats.tx_bytes !== undefined) {
|
||||
if (iface.stats.tx_bytes !== undefined) {
|
||||
stats.nodes.push({
|
||||
text:
|
||||
"TX bytes: " +
|
||||
intl.format(interface.stats.tx_bytes.value) +
|
||||
intl.format(iface.stats.tx_bytes.value) +
|
||||
" " +
|
||||
interface.stats.tx_bytes.unit,
|
||||
iface.stats.tx_bytes.unit,
|
||||
icon: "fa fa-upload fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.stats.rx_packets !== undefined) {
|
||||
if (iface.stats.rx_packets !== undefined) {
|
||||
stats.nodes.push({
|
||||
text: "RX packets: " + intl.format(interface.stats.rx_packets),
|
||||
text: "RX packets: " + intl.format(iface.stats.rx_packets),
|
||||
icon: "fa fa-download fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.stats.rx_errors !== undefined) {
|
||||
if (iface.stats.rx_errors !== undefined) {
|
||||
stats.nodes.push({
|
||||
text:
|
||||
"RX errors: " +
|
||||
intl.format(interface.stats.rx_errors) +
|
||||
intl.format(iface.stats.rx_errors) +
|
||||
" (" +
|
||||
((interface.stats.rx_errors / interface.stats.rx_packets) * 100).toFixed(1) +
|
||||
((iface.stats.rx_errors / iface.stats.rx_packets) * 100).toFixed(1) +
|
||||
"%)",
|
||||
icon: "fa fa-download fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.stats.rx_dropped !== undefined) {
|
||||
if (iface.stats.rx_dropped !== undefined) {
|
||||
stats.nodes.push({
|
||||
text:
|
||||
"RX dropped: " +
|
||||
intl.format(interface.stats.rx_dropped) +
|
||||
intl.format(iface.stats.rx_dropped) +
|
||||
" (" +
|
||||
((interface.stats.rx_dropped / interface.stats.rx_packets) * 100).toFixed(1) +
|
||||
((iface.stats.rx_dropped / iface.stats.rx_packets) * 100).toFixed(1) +
|
||||
"%)",
|
||||
icon: "fa fa-download fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.stats.tx_packets !== undefined) {
|
||||
if (iface.stats.tx_packets !== undefined) {
|
||||
stats.nodes.push({
|
||||
text: "TX packets: " + intl.format(interface.stats.tx_packets),
|
||||
text: "TX packets: " + intl.format(iface.stats.tx_packets),
|
||||
icon: "fa fa-upload fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.stats.tx_errors !== undefined) {
|
||||
if (iface.stats.tx_errors !== undefined) {
|
||||
stats.nodes.push({
|
||||
text:
|
||||
"TX errors: " +
|
||||
intl.format(interface.stats.tx_errors) +
|
||||
intl.format(iface.stats.tx_errors) +
|
||||
" (" +
|
||||
((interface.stats.tx_errors / interface.stats.tx_packets) * 100).toFixed(1) +
|
||||
((iface.stats.tx_errors / iface.stats.tx_packets) * 100).toFixed(1) +
|
||||
"%)",
|
||||
icon: "fa fa-upload fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.stats.tx_dropped !== undefined) {
|
||||
if (iface.stats.tx_dropped !== undefined) {
|
||||
stats.nodes.push({
|
||||
text:
|
||||
"TX dropped: " +
|
||||
intl.format(interface.stats.tx_dropped) +
|
||||
intl.format(iface.stats.tx_dropped) +
|
||||
" (" +
|
||||
((interface.stats.tx_dropped / interface.stats.tx_packets) * 100).toFixed(1) +
|
||||
((iface.stats.tx_dropped / iface.stats.tx_packets) * 100).toFixed(1) +
|
||||
"%)",
|
||||
icon: "fa fa-upload fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.stats.multicast !== undefined) {
|
||||
if (iface.stats.multicast !== undefined) {
|
||||
stats.nodes.push({
|
||||
text: "Multicast: " + intl.format(interface.stats.multicast),
|
||||
text: "Multicast: " + intl.format(iface.stats.multicast),
|
||||
icon: "fa fa-broadcast-tower fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.stats.collisions !== undefined) {
|
||||
if (iface.stats.collisions !== undefined) {
|
||||
stats.nodes.push({
|
||||
text: "Collisions: " + intl.format(interface.stats.collisions),
|
||||
text: "Collisions: " + intl.format(iface.stats.collisions),
|
||||
icon: "fa fa-exchange-alt fa-fw",
|
||||
});
|
||||
}
|
||||
@@ -333,81 +334,78 @@ $(function () {
|
||||
{
|
||||
text:
|
||||
"Carrier: " +
|
||||
(interface.carrier
|
||||
(iface.carrier
|
||||
? "<span class='text-green'>Connected</span>"
|
||||
: "<span class='text-red'>Disconnected</span>"),
|
||||
icon: "fa fa-link fa-fw",
|
||||
},
|
||||
{
|
||||
text: "State: " + utils.escapeHtml(interface.state.toUpperCase()),
|
||||
text: "State: " + utils.escapeHtml(iface.state.toUpperCase()),
|
||||
icon: "fa fa-server fa-fw",
|
||||
}
|
||||
);
|
||||
|
||||
if (interface.parent_dev_name !== undefined) {
|
||||
if (iface.parent_dev_name !== undefined) {
|
||||
let extra = "";
|
||||
if (interface.parent_dev_bus_name !== undefined) {
|
||||
extra = " @ " + utils.escapeHtml(interface.parent_dev_bus_name);
|
||||
if (iface.parent_dev_bus_name !== undefined) {
|
||||
extra = " @ " + utils.escapeHtml(iface.parent_dev_bus_name);
|
||||
}
|
||||
|
||||
furtherDetails.nodes.push({
|
||||
text:
|
||||
"Parent device: <code>" +
|
||||
utils.escapeHtml(interface.parent_dev_name) +
|
||||
extra +
|
||||
"</code>",
|
||||
"Parent device: <code>" + utils.escapeHtml(iface.parent_dev_name) + extra + "</code>",
|
||||
icon: "fa fa-network-wired fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.carrier_changes !== undefined) {
|
||||
if (iface.carrier_changes !== undefined) {
|
||||
furtherDetails.nodes.push({
|
||||
text: "Carrier changes: " + intl.format(interface.carrier_changes),
|
||||
text: "Carrier changes: " + intl.format(iface.carrier_changes),
|
||||
icon: "fa fa-exchange-alt fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.broadcast) {
|
||||
if (iface.broadcast) {
|
||||
furtherDetails.nodes.push({
|
||||
text: "Broadcast: <code>" + utils.escapeHtml(interface.broadcast) + "</code>",
|
||||
text: "Broadcast: <code>" + utils.escapeHtml(iface.broadcast) + "</code>",
|
||||
icon: "fa fa-broadcast-tower fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.mtu) {
|
||||
if (iface.mtu) {
|
||||
let extra = "";
|
||||
if (interface.min_mtu !== undefined && interface.max_mtu !== undefined) {
|
||||
if (iface.min_mtu !== undefined && iface.max_mtu !== undefined) {
|
||||
extra +=
|
||||
" (min: " +
|
||||
intl.format(interface.min_mtu) +
|
||||
intl.format(iface.min_mtu) +
|
||||
" bytes, max: " +
|
||||
intl.format(interface.max_mtu) +
|
||||
intl.format(iface.max_mtu) +
|
||||
" bytes)";
|
||||
}
|
||||
|
||||
furtherDetails.nodes.push({
|
||||
text: "MTU: " + intl.format(interface.mtu) + " bytes" + extra,
|
||||
text: "MTU: " + intl.format(iface.mtu) + " bytes" + extra,
|
||||
icon: "fa fa-arrows-alt-h fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.txqlen) {
|
||||
if (iface.txqlen) {
|
||||
furtherDetails.nodes.push({
|
||||
text: "TX queue length: " + intl.format(interface.txqlen),
|
||||
text: "TX queue length: " + intl.format(iface.txqlen),
|
||||
icon: "fa fa-file-upload fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.promiscuity !== undefined) {
|
||||
if (iface.promiscuity !== undefined) {
|
||||
furtherDetails.nodes.push({
|
||||
text: "Promiscuity mode: " + (interface.promiscuity ? "Yes" : "No"),
|
||||
text: "Promiscuity mode: " + (iface.promiscuity ? "Yes" : "No"),
|
||||
icon: "fa fa-eye fa-fw",
|
||||
});
|
||||
}
|
||||
|
||||
if (interface.qdisc !== undefined) {
|
||||
if (iface.qdisc !== undefined) {
|
||||
furtherDetails.nodes.push({
|
||||
text: "Scheduler: " + utils.escapeHtml(interface.qdisc),
|
||||
text: "Scheduler: " + utils.escapeHtml(iface.qdisc),
|
||||
icon: "fa fa-network-wired fa-fw",
|
||||
});
|
||||
}
|
||||
@@ -416,8 +414,8 @@ $(function () {
|
||||
obj.nodes.push(furtherDetails);
|
||||
}
|
||||
|
||||
interfaces[interface.name] = obj;
|
||||
});
|
||||
interfaces[iface.name] = obj;
|
||||
}
|
||||
|
||||
// Sort interfaces based on masterInterfaces. If an item is found in
|
||||
// masterInterfaces, it should be placed after the master interface
|
||||
|
||||
@@ -5,31 +5,34 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
"use strict";
|
||||
|
||||
// This code has been taken from
|
||||
// https://datatables.net/plug-ins/sorting/ip-address
|
||||
// and was modified by the Pi-hole team to support
|
||||
// CIDR notation and be more robust against invalid
|
||||
// input data (like empty IP addresses)
|
||||
$.extend($.fn.dataTableExt.oSort, {
|
||||
"ip-address-pre": function (a) {
|
||||
"ip-address-pre"(a) {
|
||||
// Skip empty fields (IP address might have expired or
|
||||
// reassigned to a different device)
|
||||
if (!a || a.length === 0) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
var i, item;
|
||||
let i;
|
||||
let item;
|
||||
// Use the first IP in case there is a list of IPs
|
||||
// for a given device
|
||||
if (Array.isArray(a)) {
|
||||
a = a[0];
|
||||
}
|
||||
|
||||
var m = a.split("."),
|
||||
n = a.split(":"),
|
||||
x = "",
|
||||
xa = "",
|
||||
cidr = [];
|
||||
let m = a.split(".");
|
||||
let n = a.split(":");
|
||||
let x = "";
|
||||
let xa = "";
|
||||
let cidr = [];
|
||||
if (m.length === 4) {
|
||||
// IPV4 (possibly with CIDR)
|
||||
cidr = m[3].split("/");
|
||||
@@ -51,7 +54,7 @@ $.extend($.fn.dataTableExt.oSort, {
|
||||
}
|
||||
} else if (n.length > 0) {
|
||||
// IPV6 (possibly with CIDR)
|
||||
var count = 0;
|
||||
let count = 0;
|
||||
for (i = 0; i < n.length; i++) {
|
||||
item = n[i];
|
||||
|
||||
@@ -96,12 +99,12 @@ $.extend($.fn.dataTableExt.oSort, {
|
||||
|
||||
// Padding the ::
|
||||
n = xa.split(":");
|
||||
var paddDone = 0;
|
||||
let paddDone = 0;
|
||||
|
||||
for (i = 0; i < n.length; i++) {
|
||||
item = n[i];
|
||||
if (item.length === 0 && paddDone === 0) {
|
||||
for (var padding = 0; padding < 32 - count; padding++) {
|
||||
for (let padding = 0; padding < 32 - count; padding++) {
|
||||
x += "0";
|
||||
paddDone = 1;
|
||||
}
|
||||
@@ -127,11 +130,11 @@ $.extend($.fn.dataTableExt.oSort, {
|
||||
return x;
|
||||
},
|
||||
|
||||
"ip-address-asc": function (a, b) {
|
||||
"ip-address-asc"(a, b) {
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
},
|
||||
|
||||
"ip-address-desc": function (a, b) {
|
||||
"ip-address-desc"(a, b) {
|
||||
return a < b ? 1 : a > b ? -1 : 0;
|
||||
},
|
||||
});
|
||||
|
||||
+24
-26
@@ -5,14 +5,16 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl: false, NProgress:false */
|
||||
/* global utils:false, NProgress:false */
|
||||
|
||||
var _isLoginPage = true;
|
||||
"use strict";
|
||||
|
||||
globalThis._isLoginPage = true;
|
||||
|
||||
function redirect() {
|
||||
// Login succeeded or not needed (empty password)
|
||||
// Default: Send back to dashboard
|
||||
var target = ".";
|
||||
let target = ".";
|
||||
|
||||
// If DNS failure: send to Pi-hole diagnosis messages page
|
||||
if ($("#dns-failure-label").is(":visible")) {
|
||||
@@ -25,8 +27,8 @@ function redirect() {
|
||||
|
||||
function wrongPassword(isError = false, isSuccess = false, data = null) {
|
||||
if (isError) {
|
||||
let isErrorResponse = false,
|
||||
isInvalidTOTP = false;
|
||||
let isErrorResponse = false;
|
||||
let isInvalidTOTP = false;
|
||||
|
||||
// Reset hint and error message
|
||||
$("#error-message").text("");
|
||||
@@ -71,7 +73,9 @@ function wrongPassword(isError = false, isSuccess = false, data = null) {
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (isSuccess) {
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
$("#pw-field").addClass("has-success");
|
||||
$("#totp_input").addClass("has-success");
|
||||
} else {
|
||||
@@ -91,35 +95,29 @@ function doLogin(password) {
|
||||
NProgress.start();
|
||||
utils.disableAll();
|
||||
$.ajax({
|
||||
url: apiUrl + "/auth",
|
||||
url: document.body.dataset.apiurl + "/auth",
|
||||
method: "POST",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ password: password, totp: parseInt($("#totp").val(), 10) }),
|
||||
data: JSON.stringify({ password, totp: Number.parseInt($("#totp").val(), 10) }),
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
wrongPassword(false, true, data);
|
||||
NProgress.done();
|
||||
redirect();
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
wrongPassword(true, false, data);
|
||||
NProgress.done();
|
||||
utils.enableAll();
|
||||
});
|
||||
}
|
||||
|
||||
$("#loginform").submit(function (e) {
|
||||
$("#loginform").on("submit", event => {
|
||||
// Cancel the native submit event (prevent the form from being
|
||||
// submitted) because we want to do a two-step challenge-response login
|
||||
e.preventDefault();
|
||||
|
||||
// Check if cookie checkbox is enabled
|
||||
/* if (!$("#logincookie").is(":checked")) {
|
||||
alert("Please consent to using a login cookie to be able to log in. It is necessary to keep you logged in between page reloads. You can end the session by clicking on the logout button in the top right menu at any time.");
|
||||
return;
|
||||
}*/
|
||||
event.preventDefault();
|
||||
|
||||
doLogin($("#current-password").val());
|
||||
});
|
||||
@@ -129,7 +127,7 @@ $("#totp").on("input", function () {
|
||||
const code = $(this).val();
|
||||
const password = $("#current-password").val();
|
||||
if (code.length === 6 && password.length > 0) {
|
||||
$("#loginform").submit();
|
||||
$("#loginform").trigger("submit");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -139,7 +137,7 @@ $("#toggle-password").on("click", function () {
|
||||
$(".field-icon", this).toggleClass("fa-eye fa-eye-slash");
|
||||
|
||||
// Password field
|
||||
var $pwd = $("#current-password");
|
||||
const $pwd = $("#current-password");
|
||||
if ($pwd.attr("type") === "password") {
|
||||
$pwd.attr("type", "text");
|
||||
$pwd.attr("title", "Hide password");
|
||||
@@ -160,16 +158,16 @@ function showDNSfailure() {
|
||||
$("#login-box").addClass("error-box");
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
// Check if we need to login at all
|
||||
$.ajax({
|
||||
url: apiUrl + "/auth",
|
||||
url: document.body.dataset.apiurl + "/auth",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
// If we are already logged in, redirect to dashboard
|
||||
if (data.session.valid === true) redirect();
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
.fail(xhr => {
|
||||
const session = xhr.responseJSON.session;
|
||||
// If TOPT is enabled, show the input field and add the required attribute
|
||||
if (session.totp === true) {
|
||||
@@ -182,8 +180,8 @@ $(function () {
|
||||
|
||||
// Get information about HTTPS port and DNS status
|
||||
$.ajax({
|
||||
url: apiUrl + "/info/login",
|
||||
}).done(function (data) {
|
||||
url: document.body.dataset.apiurl + "/info/login",
|
||||
}).done(data => {
|
||||
if (data.dns === false) showDNSfailure();
|
||||
|
||||
// Generate HTTPS redirection link (only used if not already HTTPS)
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
|
||||
/* global utils:false */
|
||||
|
||||
"use strict";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const logoutButton = document.getElementById("logout-button");
|
||||
const logoutUrl = document.body.dataset.logoutUrl;
|
||||
const logoutUrl = document.body.dataset.webhome + "login";
|
||||
|
||||
logoutButton.addEventListener("click", event => {
|
||||
logoutButton?.addEventListener("click", event => {
|
||||
event.preventDefault();
|
||||
utils.doLogout(logoutUrl);
|
||||
});
|
||||
|
||||
+30
-24
@@ -5,18 +5,24 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils: false, apiUrl: false */
|
||||
var table,
|
||||
toasts = {};
|
||||
/* global utils: false */
|
||||
|
||||
$(function () {
|
||||
var ignoreNonfatal = localStorage
|
||||
"use strict";
|
||||
|
||||
let table;
|
||||
const toasts = {};
|
||||
|
||||
$(() => {
|
||||
const ignoreNonfatal = localStorage
|
||||
? localStorage.getItem("hideNonfatalDnsmasqWarnings_chkbox") === "true"
|
||||
: false;
|
||||
var url = apiUrl + "/info/messages" + (ignoreNonfatal ? "?filter_dnsmasq_warnings=true" : "");
|
||||
const url =
|
||||
document.body.dataset.apiurl +
|
||||
"/info/messages" +
|
||||
(ignoreNonfatal ? "?filter_dnsmasq_warnings=true" : "");
|
||||
table = $("#messagesTable").DataTable({
|
||||
ajax: {
|
||||
url: url,
|
||||
url,
|
||||
type: "GET",
|
||||
dataSrc: "messages",
|
||||
},
|
||||
@@ -34,7 +40,7 @@ $(function () {
|
||||
targets: 1,
|
||||
orderable: false,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
render() {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
@@ -43,19 +49,19 @@ $(function () {
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
drawCallback() {
|
||||
$('button[id^="deleteMessage_"]').on("click", deleteMessage);
|
||||
|
||||
// Hide buttons if all messages were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
const hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
rowCallback(row, data) {
|
||||
$(row).attr("data-id", data.id);
|
||||
var button =
|
||||
const button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteMessage_' +
|
||||
data.id +
|
||||
'" data-del-id="' +
|
||||
@@ -75,7 +81,7 @@ $(function () {
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
action() {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -83,7 +89,7 @@ $(function () {
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
action() {
|
||||
table.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -97,7 +103,7 @@ $(function () {
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
action() {
|
||||
// For each ".selected" row ...
|
||||
$("tr.selected").each(function () {
|
||||
// ... delete the row identified by "data-id".
|
||||
@@ -121,11 +127,11 @@ $(function () {
|
||||
},
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
stateSaveCallback(settings, data) {
|
||||
utils.stateSaveCallback("messages-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("messages-table");
|
||||
stateLoadCallback() {
|
||||
const data = utils.stateLoadCallback("messages-table");
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
@@ -138,7 +144,7 @@ $(function () {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
table.on("init select deselect", function () {
|
||||
table.on("init select deselect", () => {
|
||||
utils.changeTableButtonStates(table);
|
||||
});
|
||||
});
|
||||
@@ -152,15 +158,15 @@ function deleteMessage() {
|
||||
}
|
||||
|
||||
function delMsg(id) {
|
||||
id = parseInt(id, 10);
|
||||
id = Number.parseInt(id, 10);
|
||||
utils.disableAll();
|
||||
toasts[id] = utils.showAlert("info", "", "Deleting message...", "ID: " + id, null);
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl + "/info/messages/" + id,
|
||||
url: document.body.dataset.apiurl + "/info/messages/" + id,
|
||||
method: "DELETE",
|
||||
})
|
||||
.done(function (response) {
|
||||
.done(response => {
|
||||
utils.enableAll();
|
||||
if (response === undefined) {
|
||||
utils.showAlert(
|
||||
@@ -188,9 +194,9 @@ function delMsg(id) {
|
||||
utils.changeTableButtonStates(table);
|
||||
})
|
||||
.done(
|
||||
utils.checkMessages // Update icon warnings count
|
||||
utils.checkMessages() // Update icon warnings count
|
||||
)
|
||||
.fail(function (jqXHR, exception) {
|
||||
.fail((jqXHR, exception) => {
|
||||
utils.enableAll();
|
||||
utils.showAlert(
|
||||
"error",
|
||||
|
||||
+42
-42
@@ -5,9 +5,11 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, apiFailure:false */
|
||||
/* global utils:false, apiFailure:false */
|
||||
|
||||
var tableApi;
|
||||
"use strict";
|
||||
|
||||
let tableApi;
|
||||
|
||||
// How many IPs do we show at most per device?
|
||||
const MAXIPDISPLAY = 3;
|
||||
@@ -16,7 +18,7 @@ const DAY_IN_SECONDS = 24 * 60 * 60;
|
||||
function handleAjaxError(xhr, textStatus) {
|
||||
if (textStatus === "timeout") {
|
||||
alert("The server took too long to send the data.");
|
||||
} else if (xhr.responseText.indexOf("Connection refused") === -1) {
|
||||
} else if (!xhr.responseText.includes("Connection refused")) {
|
||||
alert("An unknown error occurred while loading the data.\n" + xhr.responseText);
|
||||
} else {
|
||||
alert("An error occurred while loading the data: Connection refused. Is FTL running?");
|
||||
@@ -32,7 +34,7 @@ function getTimestamp() {
|
||||
}
|
||||
|
||||
function valueToHex(c) {
|
||||
var hex = Math.round(c).toString(16);
|
||||
const hex = Math.round(c).toString(16);
|
||||
return hex.length === 1 ? "0" + hex : hex;
|
||||
}
|
||||
|
||||
@@ -49,7 +51,7 @@ function mixColors(ratio, rgb1, rgb2) {
|
||||
}
|
||||
|
||||
function parseColor(input) {
|
||||
var match = input.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
|
||||
const match = input.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
|
||||
|
||||
if (match) {
|
||||
return [match[1], match[2], match[3]];
|
||||
@@ -64,9 +66,9 @@ function deleteNetworkEntry() {
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Deleting network table entry...");
|
||||
$.ajax({
|
||||
url: apiUrl + "/network/devices/" + id,
|
||||
url: document.body.dataset.apiurl + "/network/devices/" + id,
|
||||
method: "DELETE",
|
||||
success: function () {
|
||||
success() {
|
||||
utils.enableAll();
|
||||
utils.showAlert(
|
||||
"success",
|
||||
@@ -76,7 +78,7 @@ function deleteNetworkEntry() {
|
||||
);
|
||||
tableApi.row(tr).remove().draw(false).ajax.reload(null, false);
|
||||
},
|
||||
error: function (data, exception) {
|
||||
error(data, exception) {
|
||||
apiFailure(data);
|
||||
utils.enableAll();
|
||||
utils.showAlert(
|
||||
@@ -90,24 +92,23 @@ function deleteNetworkEntry() {
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
tableApi = $("#network-entries").DataTable({
|
||||
rowCallback: function (row, data) {
|
||||
var color;
|
||||
var index;
|
||||
var iconClasses;
|
||||
var lastQuery = parseInt(data.lastQuery, 10);
|
||||
var diff = getTimestamp() - lastQuery;
|
||||
var networkRecent = $(".network-recent").css("background-color");
|
||||
var networkOld = $(".network-old").css("background-color");
|
||||
var networkOlder = $(".network-older").css("background-color");
|
||||
var networkNever = $(".network-never").css("background-color");
|
||||
rowCallback(row, data) {
|
||||
let color;
|
||||
let iconClasses;
|
||||
const lastQuery = Number.parseInt(data.lastQuery, 10);
|
||||
const diff = getTimestamp() - lastQuery;
|
||||
const networkRecent = $(".network-recent").css("background-color");
|
||||
const networkOld = $(".network-old").css("background-color");
|
||||
const networkOlder = $(".network-older").css("background-color");
|
||||
const networkNever = $(".network-never").css("background-color");
|
||||
|
||||
if (lastQuery > 0) {
|
||||
if (diff <= DAY_IN_SECONDS) {
|
||||
// Last query came in within the last 24 hours
|
||||
// Color: light-green to light-yellow
|
||||
var ratio = Number(diff) / DAY_IN_SECONDS;
|
||||
const ratio = Number(diff) / DAY_IN_SECONDS;
|
||||
color = rgbToHex(mixColors(ratio, parseColor(networkRecent), parseColor(networkOld)));
|
||||
iconClasses = "fas fa-check";
|
||||
} else {
|
||||
@@ -129,17 +130,17 @@ $(function () {
|
||||
// Insert "Never" into Last Query field when we have
|
||||
// never seen a query from this device
|
||||
if (data.lastQuery === 0) {
|
||||
$("td:eq(4)", row).html("Never");
|
||||
$("td:eq(4)", row).text("Never");
|
||||
}
|
||||
|
||||
// Set number of queries to localized string (add thousand separators)
|
||||
$("td:eq(5)", row).html(data.numQueries.toLocaleString());
|
||||
$("td:eq(5)", row).text(data.numQueries.toLocaleString());
|
||||
|
||||
var ips = [],
|
||||
iptitles = [];
|
||||
const ips = [];
|
||||
const iptitles = [];
|
||||
|
||||
// Sort IPs, IPv4 before IPv6, then alphabetically
|
||||
data.ips.sort(function (a, b) {
|
||||
data.ips.sort((a, b) => {
|
||||
if (a.ip.includes(":") && !b.ip.includes(":")) {
|
||||
return 1;
|
||||
}
|
||||
@@ -151,19 +152,18 @@ $(function () {
|
||||
return a.ip.localeCompare(b.ip);
|
||||
});
|
||||
|
||||
for (index = 0; index < data.ips.length; index++) {
|
||||
var ip = data.ips[index],
|
||||
iptext = ip.ip;
|
||||
for (const { ip, name } of data.ips) {
|
||||
let iptext = ip;
|
||||
|
||||
if (ip.name !== null && ip.name.length > 0) {
|
||||
iptext = iptext + " (" + ip.name + ")";
|
||||
if (name !== null && name.length > 0) {
|
||||
iptext = `${iptext} (${name})`;
|
||||
}
|
||||
|
||||
iptitles.push(iptext);
|
||||
|
||||
// Only add IPs to the table if we have not reached the maximum
|
||||
if (index < MAXIPDISPLAY) {
|
||||
ips.push('<a href="queries?client_ip=' + ip.ip + '">' + iptext + "</a>");
|
||||
if (ips.length < MAXIPDISPLAY) {
|
||||
ips.push(`<a href="queries?client_ip=${ip}">${iptext}</a>`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ $(function () {
|
||||
// Add delete button
|
||||
$(row).attr("data-id", data.id);
|
||||
$(row).attr("data-hwaddr", data.hwaddr);
|
||||
var button =
|
||||
const button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteNetworkEntry_' +
|
||||
data.id +
|
||||
'">' +
|
||||
@@ -208,7 +208,7 @@ $(function () {
|
||||
"<'row'<'col-sm-12'<'table-responsive'tr>>>" +
|
||||
"<'row'<'col-sm-5'i><'col-sm-7'p>>",
|
||||
ajax: {
|
||||
url: apiUrl + "/network/devices",
|
||||
url: document.body.dataset.apiurl + "/network/devices",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
data: {
|
||||
@@ -229,7 +229,7 @@ $(function () {
|
||||
{
|
||||
data: "firstSeen",
|
||||
width: "8%",
|
||||
render: function (data, type) {
|
||||
render(data, type) {
|
||||
if (type === "display") {
|
||||
return utils.datetime(data);
|
||||
}
|
||||
@@ -240,7 +240,7 @@ $(function () {
|
||||
{
|
||||
data: "lastQuery",
|
||||
width: "8%",
|
||||
render: function (data, type) {
|
||||
render(data, type) {
|
||||
if (type === "display") {
|
||||
return utils.datetime(data);
|
||||
}
|
||||
@@ -249,12 +249,12 @@ $(function () {
|
||||
},
|
||||
},
|
||||
{ data: "numQueries", width: "9%", render: $.fn.dataTable.render.text() },
|
||||
{ data: "", width: "6%", orderable: false },
|
||||
{ data: "", width: "6%", orderable: false },
|
||||
{ data: null, width: "6%", orderable: false },
|
||||
{ data: null, width: "6%", orderable: false },
|
||||
{ data: "ips[].name", visible: false, class: "hide" },
|
||||
],
|
||||
|
||||
drawCallback: function () {
|
||||
drawCallback() {
|
||||
$('button[id^="deleteNetworkEntry_"]').on("click", deleteNetworkEntry);
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
@@ -265,10 +265,10 @@ $(function () {
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
stateSaveCallback(settings, data) {
|
||||
utils.stateSaveCallback("network_table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
stateLoadCallback() {
|
||||
return utils.stateLoadCallback("network_table");
|
||||
},
|
||||
columnDefs: [
|
||||
@@ -284,7 +284,7 @@ $(function () {
|
||||
],
|
||||
});
|
||||
// Disable autocorrect in the search box
|
||||
var input = document.querySelector("input[type=search]");
|
||||
const input = document.querySelector("input[type=search]");
|
||||
input.setAttribute("autocomplete", "off");
|
||||
input.setAttribute("autocorrect", "off");
|
||||
input.setAttribute("autocapitalize", "off");
|
||||
|
||||
+94
-117
@@ -5,18 +5,20 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global moment:false, utils:false, apiUrl:false, REFRESH_INTERVAL:false */
|
||||
/* global moment:false, utils:false, REFRESH_INTERVAL:false */
|
||||
|
||||
const beginningOfTime = 1262304000; // Jan 01 2010, 00:00 in seconds
|
||||
const endOfTime = 2147483647; // Jan 19, 2038, 03:14 in seconds
|
||||
var from = beginningOfTime;
|
||||
var until = endOfTime;
|
||||
"use strict";
|
||||
|
||||
var dateformat = "MMM Do YYYY, HH:mm";
|
||||
const beginningOfTime = 1_262_304_000; // Jan 01 2010, 00:00 in seconds
|
||||
const endOfTime = 2_147_483_647; // Jan 19, 2038, 03:14 in seconds
|
||||
let from = beginningOfTime;
|
||||
let until = endOfTime;
|
||||
|
||||
var table = null;
|
||||
var cursor = null;
|
||||
var filters = [
|
||||
const dateformat = "MMM Do YYYY, HH:mm";
|
||||
|
||||
let table = null;
|
||||
let cursor = null;
|
||||
const filters = [
|
||||
"client_ip",
|
||||
"client_name",
|
||||
"domain",
|
||||
@@ -58,7 +60,7 @@ function initDateRangePicker() {
|
||||
showDropdowns: true,
|
||||
autoUpdateInput: true,
|
||||
},
|
||||
function (startt, endt) {
|
||||
(startt, endt) => {
|
||||
// Update global variables
|
||||
// Convert milliseconds (JS) to seconds (API)
|
||||
from = moment(startt).utc().valueOf() / 1000;
|
||||
@@ -81,12 +83,12 @@ function handleAjaxError(xhr, textStatus) {
|
||||
|
||||
function parseQueryStatus(data) {
|
||||
// Parse query status
|
||||
var fieldtext,
|
||||
buttontext,
|
||||
icon = null,
|
||||
colorClass = false,
|
||||
blocked = false,
|
||||
isCNAME = false;
|
||||
let fieldtext;
|
||||
let buttontext;
|
||||
let icon = null;
|
||||
let colorClass = false;
|
||||
let blocked = false;
|
||||
let isCNAME = false;
|
||||
switch (data.status) {
|
||||
case "GRAVITY":
|
||||
colorClass = "text-red";
|
||||
@@ -223,17 +225,17 @@ function parseQueryStatus(data) {
|
||||
buttontext = "";
|
||||
}
|
||||
|
||||
var matchText =
|
||||
const matchText =
|
||||
colorClass === "text-green" ? "allowed" : colorClass === "text-red" ? "blocked" : "matched";
|
||||
|
||||
return {
|
||||
fieldtext: fieldtext,
|
||||
buttontext: buttontext,
|
||||
colorClass: colorClass,
|
||||
icon: icon,
|
||||
isCNAME: isCNAME,
|
||||
matchText: matchText,
|
||||
blocked: blocked,
|
||||
fieldtext,
|
||||
buttontext,
|
||||
colorClass,
|
||||
icon,
|
||||
isCNAME,
|
||||
matchText,
|
||||
blocked,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -256,9 +258,9 @@ function formatReplyTime(replyTime, type) {
|
||||
|
||||
// Parse DNSSEC status
|
||||
function parseDNSSEC(data) {
|
||||
var icon = "", // Icon to display
|
||||
color = "", // Class to apply to text
|
||||
text = data.dnssec; // Text to display
|
||||
let icon = ""; // Icon to display
|
||||
let color = ""; // Class to apply to text
|
||||
let text = data.dnssec; // Text to display
|
||||
switch (text) {
|
||||
case "SECURE":
|
||||
icon = "fa-solid fa-lock";
|
||||
@@ -283,15 +285,15 @@ function parseDNSSEC(data) {
|
||||
icon = "";
|
||||
}
|
||||
|
||||
return { text: text, icon: icon, color: color };
|
||||
return { text, icon, color };
|
||||
}
|
||||
|
||||
function formatInfo(data) {
|
||||
// Parse Query Status
|
||||
var dnssec = parseDNSSEC(data);
|
||||
var queryStatus = parseQueryStatus(data);
|
||||
var divStart = '<div class="col-xl-2 col-lg-4 col-md-6 col-12 overflow-wrap">';
|
||||
var statusInfo = "";
|
||||
const dnssec = parseDNSSEC(data);
|
||||
const queryStatus = parseQueryStatus(data);
|
||||
const divStart = '<div class="col-xl-2 col-lg-4 col-md-6 col-12 overflow-wrap">';
|
||||
let statusInfo = "";
|
||||
if (queryStatus.colorClass !== false) {
|
||||
statusInfo =
|
||||
divStart +
|
||||
@@ -303,25 +305,24 @@ function formatInfo(data) {
|
||||
"</span></strong></div>";
|
||||
}
|
||||
|
||||
var listInfo = "",
|
||||
cnameInfo = "";
|
||||
let listInfo = "";
|
||||
if (data.list_id !== null && data.list_id !== -1) {
|
||||
// Some list matched - add link to search page
|
||||
|
||||
var listLink =
|
||||
'<a href="search?domain=' +
|
||||
encodeURIComponent(data.domain) +
|
||||
'" target="_blank">search lists</a>';
|
||||
listInfo = divStart + "Query was " + queryStatus.matchText + ", " + listLink + "</div>";
|
||||
const searchLink =
|
||||
data.domain !== "hidden"
|
||||
? `, <a href="search?domain=${encodeURIComponent(queryStatus.isCNAME ? data.cname : data.domain)}" target="_blank">search lists</a>`
|
||||
: "";
|
||||
listInfo = `${divStart}Query was ${queryStatus.matchText}${searchLink}</div>`;
|
||||
}
|
||||
|
||||
let cnameInfo = "";
|
||||
if (queryStatus.isCNAME) {
|
||||
cnameInfo =
|
||||
divStart + "Query was blocked during CNAME inspection of " + data.cname + "</div>";
|
||||
}
|
||||
|
||||
// Show TTL if applicable
|
||||
var ttlInfo = "";
|
||||
let ttlInfo = "";
|
||||
if (data.ttl > 0) {
|
||||
ttlInfo =
|
||||
divStart +
|
||||
@@ -333,14 +334,14 @@ function formatInfo(data) {
|
||||
}
|
||||
|
||||
// Show client information, show hostname only if available
|
||||
var ipInfo =
|
||||
const ipInfo =
|
||||
data.client.name !== null && data.client.name.length > 0
|
||||
? utils.escapeHtml(data.client.name) + " (" + data.client.ip + ")"
|
||||
: data.client.ip;
|
||||
var clientInfo = divStart + "Client: <strong>" + ipInfo + "</strong></div>";
|
||||
const clientInfo = divStart + "Client: <strong>" + ipInfo + "</strong></div>";
|
||||
|
||||
// Show DNSSEC status if applicable
|
||||
var dnssecInfo = "";
|
||||
let dnssecInfo = "";
|
||||
if (dnssec.color !== "") {
|
||||
dnssecInfo =
|
||||
divStart +
|
||||
@@ -352,20 +353,20 @@ function formatInfo(data) {
|
||||
}
|
||||
|
||||
// Show long-term database information if applicable
|
||||
var dbInfo = "";
|
||||
let dbInfo = "";
|
||||
if (data.dbid !== false) {
|
||||
dbInfo = divStart + "Database ID: " + data.id + "</div>";
|
||||
}
|
||||
|
||||
// Always show reply info, add reply delay if applicable
|
||||
var replyInfo = "";
|
||||
let replyInfo = "";
|
||||
replyInfo =
|
||||
data.reply.type !== "UNKNOWN"
|
||||
? divStart + "Reply:  " + data.reply.type + "</div>"
|
||||
: divStart + "Reply: No reply received</div>";
|
||||
|
||||
// Show extended DNS error if applicable
|
||||
var edeInfo = "";
|
||||
let edeInfo = "";
|
||||
if (data.ede !== null && data.ede.text !== null) {
|
||||
edeInfo = divStart + "Extended DNS error: <strong";
|
||||
if (dnssec.color !== "") {
|
||||
@@ -396,8 +397,8 @@ function formatInfo(data) {
|
||||
}
|
||||
|
||||
function addSelectSuggestion(name, dict, data) {
|
||||
var obj = $("#" + name + "_filter"),
|
||||
value = "";
|
||||
const obj = $("#" + name + "_filter");
|
||||
let value = "";
|
||||
obj.empty();
|
||||
|
||||
// In order for the placeholder value to appear, we have to have a blank
|
||||
@@ -414,12 +415,7 @@ function addSelectSuggestion(name, dict, data) {
|
||||
}
|
||||
|
||||
// Add data obtained from API
|
||||
for (var key in data) {
|
||||
if (!Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = data[key];
|
||||
for (const text of Object.values(data)) {
|
||||
obj.append($("<option />").val(text).text(text));
|
||||
}
|
||||
|
||||
@@ -431,13 +427,10 @@ function addSelectSuggestion(name, dict, data) {
|
||||
|
||||
function getSuggestions(dict) {
|
||||
$.get(
|
||||
apiUrl + "/queries/suggestions",
|
||||
function (data) {
|
||||
for (var key in filters) {
|
||||
if (Object.hasOwnProperty.call(filters, key)) {
|
||||
var f = filters[key];
|
||||
addSelectSuggestion(f, dict, data.suggestions[f]);
|
||||
}
|
||||
document.body.dataset.apiurl + "/queries/suggestions",
|
||||
data => {
|
||||
for (const filter of Object.values(filters)) {
|
||||
addSelectSuggestion(filter, dict, data.suggestions[filter]);
|
||||
}
|
||||
},
|
||||
"json"
|
||||
@@ -445,15 +438,7 @@ function getSuggestions(dict) {
|
||||
}
|
||||
|
||||
function parseFilters() {
|
||||
var filter = {};
|
||||
for (var key in filters) {
|
||||
if (Object.hasOwnProperty.call(filters, key)) {
|
||||
var f = filters[key];
|
||||
filter[f] = $("#" + f + "_filter").val();
|
||||
}
|
||||
}
|
||||
|
||||
return filter;
|
||||
return Object.fromEntries(filters.map(filter => [filter, $(`#${filter}_filter`).val()]));
|
||||
}
|
||||
|
||||
function filterOn(param, dict) {
|
||||
@@ -462,14 +447,11 @@ function filterOn(param, dict) {
|
||||
}
|
||||
|
||||
function getAPIURL(filters) {
|
||||
var apiurl = apiUrl + "/queries?";
|
||||
for (var key in filters) {
|
||||
if (Object.hasOwnProperty.call(filters, key)) {
|
||||
var filter = filters[key];
|
||||
if (filterOn(key, filters)) {
|
||||
if (!apiurl.endsWith("?")) apiurl += "&";
|
||||
apiurl += key + "=" + encodeURIComponent(filter);
|
||||
}
|
||||
let apiurl = document.body.dataset.apiurl + "/queries?";
|
||||
for (const [key, filter] of Object.entries(filters)) {
|
||||
if (filterOn(key, filters)) {
|
||||
if (!apiurl.endsWith("?")) apiurl += "&";
|
||||
apiurl += `${key}=${encodeURIComponent(filter)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +466,7 @@ function getAPIURL(filters) {
|
||||
return encodeURI(apiurl);
|
||||
}
|
||||
|
||||
var liveMode = false;
|
||||
let liveMode = false;
|
||||
$("#live").prop("checked", liveMode);
|
||||
$("#live").on("click", function () {
|
||||
liveMode = $(this).prop("checked");
|
||||
@@ -497,47 +479,42 @@ function liveUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
// Do we want to filter queries?
|
||||
var GETDict = utils.parseQueryString();
|
||||
const GETDict = utils.parseQueryString();
|
||||
|
||||
for (var sel in filters) {
|
||||
if (Object.hasOwnProperty.call(filters, sel)) {
|
||||
var element = filters[sel];
|
||||
$("#" + element + "_filter").select2({
|
||||
width: "100%",
|
||||
tags: sel < 3, // Only the first three are allowed to freely specify input
|
||||
placeholder: "Select...",
|
||||
allowClear: true,
|
||||
});
|
||||
}
|
||||
for (const [sel, element] of Object.entries(filters)) {
|
||||
$(`#${element}_filter`).select2({
|
||||
width: "100%",
|
||||
tags: sel < 4, // Only the first four (client(IP/name), domain, upstream) are allowed to freely specify input
|
||||
placeholder: "Select...",
|
||||
allowClear: true,
|
||||
});
|
||||
}
|
||||
|
||||
getSuggestions(GETDict);
|
||||
var apiurl = getAPIURL(GETDict);
|
||||
const apiURL = getAPIURL(GETDict);
|
||||
|
||||
if ("from" in GETDict) {
|
||||
from = GETDict.from;
|
||||
$("#from").val(moment.unix(from).format("Y-MM-DD HH:mm:ss"));
|
||||
}
|
||||
|
||||
if ("until" in GETDict) {
|
||||
until = GETDict.until;
|
||||
$("#until").val(moment.unix(until).format("Y-MM-DD HH:mm:ss"));
|
||||
}
|
||||
|
||||
initDateRangePicker();
|
||||
|
||||
table = $("#all-queries").DataTable({
|
||||
ajax: {
|
||||
url: apiurl,
|
||||
url: apiURL,
|
||||
error: handleAjaxError,
|
||||
dataSrc: "queries",
|
||||
data: function (d) {
|
||||
data(d) {
|
||||
if (cursor !== null) d.cursor = cursor;
|
||||
},
|
||||
dataFilter: function (d) {
|
||||
var json = jQuery.parseJSON(d);
|
||||
dataFilter(d) {
|
||||
const json = JSON.parse(d);
|
||||
cursor = json.cursor; // Extract cursor from original data
|
||||
if (liveMode) {
|
||||
utils.setTimer(liveUpdate, REFRESH_INTERVAL.query_log);
|
||||
@@ -558,7 +535,7 @@ $(function () {
|
||||
{
|
||||
data: "time",
|
||||
width: "10%",
|
||||
render: function (data, type) {
|
||||
render(data, type) {
|
||||
if (type === "display") {
|
||||
return moment.unix(data).format("Y-MM-DD [<br class='hidden-lg'>]HH:mm:ss z");
|
||||
}
|
||||
@@ -580,14 +557,14 @@ $(function () {
|
||||
],
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
stateSaveCallback(settings, data) {
|
||||
utils.stateSaveCallback("query_log_table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
stateLoadCallback() {
|
||||
return utils.stateLoadCallback("query_log_table");
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
var querystatus = parseQueryStatus(data);
|
||||
rowCallback(row, data) {
|
||||
const querystatus = parseQueryStatus(data);
|
||||
const dnssec = parseDNSSEC(data);
|
||||
|
||||
if (querystatus.icon !== false) {
|
||||
@@ -608,10 +585,10 @@ $(function () {
|
||||
$(row).addClass(querystatus.blocked === true ? "blocked-row" : "allowed-row");
|
||||
|
||||
// Substitute domain by "." if empty
|
||||
var domain = data.domain === 0 ? "." : data.domain;
|
||||
let domain = data.domain === 0 ? "." : data.domain;
|
||||
|
||||
// Prefix colored DNSSEC icon to domain text
|
||||
var dnssecIcon = "";
|
||||
let dnssecIcon = "";
|
||||
dnssecIcon =
|
||||
'<i class="mr-2 fa fa-fw ' +
|
||||
dnssec.icon +
|
||||
@@ -648,7 +625,7 @@ $(function () {
|
||||
$("td:eq(6)", row).html(querystatus.buttontext);
|
||||
}
|
||||
},
|
||||
initComplete: function () {
|
||||
initComplete() {
|
||||
this.api()
|
||||
.columns()
|
||||
.every(function () {
|
||||
@@ -678,11 +655,11 @@ $(function () {
|
||||
|
||||
// Add event listener for adding domains to the allow-/blocklist
|
||||
$("#all-queries tbody").on("click", "button", function (event) {
|
||||
var button = $(this);
|
||||
var tr = button.parents("tr");
|
||||
var allowButton = button[0].classList.contains("text-green");
|
||||
var denyButton = button[0].classList.contains("text-red");
|
||||
var data = table.row(tr).data();
|
||||
const button = $(this);
|
||||
const tr = button.parents("tr");
|
||||
const allowButton = button[0].classList.contains("text-green");
|
||||
const denyButton = button[0].classList.contains("text-red");
|
||||
const data = table.row(tr).data();
|
||||
if (denyButton) {
|
||||
utils.addFromQueryLog(data.domain, "deny");
|
||||
} else if (allowButton) {
|
||||
@@ -696,8 +673,8 @@ $(function () {
|
||||
|
||||
// Add event listener for opening and closing details, except on rows with "details-row" class
|
||||
$("#all-queries tbody").on("click", "tr:not(.details-row)", function () {
|
||||
var tr = $(this);
|
||||
var row = table.row(tr);
|
||||
const tr = $(this);
|
||||
const row = table.row(tr);
|
||||
|
||||
if (globalThis.getSelection().toString().length > 0) {
|
||||
// This event was triggered by a selection, so don't open the row
|
||||
@@ -737,9 +714,9 @@ function refreshTable() {
|
||||
table.clear();
|
||||
|
||||
// Source data from API
|
||||
var filters = parseFilters();
|
||||
const filters = parseFilters();
|
||||
filters.from = from;
|
||||
filters.until = until;
|
||||
var apiurl = getAPIURL(filters);
|
||||
table.ajax.url(apiurl).draw();
|
||||
const apiUrl = getAPIURL(filters);
|
||||
table.ajax.url(apiUrl).draw();
|
||||
}
|
||||
|
||||
+16
-13
@@ -5,10 +5,13 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, apiFailure:false */
|
||||
var GETDict = {};
|
||||
/* global utils:false, apiFailure:false */
|
||||
|
||||
$(function () {
|
||||
"use strict";
|
||||
|
||||
let GETDict = {};
|
||||
|
||||
$(() => {
|
||||
GETDict = utils.parseQueryString();
|
||||
if (GETDict.domain !== undefined) {
|
||||
$("input[id^='domain']").val(GETDict.domain);
|
||||
@@ -31,23 +34,23 @@ function doSearch() {
|
||||
return;
|
||||
}
|
||||
|
||||
var verb = partial ? "partially" : "exactly";
|
||||
const verb = partial ? "partially" : "exactly";
|
||||
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: apiUrl + "/search/" + encodeURIComponent(q),
|
||||
url: document.body.dataset.apiurl + "/search/" + encodeURIComponent(q),
|
||||
async: false,
|
||||
data: {
|
||||
partial: partial,
|
||||
N: N,
|
||||
partial,
|
||||
N,
|
||||
},
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
ta.empty();
|
||||
ta.show();
|
||||
|
||||
const res = data.search;
|
||||
var result = "";
|
||||
let result = "";
|
||||
const numDomains = res.domains.length;
|
||||
result =
|
||||
"Found " +
|
||||
@@ -91,7 +94,7 @@ function doSearch() {
|
||||
}
|
||||
|
||||
// Group results in res.gravity by res.gravity[].address
|
||||
var grouped = {};
|
||||
const grouped = {};
|
||||
for (const list of res.gravity) {
|
||||
if (grouped[list.address + "_" + list.type] === undefined) {
|
||||
grouped[list.address + "_" + list.type] = [];
|
||||
@@ -190,13 +193,13 @@ function doSearch() {
|
||||
|
||||
ta.append(result);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle enter key
|
||||
$("#domain").on("keypress", function (e) {
|
||||
$("#domain").on("keypress", e => {
|
||||
if (e.which === 13) {
|
||||
// Enter was pressed, and the input has focus
|
||||
doSearch();
|
||||
@@ -204,6 +207,6 @@ $("#domain").on("keypress", function (e) {
|
||||
});
|
||||
|
||||
// Handle search buttons
|
||||
$("button[id='btnSearch']").on("click", function () {
|
||||
$("button[id='btnSearch']").on("click", () => {
|
||||
doSearch();
|
||||
});
|
||||
|
||||
@@ -5,22 +5,23 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, apiFailure: false, applyCheckboxRadioStyle: false, saveSettings:false */
|
||||
/* exported createDynamicConfigTabs */
|
||||
/* global utils:false, apiFailure: false, applyCheckboxRadioStyle: false */
|
||||
|
||||
"use strict";
|
||||
|
||||
function addAllowedValues(allowed) {
|
||||
if (typeof allowed === "object") {
|
||||
return (
|
||||
"<p>Available options:</p><ul><li>" +
|
||||
allowed
|
||||
.map(function (option) {
|
||||
return "<code>" + option.item + "</code>: " + utils.escapeHtml(option.description);
|
||||
})
|
||||
.map(option => "<code>" + option.item + "</code>: " + utils.escapeHtml(option.description))
|
||||
.join("</li><li>") +
|
||||
"</li></ul>"
|
||||
);
|
||||
} else if (typeof allowed === "string") {
|
||||
return "<p><small>Allowed value: " + utils.escapeHtml(allowed) + "</small></p>";
|
||||
}
|
||||
|
||||
if (typeof allowed === "string") {
|
||||
return `<p class="small">Allowed value: ${utils.escapeHtml(allowed)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +213,7 @@ function valueDetails(key, value) {
|
||||
case "enum (unsigned integer)": // fallthrough
|
||||
case "enum (string)": {
|
||||
content += '<div class="col-sm-12">';
|
||||
value.allowed.forEach(function (option, i) {
|
||||
for (const [i, option] of value.allowed.entries()) {
|
||||
content +=
|
||||
"<div>" +
|
||||
// Radio button
|
||||
@@ -227,7 +228,8 @@ function valueDetails(key, value) {
|
||||
// Paragraph with description
|
||||
`<p class="help-block">${option.description}</p>` +
|
||||
"</div>";
|
||||
});
|
||||
}
|
||||
|
||||
content += "</div>";
|
||||
|
||||
break;
|
||||
@@ -262,15 +264,15 @@ function valueDetails(key, value) {
|
||||
function generateRow(topic, key, value) {
|
||||
// If the value is an object, we need to recurse
|
||||
if (!("description" in value)) {
|
||||
Object.keys(value).forEach(function (subkey) {
|
||||
var subvalue = value[subkey];
|
||||
for (const [subkey, subvalue] of Object.entries(value)) {
|
||||
generateRow(topic, key + "." + subkey, subvalue);
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// else: we have a setting we can display
|
||||
var box =
|
||||
const box =
|
||||
'<div class="box settings-box">' +
|
||||
'<div class="box-header with-border">' +
|
||||
'<h3 class="box-title" data-key="' +
|
||||
@@ -289,20 +291,18 @@ function generateRow(topic, key, value) {
|
||||
valueDetails(key, value) +
|
||||
"</div></div> ";
|
||||
|
||||
var topKey = key.split(".")[0];
|
||||
var elem = $("#advanced-content-" + topKey + "-flex");
|
||||
const topKey = key.split(".")[0];
|
||||
const elem = $("#advanced-content-" + topKey + "-flex");
|
||||
elem.append(box);
|
||||
}
|
||||
|
||||
function createDynamicConfigTabs() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/config?detailed=true",
|
||||
url: document.body.dataset.apiurl + "/config?detailed=true",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
// Create the tabs for the advanced dynamic config topics
|
||||
Object.keys(data.topics).forEach(function (n) {
|
||||
var topic = data.topics[n];
|
||||
|
||||
for (const topic of Object.values(data.topics)) {
|
||||
$("#advanced-settings-tabs").append(`
|
||||
<div id="advanced-content-${topic.name}" role="tabpanel" class="tab-pane fade">
|
||||
<h3 class="page-header">${topic.description} (<code>${topic.name}</code>)</h3>
|
||||
@@ -315,30 +315,26 @@ function createDynamicConfigTabs() {
|
||||
// Dynamically create the settings menu
|
||||
$("#advanced-settings-menu ul").append(`
|
||||
<li role="presentation">
|
||||
<a href="#advanced-content-${topic.name}" class="btn btn-default" aria-controls="advanced-content-${topic.name}" role="pill" data-toggle="pill">${topic.description.replace(" settings", "")}</a>
|
||||
<a href="#advanced-content-${topic.name}" class="btn btn-default" aria-controls="advanced-content-${topic.name}" role="tab" data-toggle="pill">${topic.description.replace(" settings", "")}</a>
|
||||
</li>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// Dynamically fill the tabs with config topics
|
||||
Object.keys(data.config).forEach(function (topic) {
|
||||
var value = data.config[topic];
|
||||
for (const [topic, value] of Object.entries(data.config)) {
|
||||
generateRow(topic, topic, value);
|
||||
});
|
||||
}
|
||||
|
||||
$("#advanced-overlay").hide();
|
||||
|
||||
// Select the first tab and show the content
|
||||
$("#advanced-settings-menu ul li:first-child").addClass("active");
|
||||
$("#advanced-settings-tabs > div:first-child").addClass("active in");
|
||||
|
||||
$("button[id='save']").on("click", function () {
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
applyCheckboxRadioStyle();
|
||||
applyOnlyChanged();
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
@@ -401,7 +397,7 @@ function applyOnlyChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$(() => {
|
||||
createDynamicConfigTabs();
|
||||
initOnlyChanged();
|
||||
});
|
||||
|
||||
+66
-68
@@ -5,18 +5,20 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, setConfigValues: false, apiFailure: false, QRious: false */
|
||||
/* global utils:false, setConfigValues: false, apiFailure: false, QRious: false */
|
||||
|
||||
var apiSessionsTable = null;
|
||||
var ownSessionID = null;
|
||||
var deleted = 0;
|
||||
var TOTPdata = null;
|
||||
var apppwSet = false;
|
||||
"use strict";
|
||||
|
||||
let apiSessionsTable = null;
|
||||
let ownSessionID = null;
|
||||
let deleted = 0;
|
||||
let TOTPdata = null;
|
||||
let apppwSet = false;
|
||||
|
||||
function renderBool(data, type) {
|
||||
// Display and search content
|
||||
if (type === "display" || type === "filter") {
|
||||
var icon = "fa-xmark text-danger";
|
||||
let icon = "fa-xmark text-danger";
|
||||
if (data === true) {
|
||||
icon = "fa-check text-success";
|
||||
}
|
||||
@@ -28,10 +30,10 @@ function renderBool(data, type) {
|
||||
return data;
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
apiSessionsTable = $("#APISessionsTable").DataTable({
|
||||
ajax: {
|
||||
url: apiUrl + "/auth/sessions",
|
||||
url: document.body.dataset.apiurl + "/auth/sessions",
|
||||
type: "GET",
|
||||
dataSrc: "sessions",
|
||||
},
|
||||
@@ -54,7 +56,7 @@ $(function () {
|
||||
targets: 0,
|
||||
orderable: false,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
render() {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
@@ -63,19 +65,19 @@ $(function () {
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
drawCallback() {
|
||||
$('button[id^="deleteSession_"]').on("click", deleteThisSession);
|
||||
|
||||
// Hide buttons if all messages were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
const hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
rowCallback(row, data) {
|
||||
$(row).attr("data-id", data.id);
|
||||
var button =
|
||||
const button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteSession_' +
|
||||
data.id +
|
||||
'" data-del-id="' +
|
||||
@@ -113,7 +115,7 @@ $(function () {
|
||||
// Show x-forwarded-for instead of the remote address in italics
|
||||
// and show the remote address in the title attribute
|
||||
if (data.x_forwarded_for !== null) {
|
||||
$("td:eq(8)", row).html("<i>" + data.x_forwarded_for + "</i>");
|
||||
$("td:eq(8)", row).html("<em>" + data.x_forwarded_for + "</em>");
|
||||
$("td:eq(8)", row).attr("title", "Original remote address: " + data.remote_addr);
|
||||
}
|
||||
},
|
||||
@@ -127,7 +129,7 @@ $(function () {
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
action() {
|
||||
apiSessionsTable.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -135,7 +137,7 @@ $(function () {
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
action() {
|
||||
apiSessionsTable.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -149,12 +151,12 @@ $(function () {
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
action() {
|
||||
// For each ".selected" row ...
|
||||
var ids = [];
|
||||
const ids = [];
|
||||
$("tr.selected").each(function () {
|
||||
// ... add the row identified by "data-id".
|
||||
ids.push(parseInt($(this).attr("data-id"), 10));
|
||||
ids.push(Number.parseInt($(this).attr("data-id"), 10));
|
||||
});
|
||||
// Delete all selected rows at once
|
||||
deleteMultipleSessions(ids);
|
||||
@@ -176,11 +178,11 @@ $(function () {
|
||||
},
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
stateSaveCallback(settings, data) {
|
||||
utils.stateSaveCallback("api-sessions-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("api-sessions-table");
|
||||
stateLoadCallback() {
|
||||
const data = utils.stateLoadCallback("api-sessions-table");
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
@@ -190,7 +192,7 @@ $(function () {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
apiSessionsTable.on("init select deselect", function () {
|
||||
apiSessionsTable.on("init select deselect", () => {
|
||||
utils.changeTableButtonStates(apiSessionsTable);
|
||||
});
|
||||
});
|
||||
@@ -198,7 +200,7 @@ $(function () {
|
||||
function deleteThisSession() {
|
||||
// This function is called when a red trash button is clicked
|
||||
// We get the ID of the current item from the data-del-id attribute
|
||||
const thisID = parseInt($(this).attr("data-del-id"), 10);
|
||||
const thisID = Number.parseInt($(this).attr("data-del-id"), 10);
|
||||
deleted = 0;
|
||||
deleteOneSession(thisID, 1, false);
|
||||
}
|
||||
@@ -210,15 +212,13 @@ function deleteMultipleSessions(ids) {
|
||||
// Check input validity
|
||||
if (!Array.isArray(ids)) return;
|
||||
|
||||
// Exploit prevention: Return early for non-numeric IDs
|
||||
// Exploit prevention: return early for non-numeric IDs
|
||||
for (const id of ids) {
|
||||
if (Object.hasOwnProperty.call(ids, id) && isNaN(ids[id])) return;
|
||||
if (!Number.isInteger(id)) return;
|
||||
}
|
||||
|
||||
// Convert all ids to integers
|
||||
ids = ids.map(function (value) {
|
||||
return parseInt(value, 10);
|
||||
});
|
||||
ids = ids.map(value => Number.parseInt(value, 10));
|
||||
|
||||
// Check if own session is selected and remove it when deleting multiple
|
||||
// We need this only when multiple sessions are removed to ensure we do not
|
||||
@@ -228,9 +228,7 @@ function deleteMultipleSessions(ids) {
|
||||
if (ids.includes(ownSessionID) && ids.length > 1) {
|
||||
ownSessionDelete = true;
|
||||
// Strip own session ID from array
|
||||
ids = ids.filter(function (value) {
|
||||
return value !== ownSessionID;
|
||||
});
|
||||
ids = ids.filter(value => value !== ownSessionID);
|
||||
}
|
||||
|
||||
// Loop through IDs and delete them
|
||||
@@ -247,10 +245,10 @@ function deleteOneSession(id, len, ownSessionDelete) {
|
||||
// our own session is then triggered by the last successful deletion of
|
||||
// another session (ownSessionDelete == true, len == global deleted)
|
||||
$.ajax({
|
||||
url: apiUrl + "/auth/session/" + id,
|
||||
url: document.body.dataset.apiurl + "/auth/session/" + id,
|
||||
method: "DELETE",
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
// Do not reload page when deleting multiple sessions
|
||||
if (++deleted < len) return;
|
||||
|
||||
@@ -265,16 +263,16 @@ function deleteOneSession(id, len, ownSessionDelete) {
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
function processWebServerConfig() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/config/webserver?detailed=true",
|
||||
url: document.body.dataset.apiurl + "/config/webserver?detailed=true",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
setConfigValues("webserver", "webserver", data.config.webserver);
|
||||
if (data.config.webserver.api.app_pwhash.value.length > 0) {
|
||||
apppwSet = true;
|
||||
@@ -284,19 +282,19 @@ function processWebServerConfig() {
|
||||
$("#apppw_submit").addClass("btn-warning");
|
||||
} else $("#apppw_clear").hide();
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
$("#modal-totp").on("shown.bs.modal", function () {
|
||||
$("#modal-totp").on("shown.bs.modal", () => {
|
||||
$.ajax({
|
||||
url: apiUrl + "/auth/totp",
|
||||
url: document.body.dataset.apiurl + "/auth/totp",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
TOTPdata = data.totp;
|
||||
$("#totp_secret").text(data.totp.secret);
|
||||
var qrlink =
|
||||
const qrlink =
|
||||
"otpauth://totp/" +
|
||||
data.totp.issuer +
|
||||
":" +
|
||||
@@ -321,28 +319,28 @@ $("#modal-totp").on("shown.bs.modal", function () {
|
||||
$("#qrcode-spinner").hide();
|
||||
$("#qrcode").show();
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
});
|
||||
|
||||
var apppwhash = null;
|
||||
$("#modal-apppw").on("shown.bs.modal", function () {
|
||||
let apppwhash = null;
|
||||
$("#modal-apppw").on("shown.bs.modal", () => {
|
||||
$.ajax({
|
||||
url: apiUrl + "/auth/app",
|
||||
url: document.body.dataset.apiurl + "/auth/app",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
apppwhash = data.app.hash;
|
||||
$("#password_code").text(data.app.password);
|
||||
$("#password_display").removeClass("hidden");
|
||||
$("#password-spinner").hide();
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
});
|
||||
|
||||
$("#apppw_submit").on("click", function () {
|
||||
$("#apppw_submit").on("click", () => {
|
||||
// Enable app password
|
||||
if (!apppwSet) {
|
||||
return setAppPassword();
|
||||
@@ -353,7 +351,7 @@ $("#apppw_submit").on("click", function () {
|
||||
text: "Are you sure you want to replace your previous app password? You will need to re-login to continue using the web interface.",
|
||||
title: "Confirmation required",
|
||||
confirm: setAppPassword,
|
||||
cancel: function () {
|
||||
cancel() {
|
||||
// nothing to do
|
||||
},
|
||||
confirmButton: "Yes, replace password",
|
||||
@@ -365,7 +363,7 @@ $("#apppw_submit").on("click", function () {
|
||||
});
|
||||
});
|
||||
|
||||
$("#apppw_clear").on("click", function () {
|
||||
$("#apppw_clear").on("click", () => {
|
||||
// Disable app password
|
||||
apppwhash = "";
|
||||
setAppPassword();
|
||||
@@ -373,14 +371,14 @@ $("#apppw_clear").on("click", function () {
|
||||
|
||||
function setAppPassword() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/config",
|
||||
url: document.body.dataset.apiurl + "/config",
|
||||
type: "PATCH",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
data: JSON.stringify({ config: { webserver: { api: { app_pwhash: apppwhash } } } }),
|
||||
contentType: "application/json; charset=utf-8",
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
$("#modal-apppw").modal("hide");
|
||||
const verb = apppwhash.length > 0 ? "enabled" : "disabled";
|
||||
const verb2 = apppwhash.length > 0 ? "will" : "may";
|
||||
@@ -393,7 +391,7 @@ function setAppPassword() {
|
||||
);
|
||||
location.reload();
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
@@ -408,12 +406,12 @@ $("#password_code").on("mouseout blur", function () {
|
||||
});
|
||||
|
||||
// Trigger keyup event when pasting into the TOTP code input field
|
||||
$("#totp_code").on("paste", function (e) {
|
||||
$(e.target).keyup();
|
||||
$("#totp_code").on("paste", event => {
|
||||
$(event.target).trigger("keyup");
|
||||
});
|
||||
|
||||
$("#totp_code").on("keyup", function () {
|
||||
var code = parseInt($(this).val(), 10);
|
||||
const code = Number.parseInt($(this).val(), 10);
|
||||
if (TOTPdata.codes.includes(code)) {
|
||||
$("#totp_div").removeClass("has-error");
|
||||
$("#totp_div").addClass("has-success");
|
||||
@@ -426,19 +424,19 @@ $("#totp_code").on("keyup", function () {
|
||||
|
||||
function setTOTPSecret(secret) {
|
||||
$.ajax({
|
||||
url: apiUrl + "/config",
|
||||
url: document.body.dataset.apiurl + "/config",
|
||||
type: "PATCH",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
data: JSON.stringify({ config: { webserver: { api: { totp_secret: secret } } } }),
|
||||
contentType: "application/json; charset=utf-8",
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
$("#button-enable-totp").addClass("hidden");
|
||||
$("#button-disable-totp").removeClass("hidden");
|
||||
$("#totp_code").val("");
|
||||
$("#modal-totp").modal("hide");
|
||||
var verb = secret.length > 0 ? "enabled" : "disabled";
|
||||
const verb = secret.length > 0 ? "enabled" : "disabled";
|
||||
alert(
|
||||
"Two-factor authentication has been " +
|
||||
verb +
|
||||
@@ -446,12 +444,12 @@ function setTOTPSecret(secret) {
|
||||
);
|
||||
location.reload();
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
$("#totp_submit").on("click", function () {
|
||||
$("#totp_submit").on("click", () => {
|
||||
// Enable TOTP
|
||||
setTOTPSecret(TOTPdata.secret);
|
||||
});
|
||||
@@ -459,11 +457,11 @@ $("#totp_submit").on("click", function () {
|
||||
$("#button-disable-totp").confirm({
|
||||
text: "Are you sure you want to disable 2FA authentication on your Pi-hole?",
|
||||
title: "Confirmation required",
|
||||
confirm: function () {
|
||||
confirm() {
|
||||
// Disable TOTP
|
||||
setTOTPSecret("");
|
||||
},
|
||||
cancel: function () {
|
||||
cancel() {
|
||||
// nothing to do
|
||||
},
|
||||
confirmButton: "Yes, disable 2FA",
|
||||
@@ -474,12 +472,12 @@ $("#button-disable-totp").confirm({
|
||||
dialogClass: "modal-dialog",
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
$(() => {
|
||||
processWebServerConfig();
|
||||
// Check if TOTP is enabled
|
||||
$.ajax({
|
||||
url: apiUrl + "/auth",
|
||||
}).done(function (data) {
|
||||
url: document.body.dataset.apiurl + "/auth",
|
||||
}).done(data => {
|
||||
if (data.session.totp === false) $("#button-enable-totp").removeClass("hidden");
|
||||
else $("#button-disable-totp").removeClass("hidden");
|
||||
});
|
||||
|
||||
+28
-26
@@ -5,13 +5,15 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, setConfigValues: false, apiFailure: false */
|
||||
/* global utils:false, setConfigValues: false, apiFailure: false */
|
||||
|
||||
var dhcpLeaesTable = null,
|
||||
toasts = {};
|
||||
"use strict";
|
||||
|
||||
let dhcpLeaesTable = null;
|
||||
const toasts = {};
|
||||
|
||||
// DHCP leases tooltips
|
||||
$(function () {
|
||||
$(() => {
|
||||
$('[data-toggle="tooltip"]').tooltip({ html: true, container: "body" });
|
||||
});
|
||||
|
||||
@@ -19,7 +21,7 @@ function renderHostnameCLID(data, type) {
|
||||
// Display and search content
|
||||
if (type === "display" || type === "filter") {
|
||||
if (data === "*") {
|
||||
return "<i>---</i>";
|
||||
return "<em>---</em>";
|
||||
}
|
||||
|
||||
return data;
|
||||
@@ -29,10 +31,10 @@ function renderHostnameCLID(data, type) {
|
||||
return data;
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
dhcpLeaesTable = $("#DHCPLeasesTable").DataTable({
|
||||
ajax: {
|
||||
url: apiUrl + "/dhcp/leases",
|
||||
url: document.body.dataset.apiurl + "/dhcp/leases",
|
||||
type: "GET",
|
||||
dataSrc: "leases",
|
||||
},
|
||||
@@ -51,7 +53,7 @@ $(function () {
|
||||
targets: 0,
|
||||
orderable: false,
|
||||
className: "select-checkbox",
|
||||
render: function () {
|
||||
render() {
|
||||
return "";
|
||||
},
|
||||
},
|
||||
@@ -60,19 +62,19 @@ $(function () {
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
drawCallback() {
|
||||
$('button[id^="deleteLease_"]').on("click", deleteLease);
|
||||
|
||||
// Hide buttons if all messages were deleted
|
||||
var hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
const hasRows = this.api().rows({ filter: "applied" }).data().length > 0;
|
||||
$(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden");
|
||||
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
rowCallback(row, data) {
|
||||
$(row).attr("data-id", data.ip);
|
||||
var button =
|
||||
const button =
|
||||
'<button type="button" class="btn btn-danger btn-xs" id="deleteLease_' +
|
||||
data.ip +
|
||||
'" data-del-ip="' +
|
||||
@@ -92,7 +94,7 @@ $(function () {
|
||||
text: '<span class="far fa-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectAll",
|
||||
action: function () {
|
||||
action() {
|
||||
dhcpLeaesTable.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -100,7 +102,7 @@ $(function () {
|
||||
text: '<span class="far fa-plus-square"></span>',
|
||||
titleAttr: "Select All",
|
||||
className: "btn-sm datatable-bt selectMore",
|
||||
action: function () {
|
||||
action() {
|
||||
dhcpLeaesTable.rows({ page: "current" }).select();
|
||||
},
|
||||
},
|
||||
@@ -114,7 +116,7 @@ $(function () {
|
||||
text: '<span class="far fa-trash-alt"></span>',
|
||||
titleAttr: "Delete Selected",
|
||||
className: "btn-sm datatable-bt deleteSelected",
|
||||
action: function () {
|
||||
action() {
|
||||
// For each ".selected" row ...
|
||||
$("tr.selected").each(function () {
|
||||
// ... delete the row identified by "data-id".
|
||||
@@ -138,11 +140,11 @@ $(function () {
|
||||
},
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
stateSaveCallback(settings, data) {
|
||||
utils.stateSaveCallback("dhcp-leases-table", data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback("dhcp-leases-table");
|
||||
stateLoadCallback() {
|
||||
const data = utils.stateLoadCallback("dhcp-leases-table");
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
@@ -152,7 +154,7 @@ $(function () {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
dhcpLeaesTable.on("init select deselect", function () {
|
||||
dhcpLeaesTable.on("init select deselect", () => {
|
||||
utils.changeTableButtonStates(dhcpLeaesTable);
|
||||
});
|
||||
});
|
||||
@@ -167,10 +169,10 @@ function delLease(ip) {
|
||||
toasts[ip] = utils.showAlert("info", "", "Deleting lease...", ip, null);
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl + "/dhcp/leases/" + encodeURIComponent(ip),
|
||||
url: document.body.dataset.apiurl + "/dhcp/leases/" + encodeURIComponent(ip),
|
||||
method: "DELETE",
|
||||
})
|
||||
.done(function (response) {
|
||||
.done(response => {
|
||||
utils.enableAll();
|
||||
if (response === undefined) {
|
||||
utils.showAlert(
|
||||
@@ -195,7 +197,7 @@ function delLease(ip) {
|
||||
dhcpLeaesTable.rows().deselect();
|
||||
utils.changeTableButtonStates(dhcpLeaesTable);
|
||||
})
|
||||
.fail(function (jqXHR, exception) {
|
||||
.fail((jqXHR, exception) => {
|
||||
utils.enableAll();
|
||||
utils.showAlert(
|
||||
"error",
|
||||
@@ -214,17 +216,17 @@ function fillDHCPhosts(data) {
|
||||
|
||||
function processDHCPConfig() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/config/dhcp?detailed=true",
|
||||
url: document.body.dataset.apiurl + "/config/dhcp?detailed=true",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
fillDHCPhosts(data.config.dhcp.hosts);
|
||||
setConfigValues("dhcp", "dhcp", data.config.dhcp);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$(() => {
|
||||
processDHCPConfig();
|
||||
});
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Precord see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils: false, apiUrl: false, apiFailure:false, setConfigValues: false */
|
||||
/* global utils: false, apiFailure:false, setConfigValues: false */
|
||||
|
||||
"use strict";
|
||||
|
||||
function hostsDomain(data) {
|
||||
// Split record in format IP NAME1 [NAME2 [NAME3 [NAME...]]]
|
||||
@@ -46,7 +48,7 @@ function CNAMEttl(data) {
|
||||
}
|
||||
|
||||
function populateDataTable(endpoint) {
|
||||
var columns = "";
|
||||
let columns = "";
|
||||
if (endpoint === "hosts") {
|
||||
columns = [
|
||||
{ data: null, render: hostsDomain },
|
||||
@@ -62,10 +64,10 @@ function populateDataTable(endpoint) {
|
||||
];
|
||||
}
|
||||
|
||||
var setByEnv = false;
|
||||
const setByEnv = false;
|
||||
$.ajax({
|
||||
url: `/api/config/dns/${endpoint}?detailed=true`,
|
||||
}).done(function (data) {
|
||||
url: document.body.dataset.apiurl + "/config/dns/" + endpoint + "?detailed=true",
|
||||
}).done(data => {
|
||||
// Set the title icons if needed
|
||||
setConfigValues("dns", "dns", data.config.dns);
|
||||
|
||||
@@ -77,27 +79,27 @@ function populateDataTable(endpoint) {
|
||||
|
||||
$(`#${endpoint}-Table`).DataTable({
|
||||
ajax: {
|
||||
url: `/api/config/dns/${endpoint}`,
|
||||
url: document.body.dataset.apiurl + "/config/dns/" + endpoint,
|
||||
type: "GET",
|
||||
dataSrc: `config.dns.${endpoint}`,
|
||||
},
|
||||
autoWidth: false,
|
||||
columns: columns,
|
||||
columns,
|
||||
columnDefs: [
|
||||
{
|
||||
targets: "_all",
|
||||
render: $.fn.dataTable.render.text(),
|
||||
},
|
||||
],
|
||||
drawCallback: function () {
|
||||
drawCallback() {
|
||||
$(`button[id^="delete${endpoint}"]`).on("click", deleteRecord);
|
||||
|
||||
// Remove visible dropdown to prevent orphaning
|
||||
$("body > .bootstrap-select.dropdown").remove();
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
rowCallback(row, data) {
|
||||
$(row).attr("data-id", data);
|
||||
var button = `<button type="button"
|
||||
const button = `<button type="button"
|
||||
class="btn btn-danger btn-xs"
|
||||
id="delete${endpoint}${utils.hexEncode(data)}"
|
||||
data-tag="${data}"
|
||||
@@ -118,7 +120,7 @@ function populateDataTable(endpoint) {
|
||||
[10, 25, 50, 100, "All"],
|
||||
],
|
||||
language: {
|
||||
emptyTable: function () {
|
||||
emptyTable() {
|
||||
return endpoint === "hosts"
|
||||
? "No local DNS records defined."
|
||||
: "No local CNAME records defined.";
|
||||
@@ -127,11 +129,11 @@ function populateDataTable(endpoint) {
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
processing: true,
|
||||
stateSaveCallback: function (settings, data) {
|
||||
stateSaveCallback(settings, data) {
|
||||
utils.stateSaveCallback(`${endpoint}-records-table`, data);
|
||||
},
|
||||
stateLoadCallback: function () {
|
||||
var data = utils.stateLoadCallback(`${endpoint}-records-table`);
|
||||
stateLoadCallback() {
|
||||
const data = utils.stateLoadCallback(`${endpoint}-records-table`);
|
||||
// Return if not available
|
||||
if (data === null) {
|
||||
return null;
|
||||
@@ -143,7 +145,7 @@ function populateDataTable(endpoint) {
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
populateDataTable("hosts");
|
||||
populateDataTable("cnameRecords");
|
||||
});
|
||||
@@ -156,18 +158,18 @@ function deleteRecord() {
|
||||
function delHosts(elem) {
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Deleting DNS record...", elem);
|
||||
const url = apiUrl + "/config/dns/hosts/" + encodeURIComponent(elem);
|
||||
const url = document.body.dataset.apiurl + "/config/dns/hosts/" + encodeURIComponent(elem);
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
url,
|
||||
method: "DELETE",
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "fas fa-trash-alt", "Successfully deleted DNS record", elem);
|
||||
$("#hosts-Table").DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
.fail((data, exception) => {
|
||||
utils.enableAll();
|
||||
apiFailure(data);
|
||||
utils.showAlert(
|
||||
@@ -183,13 +185,13 @@ function delHosts(elem) {
|
||||
function delCNAME(elem) {
|
||||
utils.disableAll();
|
||||
utils.showAlert("info", "", "Deleting local CNAME record...", elem);
|
||||
const url = apiUrl + "/config/dns/cnameRecords/" + encodeURIComponent(elem);
|
||||
const url = document.body.dataset.apiurl + "/config/dns/cnameRecords/" + encodeURIComponent(elem);
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
url,
|
||||
method: "DELETE",
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
utils.enableAll();
|
||||
utils.showAlert(
|
||||
"success",
|
||||
@@ -199,7 +201,7 @@ function delCNAME(elem) {
|
||||
);
|
||||
$("#cnameRecords-Table").DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
.fail((data, exception) => {
|
||||
utils.enableAll();
|
||||
apiFailure(data);
|
||||
utils.showAlert(
|
||||
@@ -212,53 +214,56 @@ function delCNAME(elem) {
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$("#btnAdd-host").on("click", function () {
|
||||
$(() => {
|
||||
$("#btnAdd-host").on("click", () => {
|
||||
utils.disableAll();
|
||||
const elem = $("#Hip").val() + " " + $("#Hdomain").val();
|
||||
const url = apiUrl + "/config/dns/hosts/" + encodeURIComponent(elem);
|
||||
const url = document.body.dataset.apiurl + "/config/dns/hosts/" + encodeURIComponent(elem);
|
||||
utils.showAlert("info", "", "Adding DNS record...", elem);
|
||||
$.ajax({
|
||||
url: url,
|
||||
url,
|
||||
method: "PUT",
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added DNS record", elem);
|
||||
$("#Hdomain").val("");
|
||||
$("#Hip").val("");
|
||||
$("#hosts-Table").DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
.fail((data, exception) => {
|
||||
utils.enableAll();
|
||||
apiFailure(data);
|
||||
utils.showAlert("error", "", "Error while deleting DNS record", data.responseText);
|
||||
utils.showAlert("error", "", "Error while adding DNS record", data.responseText);
|
||||
console.log(exception); // eslint-disable-line no-console
|
||||
});
|
||||
});
|
||||
|
||||
$("#btnAdd-cname").on("click", function () {
|
||||
$("#btnAdd-cname").on("click", () => {
|
||||
utils.disableAll();
|
||||
var elem = $("#Cdomain").val() + "," + $("#Ctarget").val();
|
||||
var ttlVal = parseInt($("#Cttl").val(), 10);
|
||||
let elem = $("#Cdomain").val() + "," + $("#Ctarget").val();
|
||||
const ttlVal = Number.parseInt($("#Cttl").val(), 10);
|
||||
// TODO Fix eslint
|
||||
// eslint-disable-next-line unicorn/prefer-number-properties
|
||||
if (isFinite(ttlVal) && ttlVal >= 0) elem += "," + ttlVal;
|
||||
const url = apiUrl + "/config/dns/cnameRecords/" + encodeURIComponent(elem);
|
||||
const url =
|
||||
document.body.dataset.apiurl + "/config/dns/cnameRecords/" + encodeURIComponent(elem);
|
||||
utils.showAlert("info", "", "Adding DNS record...", elem);
|
||||
$.ajax({
|
||||
url: url,
|
||||
url,
|
||||
method: "PUT",
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
utils.enableAll();
|
||||
utils.showAlert("success", "fas fa-plus", "Successfully added CNAME record", elem);
|
||||
$("#Cdomain").val("");
|
||||
$("#Ctarget").val("");
|
||||
$("#cnameRecords-Table").DataTable().ajax.reload(null, false);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
.fail((data, exception) => {
|
||||
utils.enableAll();
|
||||
apiFailure(data);
|
||||
utils.showAlert("error", "", "Error while deleting CNAME record", data.responseText);
|
||||
utils.showAlert("error", "", "Error while adding CNAME record", data.responseText);
|
||||
console.log(exception); // eslint-disable-line no-console
|
||||
});
|
||||
});
|
||||
|
||||
+38
-20
@@ -5,11 +5,13 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global applyCheckboxRadioStyle:false, setConfigValues: false, apiFailure: false, apiUrl: false */
|
||||
/* global applyCheckboxRadioStyle:false, setConfigValues: false, apiFailure: false */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Remove an element from an array (inline)
|
||||
function removeFromArray(arr, what) {
|
||||
var found = arr.indexOf(what);
|
||||
let found = arr.indexOf(what);
|
||||
|
||||
while (found !== -1) {
|
||||
arr.splice(found, 1);
|
||||
@@ -18,16 +20,16 @@ function removeFromArray(arr, what) {
|
||||
}
|
||||
|
||||
function fillDNSupstreams(value, servers) {
|
||||
var disabledStr = "";
|
||||
let disabledStr = "";
|
||||
if (value.flags.env_var === true) {
|
||||
$("#DNSupstreamsTextfield").prop("disabled", true);
|
||||
disabledStr = 'disabled="Disabled"';
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
var customServers = value.value.length;
|
||||
servers.forEach(element => {
|
||||
var row = "<tr>";
|
||||
let i = 0;
|
||||
let customServers = value.value.length;
|
||||
for (const element of servers) {
|
||||
let row = "<tr>";
|
||||
// Build checkboxes for IPv4 and IPv6
|
||||
const addresses = [element.v4, element.v6];
|
||||
// Loop over address types (IPv4, IPv6)
|
||||
@@ -36,7 +38,7 @@ function fillDNSupstreams(value, servers) {
|
||||
// Loop over available addresses (up to 2)
|
||||
for (let index = 0; index < 2; index++) {
|
||||
if (address.length > index) {
|
||||
var checkedStr = "";
|
||||
let checkedStr = "";
|
||||
if (
|
||||
value.value.includes(address[index]) ||
|
||||
value.value.includes(address[index] + "#53")
|
||||
@@ -65,14 +67,14 @@ function fillDNSupstreams(value, servers) {
|
||||
|
||||
// Add row to table
|
||||
$("#DNSupstreamsTable").append(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener to checkboxes
|
||||
$("input[id^='DNSupstreams-']").on("change", function () {
|
||||
var upstreams = $("#DNSupstreamsTextfield").val().split("\n");
|
||||
var customServers = 0;
|
||||
$("input[id^='DNSupstreams-']").on("change", () => {
|
||||
const upstreams = $("#DNSupstreamsTextfield").val().split("\n");
|
||||
let customServers = 0;
|
||||
$("#DNSupstreamsTable input").each(function () {
|
||||
var title = $(this).closest("td").attr("title");
|
||||
const title = $(this).closest("td").attr("title");
|
||||
if (this.checked && !upstreams.includes(title)) {
|
||||
// Add server to array
|
||||
upstreams.push(title);
|
||||
@@ -99,9 +101,25 @@ function fillDNSupstreams(value, servers) {
|
||||
applyCheckboxRadioStyle();
|
||||
}
|
||||
|
||||
function setInterfaceName(interface) {
|
||||
$("#interface-name-1").text(interface);
|
||||
$("#interface-name-2").text(interface);
|
||||
function setInterfaceName(name) {
|
||||
// If dns.interface is empty in pihole.toml, we use the first interface
|
||||
// (same default value used by FTL)
|
||||
if (name === "") {
|
||||
$.ajax({
|
||||
url: document.body.dataset.apiurl + "/network/gateway",
|
||||
async: false,
|
||||
})
|
||||
.done(data => {
|
||||
name = data.gateway[0].interface;
|
||||
})
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
name = "not found";
|
||||
});
|
||||
}
|
||||
|
||||
$("#interface-name-1").text(name);
|
||||
$("#interface-name-2").text(name);
|
||||
}
|
||||
|
||||
// Update the textfield with all (incl. custom) upstream servers
|
||||
@@ -114,19 +132,19 @@ function updateDNSserversTextfield(upstreams, customServers) {
|
||||
|
||||
function processDNSConfig() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/config/dns?detailed=true", // We need the detailed output to get the DNS server list
|
||||
url: document.body.dataset.apiurl + "/config/dns?detailed=true", // We need the detailed output to get the DNS server list
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
// Initialize the DNS upstreams
|
||||
fillDNSupstreams(data.config.dns.upstreams, data.dns_servers);
|
||||
setInterfaceName(data.config.dns.interface.value);
|
||||
setConfigValues("dns", "dns", data.config.dns);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$(() => {
|
||||
processDNSConfig();
|
||||
});
|
||||
|
||||
@@ -5,20 +5,22 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global setConfigValues: false, apiFailure: false, apiUrl: false */
|
||||
/* global setConfigValues: false, apiFailure: false */
|
||||
|
||||
"use strict";
|
||||
|
||||
function getConfig() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/config/?detailed=true",
|
||||
url: document.body.dataset.apiurl + "/config/?detailed=true",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
setConfigValues("", "", data.config);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$(() => {
|
||||
getConfig();
|
||||
});
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global apiFailure:false, apiUrl: false, Chart:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false, REFRESH_INTERVAL: false, utils: false */
|
||||
/* global apiFailure:false, Chart:false, THEME_COLORS:false, customTooltips:false, htmlLegendPlugin:false,doughnutTooltip:false, ChartDeferred:false, REFRESH_INTERVAL: false, utils: false */
|
||||
|
||||
var hostinfoTimer = null;
|
||||
var cachePieChart = null;
|
||||
var cacheSize = 0,
|
||||
cacheEntries = 0;
|
||||
"use strict";
|
||||
|
||||
let hostinfoTimer = null;
|
||||
let cachePieChart = null;
|
||||
let cacheSize = 0;
|
||||
let cacheEntries = 0;
|
||||
|
||||
// Register the ChartDeferred plugin to all charts:
|
||||
Chart.register(ChartDeferred);
|
||||
@@ -20,34 +22,37 @@ Chart.defaults.set("plugins.deferred", {
|
||||
});
|
||||
|
||||
function updateCachePie(data) {
|
||||
var v = [],
|
||||
c = [],
|
||||
k = [],
|
||||
i = 0;
|
||||
const v = [];
|
||||
const c = [];
|
||||
const k = [];
|
||||
let i = 0;
|
||||
|
||||
// Compute total number of cache entries
|
||||
cacheEntries = 0;
|
||||
Object.keys(data).forEach(function (item) {
|
||||
for (const item of Object.keys(data)) {
|
||||
cacheEntries += data[item].valid;
|
||||
cacheEntries += data[item].stale;
|
||||
});
|
||||
}
|
||||
|
||||
// Sort data by value, put OTHER always as last
|
||||
var sorted = Object.keys(data).sort(function (a, b) {
|
||||
const sorted = Object.keys(data).sort((a, b) => {
|
||||
if (a === "OTHER") {
|
||||
return 1;
|
||||
} else if (b === "OTHER") {
|
||||
return -1;
|
||||
} else {
|
||||
return data[b].valid + data[b].stale - (data[a].valid + data[a].stale);
|
||||
}
|
||||
|
||||
if (b === "OTHER") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return data[b].valid + data[b].stale - (data[a].valid + data[a].stale);
|
||||
});
|
||||
|
||||
// Rebuild data object
|
||||
var tmp = {};
|
||||
sorted.forEach(function (item) {
|
||||
const tmp = {};
|
||||
for (const item of sorted) {
|
||||
tmp[item] = data[item];
|
||||
});
|
||||
}
|
||||
|
||||
data = tmp;
|
||||
|
||||
// Add empty space to chart
|
||||
@@ -55,27 +60,27 @@ function updateCachePie(data) {
|
||||
data.empty.valid = cacheSize - cacheEntries;
|
||||
|
||||
// Fill chart with data
|
||||
Object.keys(data).forEach(function (item) {
|
||||
if (data[item].valid > 0) {
|
||||
v.push((100 * data[item].valid) / cacheSize);
|
||||
for (const [item, value] of Object.entries(data)) {
|
||||
if (value.valid > 0) {
|
||||
v.push((100 * value.valid) / cacheSize);
|
||||
c.push(item !== "empty" ? THEME_COLORS[i++ % THEME_COLORS.length] : "#80808040");
|
||||
k.push(item);
|
||||
}
|
||||
|
||||
if (data[item].stale > 0) {
|
||||
if (value.stale > 0) {
|
||||
// There are no stale empty entries
|
||||
v.push((100 * data[item].stale) / cacheSize);
|
||||
v.push((100 * value.stale) / cacheSize);
|
||||
c.push(THEME_COLORS[i++ % THEME_COLORS.length]);
|
||||
k.push(item + " (stale)");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Build a single dataset with the data to be pushed
|
||||
var dd = { data: v, backgroundColor: c };
|
||||
const dd = { data: v, backgroundColor: c };
|
||||
// and push it at once
|
||||
cachePieChart.data.datasets[0] = dd;
|
||||
cachePieChart.data.labels = k;
|
||||
$("#cache-pie-chart .overlay").hide();
|
||||
|
||||
// Passing 'none' will prevent rotation animation for further updates
|
||||
//https://www.chartjs.org/docs/latest/developers/updates.html#preventing-animations
|
||||
cachePieChart.update("none");
|
||||
@@ -83,11 +88,11 @@ function updateCachePie(data) {
|
||||
|
||||
function updateHostInfo() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/info/host",
|
||||
url: document.body.dataset.apiurl + "/info/host",
|
||||
})
|
||||
.done(function (data) {
|
||||
var host = data.host;
|
||||
var uname = host.uname;
|
||||
.done(data => {
|
||||
const host = data.host;
|
||||
const uname = host.uname;
|
||||
if (uname.domainname !== "(none)") {
|
||||
$("#sysinfo-hostname").text(uname.nodename + "." + uname.domainname);
|
||||
} else {
|
||||
@@ -108,7 +113,7 @@ function updateHostInfo() {
|
||||
clearTimeout(hostinfoTimer);
|
||||
hostinfoTimer = utils.setTimer(updateHostInfo, REFRESH_INTERVAL.hosts);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
@@ -116,7 +121,7 @@ function updateHostInfo() {
|
||||
// Walk nested objects, create a dash-separated global key and assign the value
|
||||
// to the corresponding element (add percentage for DNS replies)
|
||||
function setMetrics(data, prefix) {
|
||||
var cacheData = {};
|
||||
const cacheData = {};
|
||||
for (const [key, val] of Object.entries(data)) {
|
||||
if (prefix === "sysinfo-dns-cache-content-") {
|
||||
// Create table row for each DNS cache entry
|
||||
@@ -151,14 +156,14 @@ function setMetrics(data, prefix) {
|
||||
}
|
||||
}
|
||||
|
||||
var metricsTimer = null;
|
||||
let metricsTimer = null;
|
||||
|
||||
function updateMetrics() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/info/metrics",
|
||||
url: document.body.dataset.apiurl + "/info/metrics",
|
||||
})
|
||||
.done(function (data) {
|
||||
var metrics = data.metrics;
|
||||
.done(data => {
|
||||
const metrics = data.metrics;
|
||||
$("#dns-cache-table").empty();
|
||||
|
||||
// Set global cache size
|
||||
@@ -175,7 +180,7 @@ function updateMetrics() {
|
||||
clearTimeout(metricsTimer);
|
||||
metricsTimer = utils.setTimer(updateMetrics, REFRESH_INTERVAL.metrics);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
@@ -196,12 +201,12 @@ function showQueryLoggingButton(state) {
|
||||
|
||||
function getLoggingButton() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/config/dns/queryLogging",
|
||||
url: document.body.dataset.apiurl + "/config/dns/queryLogging",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
showQueryLoggingButton(data.config.dns.queryLogging);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
}
|
||||
@@ -212,15 +217,15 @@ $(".confirm-restartdns").confirm({
|
||||
"This will clear the DNS cache and may temporarily interrupt your internet connection.<br>" +
|
||||
"Furthermore, you will be logged out of the web interface as consequence of this action.",
|
||||
title: "Confirmation required",
|
||||
confirm: function () {
|
||||
confirm() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/action/restartdns",
|
||||
url: document.body.dataset.apiurl + "/action/restartdns",
|
||||
type: "POST",
|
||||
}).fail(function (data) {
|
||||
}).fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
},
|
||||
cancel: function () {
|
||||
cancel() {
|
||||
// nothing to do
|
||||
},
|
||||
confirmButton: "Yes, restart DNS server",
|
||||
@@ -236,15 +241,15 @@ $(".confirm-flushlogs").confirm({
|
||||
"Are you sure you want to flush your logs?<br><br>" +
|
||||
"<strong>This will clear all logs and cannot be undone.</strong>",
|
||||
title: "Confirmation required",
|
||||
confirm: function () {
|
||||
confirm() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/action/flush/logs",
|
||||
url: document.body.dataset.apiurl + "/action/flush/logs",
|
||||
type: "POST",
|
||||
}).fail(function (data) {
|
||||
}).fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
},
|
||||
cancel: function () {
|
||||
cancel() {
|
||||
// nothing to do
|
||||
},
|
||||
confirmButton: "Yes, flush logs",
|
||||
@@ -260,15 +265,15 @@ $(".confirm-flusharp").confirm({
|
||||
"Are you sure you want to flush your network table?<br><br>" +
|
||||
"<strong>This will clear all entries and cannot be undone.</strong>",
|
||||
title: "Confirmation required",
|
||||
confirm: function () {
|
||||
confirm() {
|
||||
$.ajax({
|
||||
url: apiUrl + "/action/flush/arp",
|
||||
url: document.body.dataset.apiurl + "/action/flush/arp",
|
||||
type: "POST",
|
||||
}).fail(function (data) {
|
||||
}).fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
},
|
||||
cancel: function () {
|
||||
cancel() {
|
||||
// nothing to do
|
||||
},
|
||||
confirmButton: "Yes, flush my network table",
|
||||
@@ -283,30 +288,30 @@ $("#loggingButton").confirm({
|
||||
text:
|
||||
"Are you sure you want to switch query logging mode?<br><br>" +
|
||||
"<strong>This will restart the DNS server.</strong><br>" +
|
||||
"As consequence of this action, your DNS cache will be cleared and you may temporarily loose your internet connection.<br>" +
|
||||
"As consequence of this action, your DNS cache will be cleared and you may temporarily lose your internet connection.<br>" +
|
||||
"Furthermore, you will be logged out of the web interface.",
|
||||
title: "Confirmation required",
|
||||
confirm: function () {
|
||||
confirm() {
|
||||
const data = {};
|
||||
data.config = {};
|
||||
data.config.dns = {};
|
||||
data.config.dns.queryLogging = $("#loggingButton").data("state") !== "enabled";
|
||||
$.ajax({
|
||||
url: apiUrl + "/config/dns/queryLogging",
|
||||
url: document.body.dataset.apiurl + "/config/dns/queryLogging",
|
||||
type: "PATCH",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
showQueryLoggingButton(data.config.dns.queryLogging);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
},
|
||||
cancel: function () {
|
||||
cancel() {
|
||||
// nothing to do
|
||||
},
|
||||
confirmButton: "Yes, change query logging",
|
||||
@@ -317,12 +322,12 @@ $("#loggingButton").confirm({
|
||||
dialogClass: "modal-dialog",
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
updateHostInfo();
|
||||
updateMetrics();
|
||||
getLoggingButton();
|
||||
|
||||
var ctx = document.getElementById("cachePieChart").getContext("2d");
|
||||
const ctx = document.getElementById("cachePieChart").getContext("2d");
|
||||
cachePieChart = new Chart(ctx, {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
@@ -350,7 +355,7 @@ $(function () {
|
||||
enabled: false,
|
||||
external: customTooltips,
|
||||
callbacks: {
|
||||
title: function () {
|
||||
title() {
|
||||
return "Cache content";
|
||||
},
|
||||
label: doughnutTooltip,
|
||||
@@ -364,9 +369,9 @@ $(function () {
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl + "/network/gateway",
|
||||
url: document.body.dataset.apiurl + "/network/gateway",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
const gateway = data.gateway;
|
||||
// Get first object in gateway that has family == "inet"
|
||||
const inet = gateway.find(obj => obj.family === "inet");
|
||||
@@ -378,7 +383,7 @@ $(function () {
|
||||
$("#sysinfo-gw-v6-addr").text(inet6 ? inet6.local.join("\n") : "N/A");
|
||||
$("#sysinfo-gw-v6-iface").text(inet6 ? inet6.interface : "N/A");
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,24 +5,26 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl: false */
|
||||
/* global utils:false */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Add event listener to import button
|
||||
document.getElementById("submit-import").addEventListener("click", function () {
|
||||
document.getElementById("submit-import").addEventListener("click", () => {
|
||||
importZIP();
|
||||
});
|
||||
|
||||
// Upload file to Pi-hole
|
||||
function importZIP() {
|
||||
var file = document.getElementById("file").files[0];
|
||||
const file = document.getElementById("file").files[0];
|
||||
if (file === undefined) {
|
||||
alert("Please select a file to import.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the selected import options
|
||||
const imports = {},
|
||||
gravity = {};
|
||||
const imports = {};
|
||||
const gravity = {};
|
||||
imports.config = document.getElementById("import.config").checked;
|
||||
imports.dhcp_leases = document.getElementById("import.dhcp_leases").checked;
|
||||
gravity.group = document.getElementById("import.gravity.group").checked;
|
||||
@@ -37,7 +39,7 @@ function importZIP() {
|
||||
const formData = new FormData();
|
||||
formData.append("import", JSON.stringify(imports));
|
||||
formData.append("file", file);
|
||||
fetch(apiUrl + "/teleporter", {
|
||||
fetch(document.body.dataset.apiurl + "/teleporter", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content") },
|
||||
@@ -56,9 +58,9 @@ function importZIP() {
|
||||
} else if ("files" in data) {
|
||||
$("#modal-import-success").show();
|
||||
$("#modal-import-success-title").text("Import successful");
|
||||
var text = "<p>Processed files:</p><ul>";
|
||||
for (var i = 0; i < data.files.length; i++) {
|
||||
text += "<li>" + utils.escapeHtml(data.files[i]) + "</li>";
|
||||
let text = "<p>Processed files:</p><ul>";
|
||||
for (const file of data.files) {
|
||||
text += "<li>" + utils.escapeHtml(file) + "</li>";
|
||||
}
|
||||
|
||||
text += "</ul>";
|
||||
@@ -75,17 +77,17 @@ function importZIP() {
|
||||
}
|
||||
|
||||
// Inspired by https://stackoverflow.com/a/59576416/2087442
|
||||
$("#GETTeleporter").on("click", function () {
|
||||
$("#GETTeleporter").on("click", () => {
|
||||
$.ajax({
|
||||
url: apiUrl + "/teleporter",
|
||||
url: document.body.dataset.apiurl + "/teleporter",
|
||||
headers: { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content") },
|
||||
method: "GET",
|
||||
xhrFields: {
|
||||
responseType: "blob",
|
||||
},
|
||||
success: function (data, status, xhr) {
|
||||
var a = document.createElement("a");
|
||||
var url = globalThis.URL.createObjectURL(data);
|
||||
success(data, status, xhr) {
|
||||
const a = document.createElement("a");
|
||||
const url = globalThis.URL.createObjectURL(data);
|
||||
|
||||
a.href = url;
|
||||
a.download = xhr.getResponseHeader("Content-Disposition").match(/filename="([^"]*)"/)[1];
|
||||
@@ -98,7 +100,7 @@ $("#GETTeleporter").on("click", function () {
|
||||
});
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
// Show warning if not accessed over HTTPS
|
||||
if (location.protocol !== "https:") {
|
||||
$("#encryption-warning").show();
|
||||
|
||||
+27
-24
@@ -5,9 +5,11 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global utils:false, apiUrl:false, apiFailure:false*/
|
||||
/* global utils:false, apiFailure:false*/
|
||||
|
||||
$(function () {
|
||||
"use strict";
|
||||
|
||||
$(() => {
|
||||
// Handle hiding of alerts
|
||||
$("[data-hide]").on("click", function () {
|
||||
$(this)
|
||||
@@ -16,7 +18,7 @@ $(function () {
|
||||
});
|
||||
|
||||
// Handle saving of settings
|
||||
$(".save-button").on("click", function () {
|
||||
$(".save-button").on("click", () => {
|
||||
saveSettings();
|
||||
});
|
||||
});
|
||||
@@ -26,18 +28,18 @@ $(function () {
|
||||
function setConfigValues(topic, key, value) {
|
||||
// If the value is an object, we need to recurse
|
||||
if (!("description" in value)) {
|
||||
Object.keys(value).forEach(function (subkey) {
|
||||
var subvalue = value[subkey];
|
||||
for (const [subkey, subvalue] of Object.entries(value)) {
|
||||
// If the key is empty, we are at the top level
|
||||
var newKey = key === "" ? subkey : key + "." + subkey;
|
||||
const newKey = key === "" ? subkey : key + "." + subkey;
|
||||
setConfigValues(topic, newKey, subvalue);
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// else: we have a setting we can set
|
||||
var escapedKey = key.replaceAll(".", "\\.");
|
||||
var envTitle = $(`[data-configkeys~='${key}']`);
|
||||
const escapedKey = key.replaceAll(".", "\\.");
|
||||
const envTitle = $(`[data-configkeys~='${key}']`);
|
||||
|
||||
if (
|
||||
envTitle.parents().parents().hasClass("settings-level-expert") &&
|
||||
@@ -71,13 +73,14 @@ function setConfigValues(topic, key, value) {
|
||||
// Remove all options from select
|
||||
$("#" + escapedKey + " option").remove();
|
||||
// Add allowed select items (if available)
|
||||
value.allowed.forEach(function (allowedValue) {
|
||||
for (const allowedValue of value.allowed) {
|
||||
$("#" + escapedKey + "-" + allowedValue.item).prop("disabled", value.flags.env_var);
|
||||
var newopt = $("<option></option>")
|
||||
const newopt = $("<option></option>")
|
||||
.attr("value", allowedValue.item)
|
||||
.text(allowedValue.description);
|
||||
$("#" + escapedKey).append(newopt);
|
||||
});
|
||||
}
|
||||
|
||||
// Select the current value
|
||||
$("#" + escapedKey)
|
||||
.val(value.value)
|
||||
@@ -116,11 +119,11 @@ function setConfigValues(topic, key, value) {
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
var settings = {};
|
||||
const settings = {};
|
||||
utils.disableAll();
|
||||
$("[data-key]").each(function () {
|
||||
var key = $(this).data("key");
|
||||
var value = $(this).val();
|
||||
const key = $(this).data("key");
|
||||
let value = $(this).val();
|
||||
|
||||
// If this is a checkbox, use the checked state
|
||||
if ($(this).is(":checkbox")) {
|
||||
@@ -138,20 +141,20 @@ function saveSettings() {
|
||||
|
||||
// If this is an integer number, parse it accordingly
|
||||
if ($(this).data("type") === "integer") {
|
||||
value = parseInt(value, 10);
|
||||
value = Number.parseInt(value, 10);
|
||||
}
|
||||
|
||||
// If this is a floating point value, parse it accordingly
|
||||
if ($(this).data("type") === "float") {
|
||||
value = parseFloat(value);
|
||||
value = Number.parseFloat(value);
|
||||
}
|
||||
|
||||
// Build deep object
|
||||
// Transform "foo.bar.baz" into {foo: {bar: {baz: value}}}
|
||||
var parts = key.split(".");
|
||||
var obj = {};
|
||||
var tmp = obj;
|
||||
for (var i = 0; i < parts.length - 1; i++) {
|
||||
const parts = key.split(".");
|
||||
const obj = {};
|
||||
let tmp = obj;
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
tmp[parts[i]] = {};
|
||||
tmp = tmp[parts[i]];
|
||||
}
|
||||
@@ -164,14 +167,14 @@ function saveSettings() {
|
||||
|
||||
// Apply changes
|
||||
$.ajax({
|
||||
url: apiUrl + "/config",
|
||||
url: document.body.dataset.apiurl + "/config",
|
||||
method: "PATCH",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
data: JSON.stringify({ config: settings }),
|
||||
contentType: "application/json; charset=utf-8",
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
utils.enableAll();
|
||||
// Success
|
||||
utils.showAlert(
|
||||
@@ -183,7 +186,7 @@ function saveSettings() {
|
||||
// Show loading overlay
|
||||
utils.loadingOverlay(true);
|
||||
})
|
||||
.fail(function (data, exception) {
|
||||
.fail((data, exception) => {
|
||||
utils.enableAll();
|
||||
utils.showAlert("error", "", "Error while applying settings", data.responseText);
|
||||
console.log(exception); // eslint-disable-line no-console
|
||||
|
||||
+19
-15
@@ -5,10 +5,12 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global moment: false, apiFailure: false, utils: false, REFRESH_INTERVAL: false, apiUrl: false */
|
||||
/* global moment: false, apiFailure: false, utils: false, REFRESH_INTERVAL: false */
|
||||
|
||||
var nextID = 0;
|
||||
var lastPID = -1;
|
||||
"use strict";
|
||||
|
||||
let nextID = 0;
|
||||
let lastPID = -1;
|
||||
|
||||
// Maximum number of lines to display
|
||||
const maxlines = 5000;
|
||||
@@ -69,6 +71,8 @@ function formatFTL(line, prio) {
|
||||
return `<span class="${prioClass}">${utils.escapeHtml(prio)}</span> ${line}`;
|
||||
}
|
||||
|
||||
let gAutoScrolling;
|
||||
|
||||
// Function that asks the API for new data
|
||||
function getData() {
|
||||
// Only update when spinner is spinning
|
||||
@@ -77,18 +81,18 @@ function getData() {
|
||||
return;
|
||||
}
|
||||
|
||||
var GETDict = utils.parseQueryString();
|
||||
const GETDict = utils.parseQueryString();
|
||||
if (!("file" in GETDict)) {
|
||||
globalThis.location.href += "?file=dnsmasq";
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl + "/logs/" + GETDict.file + "?nextID=" + nextID,
|
||||
url: document.body.dataset.apiurl + "/logs/" + GETDict.file + "?nextID=" + nextID,
|
||||
timeout: 5000,
|
||||
method: "GET",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
// Check if we have a new PID -> FTL was restarted
|
||||
if (lastPID !== data.pid) {
|
||||
if (lastPID !== -1) {
|
||||
@@ -107,7 +111,7 @@ function getData() {
|
||||
// Set placeholder text if log file is empty and we have no new lines
|
||||
if (data.log.length === 0) {
|
||||
if (nextID === 0) {
|
||||
$("#output").html("<div><i>*** Log file is empty ***</i></div>");
|
||||
$("#output").html("<div><em>*** Log file is empty ***</em></div>");
|
||||
}
|
||||
|
||||
utils.setTimer(getData, REFRESH_INTERVAL.logs);
|
||||
@@ -120,7 +124,7 @@ function getData() {
|
||||
$("#output").append('<hr class="hr-small">').children(":last").fadeOut(2000);
|
||||
}
|
||||
|
||||
data.log.forEach(function (line) {
|
||||
for (const line of data.log) {
|
||||
// Escape HTML
|
||||
line.message = utils.escapeHtml(line.message);
|
||||
// Format line if applicable
|
||||
@@ -139,10 +143,10 @@ function getData() {
|
||||
//$(".left-line:last").fadeOut(2000);
|
||||
$("#output").children(":last").hide().fadeIn("fast");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Limit output to <maxlines> lines
|
||||
var lines = $("#output").val().split("\n");
|
||||
const lines = $("#output").val().split("\n");
|
||||
if (lines.length > maxlines) {
|
||||
lines.splice(0, lines.length - maxlines);
|
||||
$("#output").val(lines.join("\n"));
|
||||
@@ -162,14 +166,14 @@ function getData() {
|
||||
|
||||
utils.setTimer(getData, REFRESH_INTERVAL.logs);
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
apiFailure(data);
|
||||
utils.setTimer(getData, 5 * REFRESH_INTERVAL.logs);
|
||||
});
|
||||
}
|
||||
|
||||
var gAutoScrolling = true;
|
||||
$("#output").on("scroll", function () {
|
||||
gAutoScrolling = true;
|
||||
$("#output").on("scroll", () => {
|
||||
// Check if we are at the bottom of the output
|
||||
//
|
||||
// - $("#output")[0].scrollHeight: This gets the entire height of the content
|
||||
@@ -185,7 +189,7 @@ $("#output").on("scroll", function () {
|
||||
const bottom =
|
||||
$("#output")[0].scrollHeight - $("#output").innerHeight() - $("#output").scrollTop();
|
||||
// Add a tolerance of four line heights
|
||||
const tolerance = 4 * parseFloat($("#output").css("line-height"));
|
||||
const tolerance = 4 * Number.parseFloat($("#output").css("line-height"));
|
||||
if (bottom <= tolerance) {
|
||||
// Auto-scrolling is enabled
|
||||
gAutoScrolling = true;
|
||||
@@ -199,7 +203,7 @@ $("#output").on("scroll", function () {
|
||||
}
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$(() => {
|
||||
getData();
|
||||
|
||||
// Clicking on the element with class "fa-spinner" will toggle the play/pause state
|
||||
|
||||
+171
-204
@@ -5,9 +5,11 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
/* global moment:false, apiUrl: false, apiFailure: false, updateFtlInfo: false, NProgress:false, WaitMe:false */
|
||||
/* global moment:false, apiFailure: false, updateFtlInfo: false, NProgress:false, WaitMe:false */
|
||||
|
||||
$(function () {
|
||||
"use strict";
|
||||
|
||||
$(() => {
|
||||
// CSRF protection for AJAX requests, this has to be configured globally
|
||||
// because we are using the jQuery $.ajax() function directly in some cases
|
||||
// Furthermore, has this to be done before any AJAX request is made so that
|
||||
@@ -19,7 +21,7 @@ $(function () {
|
||||
|
||||
// Credit: https://stackoverflow.com/a/4835406
|
||||
function escapeHtml(text) {
|
||||
var map = {
|
||||
const map = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
@@ -30,13 +32,11 @@ function escapeHtml(text) {
|
||||
// Return early when text is not a string
|
||||
if (typeof text !== "string") return text;
|
||||
|
||||
return text.replaceAll(/[&<>"']/g, function (m) {
|
||||
return map[m];
|
||||
});
|
||||
return text.replaceAll(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
function unescapeHtml(text) {
|
||||
var map = {
|
||||
const map = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
@@ -55,53 +55,33 @@ function unescapeHtml(text) {
|
||||
|
||||
return text.replaceAll(
|
||||
/&(?:amp|lt|gt|quot|#039|Uuml|uuml|Auml|auml|Ouml|ouml|szlig);/g,
|
||||
function (m) {
|
||||
return map[m];
|
||||
}
|
||||
m => map[m]
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function for converting Objects to Arrays after sorting the keys
|
||||
function objectToArray(obj) {
|
||||
var arr = [];
|
||||
var idx = [];
|
||||
var keys = Object.keys(obj);
|
||||
|
||||
keys.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
arr.push(obj[keys[i]]);
|
||||
idx.push(keys[i]);
|
||||
}
|
||||
|
||||
return [idx, arr];
|
||||
}
|
||||
|
||||
function padNumber(num) {
|
||||
return ("00" + num).substr(-2, 2);
|
||||
}
|
||||
|
||||
var showAlertBox = null;
|
||||
let showAlertBox = null;
|
||||
function showAlert(type, icon, title, message, toast) {
|
||||
const options = {
|
||||
title: " <strong>" + escapeHtml(title) + "</strong><br>",
|
||||
message: escapeHtml(message),
|
||||
icon: icon,
|
||||
title: " <strong>" + escapeHtml(title) + "</strong><br>",
|
||||
message: escapeHtml(message),
|
||||
icon,
|
||||
};
|
||||
const settings = {
|
||||
type,
|
||||
delay: 5000, // default value
|
||||
mouse_over: "pause",
|
||||
animate: {
|
||||
enter: "animate__animated animate__fadeInDown",
|
||||
exit: "animate__animated animate__fadeOutUp",
|
||||
},
|
||||
settings = {
|
||||
type: type,
|
||||
delay: 5000, // default value
|
||||
mouse_over: "pause",
|
||||
animate: {
|
||||
enter: "animate__animated animate__fadeInDown",
|
||||
exit: "animate__animated animate__fadeOutUp",
|
||||
},
|
||||
};
|
||||
};
|
||||
switch (type) {
|
||||
case "info":
|
||||
options.icon = icon !== null && icon.len > 0 ? icon : "fas fa-clock";
|
||||
options.icon = icon !== null && icon.length > 0 ? icon : "fas fa-clock";
|
||||
|
||||
break;
|
||||
case "success":
|
||||
@@ -120,7 +100,7 @@ function showAlert(type, icon, title, message, toast) {
|
||||
// If the message is an API object, nicely format the error message
|
||||
// Try to parse message as JSON
|
||||
try {
|
||||
var data = JSON.parse(message);
|
||||
const data = JSON.parse(message);
|
||||
console.log(data); // eslint-disable-line no-console
|
||||
if (data.error !== undefined) {
|
||||
options.title = " <strong>" + escapeHtml(data.error.message) + "</strong><br>";
|
||||
@@ -143,24 +123,28 @@ function showAlert(type, icon, title, message, toast) {
|
||||
// Create a new notification for info boxes
|
||||
showAlertBox = $.notify(options, settings);
|
||||
return showAlertBox;
|
||||
} else if (showAlertBox !== null) {
|
||||
}
|
||||
|
||||
if (showAlertBox !== null) {
|
||||
// Update existing notification for other boxes (if available)
|
||||
showAlertBox.update(options);
|
||||
showAlertBox.update(settings);
|
||||
return showAlertBox;
|
||||
} else {
|
||||
// Create a new notification for other boxes if no previous info box exists
|
||||
return $.notify(options, settings);
|
||||
}
|
||||
} else if (toast === null) {
|
||||
|
||||
// Create a new notification for other boxes if no previous info box exists
|
||||
return $.notify(options, settings);
|
||||
}
|
||||
|
||||
if (toast === null) {
|
||||
// Always create a new toast
|
||||
return $.notify(options, settings);
|
||||
} else {
|
||||
// Update existing toast
|
||||
toast.update(options);
|
||||
toast.update(settings);
|
||||
return toast;
|
||||
}
|
||||
|
||||
// Update existing toast
|
||||
toast.update(options);
|
||||
toast.update(settings);
|
||||
return toast;
|
||||
}
|
||||
|
||||
function datetime(date, html, humanReadable) {
|
||||
@@ -168,8 +152,9 @@ function datetime(date, html, humanReadable) {
|
||||
return "Never";
|
||||
}
|
||||
|
||||
var format = html === false ? "Y-MM-DD HH:mm:ss z" : "Y-MM-DD [<br class='hidden-lg'>]HH:mm:ss z";
|
||||
var timestr = moment.unix(Math.floor(date)).format(format).trim();
|
||||
const format =
|
||||
html === false ? "Y-MM-DD HH:mm:ss z" : "Y-MM-DD [<br class='hidden-lg'>]HH:mm:ss z";
|
||||
const timestr = moment.unix(Math.floor(date)).format(format).trim();
|
||||
return humanReadable
|
||||
? '<span title="' + timestr + '">' + moment.unix(Math.floor(date)).fromNow() + "</span>"
|
||||
: timestr;
|
||||
@@ -193,7 +178,7 @@ function enableAll() {
|
||||
$("textarea").prop("disabled", false);
|
||||
|
||||
// Enable custom input field only if applicable
|
||||
var ip = $("#select") ? $("#select").val() : null;
|
||||
const ip = $("#select") ? $("#select").val() : null;
|
||||
if (ip !== null && ip !== "custom") {
|
||||
$("#ip-custom").prop("disabled", true);
|
||||
}
|
||||
@@ -201,55 +186,49 @@ function enableAll() {
|
||||
|
||||
// Pi-hole IPv4/CIDR validator by DL6ER, see regexr.com/50csh
|
||||
function validateIPv4CIDR(ip) {
|
||||
// One IPv4 element is 8bit: 0 - 256
|
||||
var ipv4elem = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)";
|
||||
// CIDR for IPv4 is 1 - 32 bit
|
||||
var v4cidr = "(\\/([1-9]|[1-2][0-9]|3[0-2])){0,1}";
|
||||
var ipv4validator = new RegExp(
|
||||
"^" + ipv4elem + "\\." + ipv4elem + "\\." + ipv4elem + "\\." + ipv4elem + v4cidr + "$"
|
||||
// One IPv4 element is 8bit: 0 - 255
|
||||
const ipv4elem = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)";
|
||||
|
||||
// CIDR for IPv4 is 1 - 32 bit (optional)
|
||||
const v4cidr = "(\\/([1-9]|[1-2][0-9]|3[0-2])){0,1}";
|
||||
|
||||
// Build the complete IPv4/CIDR validator
|
||||
// Format: xxx.xxx.xxx.xxx[/yy] where each xxx is 0-255 and optional yy is 1-32
|
||||
const ipv4validator = new RegExp(
|
||||
`^${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}${v4cidr}$`
|
||||
);
|
||||
|
||||
return ipv4validator.test(ip);
|
||||
}
|
||||
|
||||
// Pi-hole IPv6/CIDR validator by DL6ER, see regexr.com/50csn
|
||||
function validateIPv6CIDR(ip) {
|
||||
// One IPv6 element is 16bit: 0000 - FFFF
|
||||
var ipv6elem = "[0-9A-Fa-f]{1,4}";
|
||||
// CIDR for IPv6 is 1- 128 bit
|
||||
var v6cidr = "(\\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}";
|
||||
var ipv6validator = new RegExp(
|
||||
"^(((?:" +
|
||||
ipv6elem +
|
||||
"))*((?::" +
|
||||
ipv6elem +
|
||||
"))*::((?:" +
|
||||
ipv6elem +
|
||||
"))*((?::" +
|
||||
ipv6elem +
|
||||
"))*|((?:" +
|
||||
ipv6elem +
|
||||
"))((?::" +
|
||||
ipv6elem +
|
||||
")){7})" +
|
||||
v6cidr +
|
||||
"$"
|
||||
const ipv6elem = "[0-9A-Fa-f]{1,4}";
|
||||
|
||||
// CIDR for IPv6 is 1-128 bit (optional)
|
||||
const v6cidr = "(\\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}";
|
||||
|
||||
const ipv6validator = new RegExp(
|
||||
`^(((?:${ipv6elem}))*((?::${ipv6elem}))*::((?:${ipv6elem}))*((?::${ipv6elem}))*|((?:${ipv6elem}))((?::${ipv6elem})){7})${v6cidr}$`
|
||||
);
|
||||
|
||||
return ipv6validator.test(ip);
|
||||
}
|
||||
|
||||
function validateMAC(mac) {
|
||||
var macvalidator = /^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$/;
|
||||
const macvalidator = /^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$/;
|
||||
return macvalidator.test(mac);
|
||||
}
|
||||
|
||||
function validateHostname(name) {
|
||||
var namevalidator = /[^<>;"]/;
|
||||
const namevalidator = /[^<>;"]/;
|
||||
return namevalidator.test(name);
|
||||
}
|
||||
|
||||
// set bootstrap-select defaults
|
||||
function setBsSelectDefaults() {
|
||||
var bsSelectDefaults = $.fn.selectpicker.Constructor.DEFAULTS;
|
||||
const bsSelectDefaults = $.fn.selectpicker.Constructor.DEFAULTS;
|
||||
bsSelectDefaults.noneSelectedText = "none selected";
|
||||
bsSelectDefaults.selectedTextFormat = "count > 1";
|
||||
bsSelectDefaults.actionsBox = true;
|
||||
@@ -267,7 +246,7 @@ function setBsSelectDefaults() {
|
||||
};
|
||||
}
|
||||
|
||||
var backupStorage = {};
|
||||
const backupStorage = {};
|
||||
function stateSaveCallback(itemName, data) {
|
||||
if (localStorage === null) {
|
||||
backupStorage[itemName] = JSON.stringify(data);
|
||||
@@ -277,10 +256,10 @@ function stateSaveCallback(itemName, data) {
|
||||
}
|
||||
|
||||
function stateLoadCallback(itemName) {
|
||||
var data;
|
||||
let data;
|
||||
// Receive previous state from client's local storage area
|
||||
if (localStorage === null) {
|
||||
var item = backupStorage[itemName];
|
||||
const item = backupStorage[itemName];
|
||||
data = item === "undefined" ? null : item;
|
||||
} else {
|
||||
data = localStorage.getItem(itemName);
|
||||
@@ -295,9 +274,9 @@ function stateLoadCallback(itemName) {
|
||||
data = JSON.parse(data);
|
||||
|
||||
// Clear possible filtering settings
|
||||
data.columns.forEach(function (value, index) {
|
||||
data.columns[index].search.search = "";
|
||||
});
|
||||
for (const column of Object.values(data.columns)) {
|
||||
column.search.search = "";
|
||||
}
|
||||
|
||||
// Always start on the first page to show most recent queries
|
||||
data.start = 0;
|
||||
@@ -308,41 +287,41 @@ function stateLoadCallback(itemName) {
|
||||
}
|
||||
|
||||
function addFromQueryLog(domain, list) {
|
||||
var alertModal = $("#alertModal");
|
||||
var alProcessing = alertModal.find(".alProcessing");
|
||||
var alSuccess = alertModal.find(".alSuccess");
|
||||
var alFailure = alertModal.find(".alFailure");
|
||||
var alNetworkErr = alertModal.find(".alFailure #alNetErr");
|
||||
var alCustomErr = alertModal.find(".alFailure #alCustomErr");
|
||||
var alList = "#alList";
|
||||
var alDomain = "#alDomain";
|
||||
const alertModal = $("#alertModal");
|
||||
const alProcessing = alertModal.find(".alProcessing");
|
||||
const alSuccess = alertModal.find(".alSuccess");
|
||||
const alFailure = alertModal.find(".alFailure");
|
||||
const alNetworkErr = alertModal.find(".alFailure #alNetErr");
|
||||
const alCustomErr = alertModal.find(".alFailure #alCustomErr");
|
||||
const alList = "#alList";
|
||||
const alDomain = "#alDomain";
|
||||
|
||||
// Exit the function here if the Modal is already shown (multiple running interlock)
|
||||
if (alertModal.css("display") !== "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
var listtype = list === "allow" ? "Allowlist" : "Denylist";
|
||||
const listtype = list === "allow" ? "Allowlist" : "Denylist";
|
||||
|
||||
alProcessing.children(alDomain).text(domain);
|
||||
alProcessing.children(alList).text(listtype);
|
||||
alertModal.modal("show");
|
||||
|
||||
// add Domain to List after Modal has faded in
|
||||
alertModal.one("shown.bs.modal", function () {
|
||||
alertModal.one("shown.bs.modal", () => {
|
||||
$.ajax({
|
||||
url: apiUrl + "/domains/" + list + "/exact",
|
||||
url: document.body.dataset.apiurl + "/domains/" + list + "/exact",
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
processData: false,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
domain: domain,
|
||||
domain,
|
||||
comment: "Added from Query Log",
|
||||
type: list,
|
||||
kind: "exact",
|
||||
}),
|
||||
success: function (response) {
|
||||
success(response) {
|
||||
alProcessing.hide();
|
||||
if ("domains" in response && response.domains.length > 0) {
|
||||
// Success
|
||||
@@ -351,7 +330,7 @@ function addFromQueryLog(domain, list) {
|
||||
alSuccess.fadeIn(1000);
|
||||
// Update domains counter in the menu
|
||||
updateFtlInfo();
|
||||
setTimeout(function () {
|
||||
setTimeout(() => {
|
||||
alertModal.modal("hide");
|
||||
}, 2000);
|
||||
} else {
|
||||
@@ -359,17 +338,17 @@ function addFromQueryLog(domain, list) {
|
||||
alNetworkErr.hide();
|
||||
alCustomErr.html(response.message);
|
||||
alFailure.fadeIn(1000);
|
||||
setTimeout(function () {
|
||||
setTimeout(() => {
|
||||
alertModal.modal("hide");
|
||||
}, 10000);
|
||||
}, 10_000);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
error() {
|
||||
// Network Error
|
||||
alProcessing.hide();
|
||||
alNetworkErr.show();
|
||||
alFailure.fadeIn(1000);
|
||||
setTimeout(function () {
|
||||
setTimeout(() => {
|
||||
alertModal.modal("hide");
|
||||
}, 8000);
|
||||
},
|
||||
@@ -377,7 +356,7 @@ function addFromQueryLog(domain, list) {
|
||||
});
|
||||
|
||||
// Reset Modal after it has faded out
|
||||
alertModal.one("hidden.bs.modal", function () {
|
||||
alertModal.one("hidden.bs.modal", () => {
|
||||
alProcessing.show();
|
||||
alSuccess.add(alFailure).hide();
|
||||
alProcessing.add(alSuccess).children(alDomain).html("").end().children(alList).html("");
|
||||
@@ -407,18 +386,21 @@ function colorBar(percentage, total, cssClass) {
|
||||
}
|
||||
|
||||
function checkMessages() {
|
||||
var ignoreNonfatal = localStorage
|
||||
const ignoreNonfatal = localStorage
|
||||
? localStorage.getItem("hideNonfatalDnsmasqWarnings_chkbox") === "true"
|
||||
: false;
|
||||
$.ajax({
|
||||
url: apiUrl + "/info/messages/count" + (ignoreNonfatal ? "?filter_dnsmasq_warnings=true" : ""),
|
||||
url:
|
||||
document.body.dataset.apiurl +
|
||||
"/info/messages/count" +
|
||||
(ignoreNonfatal ? "?filter_dnsmasq_warnings=true" : ""),
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
})
|
||||
.done(function (data) {
|
||||
.done(data => {
|
||||
if (data.count > 0) {
|
||||
var more = '\nAccess "Tools/Pi-hole diagnosis" for further details.';
|
||||
var title =
|
||||
const more = '\nAccess "Tools/Pi-hole diagnosis" for further details.';
|
||||
const title =
|
||||
data.count > 1
|
||||
? "There are " + data.count + " warnings." + more
|
||||
: "There is one warning." + more;
|
||||
@@ -430,7 +412,7 @@ function checkMessages() {
|
||||
$(".warning-count").addClass("hidden");
|
||||
}
|
||||
})
|
||||
.fail(function (data) {
|
||||
.fail(data => {
|
||||
$(".warning-count").addClass("hidden");
|
||||
apiFailure(data);
|
||||
});
|
||||
@@ -438,9 +420,9 @@ function checkMessages() {
|
||||
|
||||
// Show only the appropriate delete buttons in datatables
|
||||
function changeBulkDeleteStates(table) {
|
||||
var allRows = table.rows({ filter: "applied" }).data().length;
|
||||
var pageLength = table.page.len();
|
||||
var selectedRows = table.rows(".selected").data().length;
|
||||
const allRows = table.rows({ filter: "applied" }).data().length;
|
||||
const pageLength = table.page.len();
|
||||
const selectedRows = table.rows(".selected").data().length;
|
||||
|
||||
if (selectedRows === 0) {
|
||||
// Nothing selected
|
||||
@@ -465,9 +447,9 @@ function changeBulkDeleteStates(table) {
|
||||
|
||||
function doLogout(url) {
|
||||
$.ajax({
|
||||
url: apiUrl + "/auth",
|
||||
url: document.body.dataset.apiurl + "/auth",
|
||||
method: "DELETE",
|
||||
}).always(function () {
|
||||
}).always(() => {
|
||||
globalThis.location = url;
|
||||
});
|
||||
}
|
||||
@@ -498,9 +480,9 @@ function htmlPass(data, _type) {
|
||||
|
||||
// Show only the appropriate buttons
|
||||
function changeTableButtonStates(table) {
|
||||
var allRows = table.rows({ filter: "applied" }).data().length;
|
||||
var pageLength = table.page.len();
|
||||
var selectedRows = table.rows(".selected").data().length;
|
||||
const allRows = table.rows({ filter: "applied" }).data().length;
|
||||
const pageLength = table.page.len();
|
||||
const selectedRows = table.rows(".selected").data().length;
|
||||
|
||||
if (selectedRows === 0) {
|
||||
// Nothing selected
|
||||
@@ -524,8 +506,8 @@ function changeTableButtonStates(table) {
|
||||
}
|
||||
|
||||
function getCSSval(cssclass, cssproperty) {
|
||||
var elem = $("<div class='" + cssclass + "'></div>"),
|
||||
val = elem.appendTo("body").css(cssproperty);
|
||||
const elem = $("<div class='" + cssclass + "'></div>");
|
||||
const val = elem.appendTo("body").css(cssproperty);
|
||||
elem.remove();
|
||||
return val;
|
||||
}
|
||||
@@ -537,11 +519,9 @@ function parseQueryString() {
|
||||
|
||||
// https://stackoverflow.com/q/21647928
|
||||
function hexEncode(string) {
|
||||
var hex, i;
|
||||
|
||||
var result = "";
|
||||
for (i = 0; i < string.length; i++) {
|
||||
hex = string.codePointAt(i).toString(16);
|
||||
let result = "";
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
const hex = string.codePointAt(i).toString(16);
|
||||
result += ("000" + hex).slice(-4);
|
||||
}
|
||||
|
||||
@@ -550,20 +530,22 @@ function hexEncode(string) {
|
||||
|
||||
// https://stackoverflow.com/q/21647928
|
||||
function hexDecode(string) {
|
||||
var j;
|
||||
var hexes = string.match(/.{1,4}/g) || [];
|
||||
var back = "";
|
||||
for (j = 0; j < hexes.length; j++) {
|
||||
back += String.fromCodePoint(parseInt(hexes[j], 16));
|
||||
const hexes = string.match(/.{1,4}/g) || [];
|
||||
let back = "";
|
||||
for (const hex of hexes) {
|
||||
back += String.fromCodePoint(Number.parseInt(hex, 16));
|
||||
}
|
||||
|
||||
return back;
|
||||
}
|
||||
|
||||
function listAlert(type, items, data) {
|
||||
function listsAlert(type, items, data) {
|
||||
// Show simple success message if there is no "processed" object in "data" or
|
||||
// if all items were processed successfully
|
||||
if (data.processed === undefined || data.processed.success.length === items.length) {
|
||||
const successLength = data.processed.success.length;
|
||||
const errorsLength = data.processed.errors.length;
|
||||
|
||||
if (data.processed === undefined || successLength === items.length) {
|
||||
showAlert(
|
||||
"success",
|
||||
"fas fa-plus",
|
||||
@@ -578,54 +560,40 @@ function listAlert(type, items, data) {
|
||||
let message = "";
|
||||
|
||||
// Show a list of successful items if there are any
|
||||
if (data.processed.success.length > 0) {
|
||||
if (successLength > 0) {
|
||||
message +=
|
||||
"Successfully added " +
|
||||
data.processed.success.length +
|
||||
" " +
|
||||
type +
|
||||
(data.processed.success.length !== 1 ? "s" : "") +
|
||||
":";
|
||||
"Successfully added " + successLength + " " + type + (successLength !== 1 ? "s" : "") + ":";
|
||||
|
||||
// Loop over data.processed.success and print "item"
|
||||
for (const item in data.processed.success) {
|
||||
if (Object.prototype.hasOwnProperty.call(data.processed.success, item)) {
|
||||
message += "\n- " + data.processed.success[item].item;
|
||||
}
|
||||
for (const item of Object.values(data.processed.success)) {
|
||||
message += "\n- " + item.item;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a line break if there are both successful and failed items
|
||||
if (data.processed.success.length > 0 && data.processed.errors.length > 0) {
|
||||
if (successLength > 0 && errorsLength > 0) {
|
||||
message += "\n\n";
|
||||
}
|
||||
|
||||
// Show a list of failed items if there are any
|
||||
if (data.processed.errors.length > 0) {
|
||||
if (errorsLength > 0) {
|
||||
message +=
|
||||
"Failed to add " +
|
||||
data.processed.errors.length +
|
||||
" " +
|
||||
type +
|
||||
(data.processed.errors.length !== 1 ? "s" : "") +
|
||||
":\n";
|
||||
"Failed to add " + errorsLength + " " + type + (errorsLength !== 1 ? "s" : "") + ":\n";
|
||||
|
||||
// Loop over data.processed.errors and print "item: error"
|
||||
for (const item in data.processed.errors) {
|
||||
if (Object.prototype.hasOwnProperty.call(data.processed.errors, item)) {
|
||||
let error = data.processed.errors[item].error;
|
||||
// Replace some error messages with a more user-friendly text
|
||||
if (error.indexOf("UNIQUE constraint failed") > -1) {
|
||||
error = "Already present";
|
||||
}
|
||||
|
||||
message += "\n- " + data.processed.errors[item].item + ": " + error;
|
||||
for (const errorItem of Object.values(data.processed.errors)) {
|
||||
let error = errorItem.error;
|
||||
// Replace some error messages with a more user-friendly text
|
||||
if (error.includes("UNIQUE constraint failed")) {
|
||||
error = "Already present";
|
||||
}
|
||||
|
||||
message += `\n- ${errorItem.item}: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Show the warning message
|
||||
const total = data.processed.success.length + data.processed.errors.length;
|
||||
const total = successLength + errorsLength;
|
||||
const processed = "(" + total + " " + type + (total !== 1 ? "s" : "") + " processed)";
|
||||
showAlert(
|
||||
"warning",
|
||||
@@ -640,12 +608,12 @@ let waitMe = null;
|
||||
function loadingOverlayTimeoutCallback(reloadAfterTimeout) {
|
||||
// Try to ping FTL to see if it finished restarting
|
||||
$.ajax({
|
||||
url: apiUrl + "/info/login",
|
||||
url: document.body.dataset.apiurl + "/info/login",
|
||||
method: "GET",
|
||||
cache: false,
|
||||
dataType: "json",
|
||||
})
|
||||
.done(function () {
|
||||
.done(() => {
|
||||
// FTL is running again, hide loading overlay
|
||||
NProgress.done();
|
||||
if (reloadAfterTimeout) {
|
||||
@@ -654,7 +622,7 @@ function loadingOverlayTimeoutCallback(reloadAfterTimeout) {
|
||||
waitMe.hideAll();
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
.fail(() => {
|
||||
// FTL is not running yet, try again in 500ms
|
||||
setTimeout(loadingOverlayTimeoutCallback, 500, reloadAfterTimeout);
|
||||
});
|
||||
@@ -714,40 +682,39 @@ function setInter(func, interval) {
|
||||
|
||||
globalThis.utils = (function () {
|
||||
return {
|
||||
escapeHtml: escapeHtml,
|
||||
unescapeHtml: unescapeHtml,
|
||||
objectToArray: objectToArray,
|
||||
padNumber: padNumber,
|
||||
showAlert: showAlert,
|
||||
datetime: datetime,
|
||||
datetimeRelative: datetimeRelative,
|
||||
disableAll: disableAll,
|
||||
enableAll: enableAll,
|
||||
validateIPv4CIDR: validateIPv4CIDR,
|
||||
validateIPv6CIDR: validateIPv6CIDR,
|
||||
setBsSelectDefaults: setBsSelectDefaults,
|
||||
stateSaveCallback: stateSaveCallback,
|
||||
stateLoadCallback: stateLoadCallback,
|
||||
validateMAC: validateMAC,
|
||||
validateHostname: validateHostname,
|
||||
addFromQueryLog: addFromQueryLog,
|
||||
addTD: addTD,
|
||||
toPercent: toPercent,
|
||||
colorBar: colorBar,
|
||||
checkMessages: checkMessages,
|
||||
changeBulkDeleteStates: changeBulkDeleteStates,
|
||||
doLogout: doLogout,
|
||||
renderTimestamp: renderTimestamp,
|
||||
renderTimespan: renderTimespan,
|
||||
htmlPass: htmlPass,
|
||||
changeTableButtonStates: changeTableButtonStates,
|
||||
getCSSval: getCSSval,
|
||||
parseQueryString: parseQueryString,
|
||||
hexEncode: hexEncode,
|
||||
hexDecode: hexDecode,
|
||||
listsAlert: listAlert,
|
||||
loadingOverlay: loadingOverlay,
|
||||
setTimer: setTimer,
|
||||
setInter: setInter,
|
||||
escapeHtml,
|
||||
unescapeHtml,
|
||||
padNumber,
|
||||
showAlert,
|
||||
datetime,
|
||||
datetimeRelative,
|
||||
disableAll,
|
||||
enableAll,
|
||||
validateIPv4CIDR,
|
||||
validateIPv6CIDR,
|
||||
setBsSelectDefaults,
|
||||
stateSaveCallback,
|
||||
stateLoadCallback,
|
||||
validateMAC,
|
||||
validateHostname,
|
||||
addFromQueryLog,
|
||||
addTD,
|
||||
toPercent,
|
||||
colorBar,
|
||||
checkMessages,
|
||||
changeBulkDeleteStates,
|
||||
doLogout,
|
||||
renderTimestamp,
|
||||
renderTimespan,
|
||||
htmlPass,
|
||||
changeTableButtonStates,
|
||||
getCSSval,
|
||||
parseQueryString,
|
||||
hexEncode,
|
||||
hexDecode,
|
||||
listsAlert,
|
||||
loadingOverlay,
|
||||
setTimer,
|
||||
setInter,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -21,12 +21,12 @@ end
|
||||
<!-- /.content -->
|
||||
</div>
|
||||
<!-- Modal for custom disable time -->
|
||||
<div class="modal fade" id="customDisableModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
<div class="modal fade" id="customDisableModal" tabindex="-1" role="dialog" aria-labelledby="customDisableModalLabel">
|
||||
<div class="modal-dialog modal-sm" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="myModalLabel">Custom disable timeout</h4>
|
||||
<h4 class="modal-title" id="customDisableModalLabel">Custom disable timeout</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group">
|
||||
@@ -52,7 +52,7 @@ end
|
||||
<footer class="main-footer">
|
||||
<div class="row row-centered text-center">
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<strong><a href="https://pi-hole.net/donate/" rel="noopener" target="_blank"><i class="fa fa-heart text-red"></i> Donate</a></strong> if you found this useful.
|
||||
<strong><a href="https://pi-hole.net/donate/" rel="noopener noreferrer" target="_blank"><i class="fa fa-heart text-red"></i> Donate</a></strong> if you found this useful.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ end
|
||||
|
||||
</div>
|
||||
<!-- ./wrapper -->
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/footer.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/footer.js')?>"></script>
|
||||
|
||||
<div id="advanced-info-data" style="display: none;" data-starttime="<?=starttime?>" data-endtime="<?=mg.time(true)?>" data-client-ip="<?=mg.request_info.remote_addr?>" data-xff="<?=x_forwarded_for?>"></div>
|
||||
</body>
|
||||
|
||||
+28
-12
@@ -14,7 +14,8 @@ webhome = pihole.webhome()
|
||||
theme = pihole.webtheme()
|
||||
|
||||
-- Get name of script by matching whatever is after the last "/" in the URI
|
||||
scriptname = mg.request_info.request_uri:match(webhome.."(.*)$")
|
||||
scriptname = mg.script_name:match(".*/(.*).lp$"):gsub("-", "/")
|
||||
|
||||
-- Fall back to "index.lp" if no match is found (e.g. when accessing the root)
|
||||
if scriptname == nil or string.len(scriptname) == 0 then scriptname = "index.lp" end
|
||||
|
||||
@@ -25,6 +26,10 @@ end
|
||||
|
||||
-- Function returning GET parameter value (or nil if not set)
|
||||
function GET(name)
|
||||
if not mg.request_info.query_string then
|
||||
return nil
|
||||
end
|
||||
|
||||
return mg.request_info.query_string:match(name.."=([^&]*)")
|
||||
-- mg.get_var(mg.request_info.query_string, "REQUEST_URI")
|
||||
end
|
||||
@@ -40,6 +45,20 @@ function in_array (val, tab)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Function to sanitize hostname containing invalid HTML characters
|
||||
function sanitize_hostname(str)
|
||||
-- Check if string contains any of the HTML special characters
|
||||
if str:find("&<>\"'") then
|
||||
return "invalid hostname"
|
||||
end
|
||||
|
||||
-- Return the original string if no special characters are found
|
||||
return str
|
||||
end
|
||||
|
||||
-- Sanitize hostname
|
||||
hostname = sanitize_hostname(hostname)
|
||||
|
||||
-- Variable to check if user is already authenticated
|
||||
is_authenticated = mg.request_info.is_authenticated
|
||||
|
||||
@@ -47,12 +66,8 @@ is_authenticated = mg.request_info.is_authenticated
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!-- Usually browsers proactively perform domain name resolution on links that the user may choose to follow. We disable DNS prefetching here -->
|
||||
<meta http-equiv="x-dns-prefetch-control" content="off">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Pi-hole <?=hostname?></title>
|
||||
|
||||
<meta name="csrf-token" content="<?=mg.request_info.csrf_token?>">
|
||||
|
||||
<link rel="apple-touch-icon" href="<?=webhome?>img/favicons/apple-touch-icon.png" sizes="180x180">
|
||||
@@ -81,6 +96,7 @@ is_authenticated = mg.request_info.is_authenticated
|
||||
|
||||
<!-- Common styles -->
|
||||
<link rel="stylesheet" href="<?=pihole.fileversion('vendor/bootstrap/css/bootstrap.min.css')?>">
|
||||
<link rel="stylesheet" href="<?=pihole.fileversion('vendor/icheck/icheck-bootstrap.min.css')?>">
|
||||
<link rel="stylesheet" href="<?=pihole.fileversion('vendor/animate/animate.min.css')?>">
|
||||
<link rel="stylesheet" href="<?=pihole.fileversion('vendor/bstreeview/bstreeview.min.css')?>">
|
||||
<link rel="stylesheet" href="<?=pihole.fileversion('vendor/font-awesome/css/all.min.css')?>">
|
||||
@@ -114,10 +130,10 @@ is_authenticated = mg.request_info.is_authenticated
|
||||
<noscript><link rel="stylesheet" href="<?=pihole.fileversion('vendor/js-warn/js-warn.css')?>"></noscript>
|
||||
|
||||
<!-- scripts -->
|
||||
<script defer src="<?=pihole.fileversion('vendor/jquery/jquery.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap/js/bootstrap.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/adminLTE/adminlte.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-notify/bootstrap-notify.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/waitMe-js/modernized-waitme-min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/nprogress/nprogress.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/utils.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/jquery/jquery.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap/js/bootstrap.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/adminLTE/adminlte.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-notify/bootstrap-notify.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/waitMe-js/modernized-waitme-min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/nprogress/nprogress.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/utils.js')?>"></script>
|
||||
|
||||
@@ -9,26 +9,26 @@
|
||||
]]--
|
||||
mg.include('header.lp','r')
|
||||
?>
|
||||
<script defer src="<?=pihole.fileversion('vendor/select2/select2.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/datatables/datatables.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/datatables-select/datatables.select.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/datatables-buttons/datatables.buttons.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/chartjs/chart.umd.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/chartjs-plugin-deferred/chartjs-plugin-deferred.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/moment/moment.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/chartjs-adapter-moment/chartjs-adapter-moment.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/hammer/hammer.min.js')?>"></script> <!-- Needed for chartjs-plugin-zoom -->
|
||||
<script defer src="<?=pihole.fileversion('vendor/chartjs-plugin-zoom/chartjs-plugin-zoom.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bstreeview/bstreeview.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/logout.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/select2/select2.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/datatables/datatables.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/datatables-select/datatables.select.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/datatables-buttons/datatables.buttons.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/chartjs/chart.umd.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/chartjs-plugin-deferred/chartjs-plugin-deferred.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/moment/moment.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/chartjs-adapter-moment/chartjs-adapter-moment.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/hammer/hammer.min.js')?>"></script> <!-- Needed for chartjs-plugin-zoom -->
|
||||
<script src="<?=pihole.fileversion('vendor/chartjs-plugin-zoom/chartjs-plugin-zoom.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bstreeview/bstreeview.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/logout.js')?>"></script>
|
||||
</head>
|
||||
<body class="<?=theme.name?> hold-transition sidebar-mini <? if pihole.boxedlayout() then ?>layout-boxed<? end ?> logged-in" data-apiurl="<?=pihole.api_url()?>" data-logout-url="<?=webhome .. 'login'?>">
|
||||
<body class="<?=theme.name?> hold-transition sidebar-mini <? if pihole.boxedlayout() then ?>layout-boxed<? end ?> logged-in page-<?=pihole.format_path(mg.request_info.request_uri)?>" data-apiurl="<?=pihole.api_url()?>" data-webhome="<?=webhome?>">
|
||||
<noscript>
|
||||
<!-- JS Warning -->
|
||||
<div>
|
||||
<input type="checkbox" id="js-hide">
|
||||
<div class="js-warn" id="js-warn-exit"><h1>JavaScript Is Disabled</h1><p>JavaScript is required for the site to function.</p>
|
||||
<p>To learn how to enable JavaScript click <a href="https://www.enable-javascript.com/" rel="noopener" target="_blank">here</a></p><label for="js-hide">Close</label>
|
||||
<p>To learn how to enable JavaScript click <a href="https://www.enable-javascript.com/" rel="noopener noreferrer" target="_blank">here</a></p><label for="js-hide">Close</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /JS Warning -->
|
||||
@@ -76,15 +76,14 @@ mg.include('header.lp','r')
|
||||
<li class="user-body" id="advanced-info" style="display:none;"></li>
|
||||
<!-- Menu Footer -->
|
||||
<li class="user-footer">
|
||||
<a class="btn-link" href="https://pi-hole.net/" rel="noopener" target="_blank">
|
||||
<a class="btn-link" href="https://pi-hole.net/" rel="noopener noreferrer" target="_blank">
|
||||
<? mg.include('../../img/logo-bw.svg', 'r') ?>
|
||||
Pi-hole Website
|
||||
</a>
|
||||
<hr>
|
||||
<a class="btn-link" href="https://docs.pi-hole.net/" rel="noopener" target="_blank"><i class="fa-fw menu-icon fa-regular fa-circle-question"></i> Documentation</a>
|
||||
<a class="btn-link" href="https://discourse.pi-hole.net/" rel="noopener" target="_blank"><i class="fa-fw menu-icon fab fa-discourse"></i> Pi-hole Discourse</a>
|
||||
<a class="btn-link" href="https://github.com/pi-hole" rel="noopener" target="_blank"><i class="fa-fw menu-icon fab fa-github"></i> GitHub</a>
|
||||
<a class="btn-link" href="https://discourse.pi-hole.net/c/announcements/5" rel="noopener" target="_blank"><i class="fa-fw menu-icon fa-solid fa-rocket"></i> Pi-hole Releases</a>
|
||||
<a class="btn-link" href="https://docs.pi-hole.net/" rel="noopener noreferrer" target="_blank"><i class="fa-fw menu-icon fa-solid fa-circle-question"></i> Documentation</a>
|
||||
<a class="btn-link" href="https://discourse.pi-hole.net/" rel="noopener noreferrer" target="_blank"><i class="fa-fw menu-icon fab fa-discourse"></i> Pi-hole Discourse</a>
|
||||
<a class="btn-link" href="https://github.com/pi-hole" rel="noopener noreferrer" target="_blank"><i class="fa-fw menu-icon fab fa-github"></i> GitHub</a>
|
||||
<a class="btn-link" href="https://discourse.pi-hole.net/c/announcements/5" rel="noopener noreferrer" target="_blank"><i class="fa-fw menu-icon fa-solid fa-rocket"></i> Pi-hole Releases</a>
|
||||
<? if pihole.needLogin() then ?>
|
||||
<a class="btn-link" href="#" id="logout-button"><i class="fa-fw menu-icon fa-solid fa-arrow-right-from-bracket"></i> Log out</a>
|
||||
<? end ?>
|
||||
|
||||
+21
-22
@@ -13,7 +13,7 @@
|
||||
<!-- Sidebar user panel -->
|
||||
<div class="user-panel">
|
||||
<div class="pull-left image">
|
||||
<img class="logo-img" src="<?=webhome?>img/logo.svg" alt="Pi-hole logo" width="52" height="75">
|
||||
<img class="logo-img" src="<?=webhome?>img/logo.svg" alt="Pi-hole logo" width="60" height="87">
|
||||
</div>
|
||||
<div class="pull-left info">
|
||||
<p>Status</p>
|
||||
@@ -85,7 +85,6 @@
|
||||
</li>
|
||||
|
||||
<li class="header text-uppercase">DNS Control</li>
|
||||
<!-- Local DNS Records -->
|
||||
<!-- Enable/Disable Blocking -->
|
||||
<li id="pihole-disable" class="menu-dns treeview">
|
||||
<a href="<?=webhome?>#">
|
||||
@@ -133,7 +132,7 @@
|
||||
</li>
|
||||
<li class="header text-uppercase">System</li>
|
||||
<!-- Settings -->
|
||||
<li class="menu-system treeview <? if startsWith(scriptname, 'settings/') then ?> active<? end ?>">
|
||||
<li class="menu-system treeview<? if startsWith(scriptname, 'settings/') then ?> active<? end ?>">
|
||||
<a href="<?=webhome?>#">
|
||||
<i class="fa fa-fw menu-icon fa-cogs"></i> <span>Settings</span>
|
||||
<span class="pull-right-container">
|
||||
@@ -141,42 +140,42 @@
|
||||
</span>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
<li class="<? if scriptname == 'settings/system' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'settings/system' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>settings/system">
|
||||
<i class="fa-fw menu-icon fa-solid fa-circle-info"></i> <span>System</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<? if scriptname == 'settings/dns' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'settings/dns' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>settings/dns">
|
||||
<i class="fa-fw menu-icon fa-solid fa-book-atlas"></i> <span>DNS</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<? if scriptname == 'settings/dhcp' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'settings/dhcp' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>settings/dhcp">
|
||||
<i class="fa-fw menu-icon fa-solid fa-sitemap"></i> <span>DHCP</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<? if scriptname == 'settings/api' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'settings/api' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>settings/api">
|
||||
<i class="fa-fw menu-icon fa-solid fa-window-restore"></i> <span>Web interface / API</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<? if scriptname == 'settings/privacy' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'settings/privacy' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>settings/privacy">
|
||||
<i class="fa-fw menu-icon fa-solid fa-binoculars"></i> <span>Privacy</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<? if scriptname == 'settings/teleporter' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'settings/teleporter' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>settings/teleporter">
|
||||
<i class="fa-fw menu-icon fa-solid fa-file-export"></i> <span>Teleporter</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<? if scriptname == 'settings/dnsrecords' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'settings/dnsrecords' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>settings/dnsrecords">
|
||||
<i class="fa-fw menu-icon fa-solid fa-address-book"></i> <span>Local DNS Records</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<? if scriptname == 'settings/all' then ?> active<? end ?> settings-level-expert">
|
||||
<li class="settings-level-expert d-none<? if scriptname == 'settings/all' then ?> active<? end ?>">
|
||||
<a href="<?=webhome?>settings/all">
|
||||
<i class="fa-fw menu-icon fa-solid fa-pen-to-square"></i> <span>All settings</span>
|
||||
</a>
|
||||
@@ -194,14 +193,14 @@
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
<!-- Pi-hole diagnosis -->
|
||||
<li class="<? if scriptname == 'messages' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'messages' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>messages">
|
||||
<i class="fa fa-fw menu-icon fa-file-medical-alt"></i> <span>Pi-hole diagnosis</span>
|
||||
<span class="pull-right-container warning-count hidden"></span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Tail log files -->
|
||||
<li class="treeview <? if scriptname == 'taillog' then ?> active<? end ?>">
|
||||
<li class="treeview<? if scriptname == 'taillog' then ?> active<? end ?>">
|
||||
<a href="<?=webhome?>#">
|
||||
<i class="fa-fw menu-icon fa-solid fa-list-ul"></i> <span>Tail log files</span>
|
||||
<span class="pull-right-container">
|
||||
@@ -210,45 +209,45 @@
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
<!-- Tail pihole.log -->
|
||||
<li class="<? if scriptname == 'taillog' and GET("file") == "dnsmasq" then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'taillog' and GET("file") == "dnsmasq" then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>taillog?file=dnsmasq">
|
||||
<i class="fa-fw menu-icon fa-solid fa-list-ul"></i> pihole.log
|
||||
</a>
|
||||
</li>
|
||||
<!-- Tail FTL.log -->
|
||||
<li class="<? if scriptname == 'taillog' and GET("file") == "ftl" then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'taillog' and GET("file") == "ftl" then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>taillog?file=ftl">
|
||||
<i class="fa-fw menu-icon fa-solid fa-list-ul"></i> FTL.log
|
||||
</a>
|
||||
</li>
|
||||
<!-- Tail webserver.log -->
|
||||
<li class="<? if scriptname == 'taillog' and GET("file") == "webserver" then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'taillog' and GET("file") == "webserver" then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>taillog?file=webserver">
|
||||
<i class="fa-fw menu-icon fa-solid fa-list-ul"></i> webserver.log
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<!-- Run gravity.sh -->
|
||||
<li class="<? if scriptname == 'gravity' then ?> active<? end ?>">
|
||||
<!-- Update Gravity -->
|
||||
<li<? if scriptname == 'gravity' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>gravity">
|
||||
<i class="fa fa-fw menu-icon fa-arrow-circle-down"></i> <span>Update Gravity</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Query Lists -->
|
||||
<li class="<? if scriptname == 'search' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'search' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>search">
|
||||
<i class="fa fa-fw menu-icon fa-search"></i> <span>Search Lists</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Interfaces -->
|
||||
<li class="<? if scriptname == 'interfaces' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'interfaces' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>interfaces">
|
||||
<i class="fa fa-fw menu-icon fa-wifi"></i> <span>Interfaces</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Network -->
|
||||
<li class="<? if scriptname == 'network' then ?> active<? end ?>">
|
||||
<li<? if scriptname == 'network' then ?> class="active"<? end ?>>
|
||||
<a href="<?=webhome?>network">
|
||||
<i class="fa fa-fw menu-icon fa-network-wired"></i> <span>Network</span>
|
||||
</a>
|
||||
@@ -259,7 +258,7 @@
|
||||
<!-- Donate button -->
|
||||
<li class="header text-uppercase">Donate</li>
|
||||
<li class="menu-donate">
|
||||
<a href="https://pi-hole.net/donate/" rel="noopener" target="_blank">
|
||||
<a href="https://pi-hole.net/donate/" rel="noopener noreferrer" target="_blank">
|
||||
<i class="fas fa-fw menu-icon fa-donate"></i> <span>Donate</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -19,7 +19,6 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
<div class="box-header">
|
||||
<div id="domain-search-block" class="input-group">
|
||||
<input id="domain" type="url" class="form-control" placeholder="Domain to look for (example.com or sub.example.com)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
<input id="quiet" type="hidden" value="no">
|
||||
<span class="input-group-btn">
|
||||
<button type="button" id="btnSearch" class="btn btn-default">Search</button>
|
||||
</span>
|
||||
@@ -48,5 +47,5 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
|
||||
<pre id="output" style="width: 100%; height: 100%;" hidden></pre>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/search.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/search.js')?>"></script>
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+5
-5
@@ -13,7 +13,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
PageTitle = "All Settings"
|
||||
mg.include('scripts/lua/settings_header.lp','r')
|
||||
?>
|
||||
<div class="row settings-level-expert" id="advanced-content">
|
||||
<div class="row settings-level-expert d-none" id="advanced-content">
|
||||
<div class="overlay" id="advanced-overlay">
|
||||
<i class="fa fa-sync fa-spin"></i>
|
||||
</div>
|
||||
@@ -28,12 +28,12 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<!-- dynamically filled with content -->
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 settings-level-expert save-button-container">
|
||||
<div class="col-sm-12 settings-level-expert d-none save-button-container">
|
||||
<button type="button" class="btn btn-primary save-button"><i class="fa-solid fa-fw fa-floppy-disk"></i> Save & Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings-advanced.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings-advanced.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+12
-12
@@ -14,7 +14,7 @@ PageTitle = "Web Interface - API Settings"
|
||||
mg.include('scripts/lua/settings_header.lp','r')
|
||||
?>
|
||||
<div class="row">
|
||||
<div class="col-md-6 settings-level-basic">
|
||||
<div class="col-md-6">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="webserver.interface.theme webserver.interface.boxed">Theme settings</h3>
|
||||
@@ -49,7 +49,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 settings-level-expert">
|
||||
<div class="col-md-6 settings-level-expert d-none">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="webserver.api.prettyJSON webserver.api.allow_destructive">Advanced Settings</h3>
|
||||
@@ -82,7 +82,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 settings-level-expert">
|
||||
<div class="col-md-12 settings-level-expert d-none">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="webserver.api.excludeDomains webserver.api.excludeClients">Exclusions</h3>
|
||||
@@ -102,7 +102,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p><strong>Important</strong>:<br>Those input fields accept regex entries only.<br>Please refer to <a href="https://docs.pi-hole.net/regex/tutorial/" rel="noopener" target="_blank">our guide</a> how to construct valid regex entries.
|
||||
<p><strong>Important</strong>:<br>Those input fields accept regex entries only.<br>Please refer to <a href="https://docs.pi-hole.net/regex/tutorial/" rel="noopener noreferrer" target="_blank">our guide</a> how to construct valid regex entries.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,7 +134,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<div class="col-md-12 settings-level-expert">
|
||||
<div class="col-md-12 settings-level-expert d-none">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Currently active sessions</h3>
|
||||
@@ -166,7 +166,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 settings-level-basic save-button-container">
|
||||
<div class="col-lg-12 save-button-container">
|
||||
<button type="button" class="btn btn-primary save-button"><i class="fa-solid fa-fw fa-floppy-disk"></i> Save & Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,7 +204,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default pull-left" data-dismiss="modal">Close</button>
|
||||
<button type="button" id="totp_submit" disabled="true" class="btn btn-default">Enable 2FA</button>
|
||||
<button type="button" id="totp_submit" class="btn btn-default" disabled>Enable 2FA</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,10 +246,10 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/jquery-confirm/jquery.confirm.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/qrious/qrious.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings-api.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/jquery-confirm/jquery.confirm.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/qrious/qrious.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings-api.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+10
-10
@@ -15,7 +15,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
?>
|
||||
<div class="row">
|
||||
<!-- DHCP Settings Box -->
|
||||
<div class="col-md-6 settings-level-basic">
|
||||
<div class="col-md-6">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="dhcp.active dhcp.start dhcp.end dhcp.router dhcp.ipv6">DHCP Settings</h3>
|
||||
@@ -25,7 +25,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<div class="col-md-12">
|
||||
<div>
|
||||
<input type="checkbox" id="dhcp.active" data-key="dhcp.active"><label for="dhcp.active"><strong>DHCP server enabled</strong></label>
|
||||
<p class="help-block" id="dhcpnotice" lookatme-text="Make sure your router's DHCP server is disabled when using the Pi-hole DHCP server!">Make sure your router's DHCP server is disabled when using the Pi-hole DHCP server!</p>
|
||||
<p class="help-block" id="dhcpnotice">Make sure your router's DHCP server is disabled when using the Pi-hole DHCP server!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
@@ -86,7 +86,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 settings-level-expert">
|
||||
<div class="col-md-6 settings-level-expert d-none">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="dhcp.leaseTime dhcp.rapidCommit dhcp.multiDNS">Advanced DHCP Settings</h3>
|
||||
@@ -165,7 +165,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 settings-level-expert">
|
||||
<div class="col-md-12 settings-level-expert d-none">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="dhcp.hosts">Static DHCP configuration</h3>
|
||||
@@ -198,7 +198,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="box-body" style="display: none;">
|
||||
<ul>
|
||||
<li> Addresses allocated like this are not constrained to be in the DHCP range specified above but they must be in the same subnet. For subnets which don't need a pool of dynamically allocated addresses, you can set a one-address range above and specify only static leases here.</li>
|
||||
<li> It is allowed to use client identifiers (called client DUID in IPv6-land) rather than hardware addresses to identify hosts by prefixing with <code>id:</code>. Thus lines like <code>id:01:02:03:04,.....</code> refer to the host with client identifier <code>01:02:03:04</code>. It is also allowed to specify the client ID as text, like this: <code>id:clientidastext,.....</code></li>
|
||||
@@ -221,13 +221,13 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 settings-level-basic save-button-container">
|
||||
<div class="col-lg-12 save-button-container">
|
||||
<button type="button" class="btn btn-primary save-button"><i class="fa-solid fa-fw fa-floppy-disk"></i> Save & Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings-dhcp.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/ip-address-sorting.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings-dhcp.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/ip-address-sorting.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+15
-15
@@ -22,7 +22,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">IPv4</th>
|
||||
@@ -45,7 +45,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="box-body" style="display: none;">
|
||||
<p>The following list contains all DNS servers selected above. Furthermore, you can add your own custom DNS servers here. The expected format is one server per line in form of <code>IP#port</code>, where the <code>port</code> is optional. If given, it has to be separated by a hash <code>#</code> from the address (e.g. <code>127.0.0.1#5335</code> for a local <code>unbound</code> instance running on port <code>5335</code>). The port defaults to 53 if omitted.</p>
|
||||
<textarea class="form-control" rows="3" id="DNSupstreamsTextfield" data-key="dns.upstreams" placeholder="Enter upstream DNS servers, one per line" style="resize: vertical;"></textarea>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-warning settings-level-expert">
|
||||
<div class="box box-warning settings-level-expert d-none">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="dns.domain dns.expandHosts">DNS domain settings</h3>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-warning settings-level-expert">
|
||||
<div class="box box-warning settings-level-expert d-none">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="dns.rateLimit.count dns.rateLimit.interval">Rate-limiting</h3>
|
||||
</div>
|
||||
@@ -99,14 +99,14 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
rate-limited clients are short-circuited at the same time.</p>
|
||||
<p>Rate-limiting may be disabled altogether by setting both
|
||||
values to zero. See
|
||||
<a href="https://docs.pi-hole.net/ftldns/configfile/#rate_limit" rel="noopener" target="_blank">our documentation</a>
|
||||
<a href="https://docs.pi-hole.net/ftldns/configfile/#rate_limit" rel="noopener noreferrer" target="_blank">our documentation</a>
|
||||
for further details.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 settings-level-expert">
|
||||
<div class="col-lg-6 settings-level-expert d-none">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h1 class="box-title" data-configkeys="dns.listeningMode">Interface settings</h1>
|
||||
@@ -144,7 +144,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
in your router!) they are safe to use.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>See <a href="https://docs.pi-hole.net/ftldns/interfaces/" rel="noopener" target="_blank">our documentation</a> for further technical details.</p>
|
||||
<p>See <a href="https://docs.pi-hole.net/ftldns/interfaces/" rel="noopener noreferrer" target="_blank">our documentation</a> for further technical details.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -173,7 +173,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<p class="help-block">All reverse lookups for private IP ranges (i.e., <code>192.168.0.x/24</code>, etc.)
|
||||
which are not found in <code>/etc/hosts</code> or the DHCP leases are answered
|
||||
with "no such domain" rather than being forwarded upstream. The set
|
||||
of prefixes affected is the list given in <a href="https://tools.ietf.org/html/rfc6303">RFC6303</a>.</p>
|
||||
of prefixes affected is the list given in <a href="https://tools.ietf.org/html/rfc6303" rel="noopener noreferrer" target="_blank">RFC6303</a>.</p>
|
||||
<p><strong>Important:</strong><br>Enabling these two options may increase your privacy,
|
||||
but may also prevent you from being able to access local hostnames if the Pi-hole is not used as DHCP server.
|
||||
Make sure you have set up conditional forwarding in this case.</p>
|
||||
@@ -189,7 +189,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
Use an upstream DNS server which supports DNSSEC when activating DNSSEC. Note that
|
||||
the size of your log might increase significantly
|
||||
when enabling DNSSEC. A DNSSEC resolver test can be found
|
||||
<a href="https://dnssec.vs.uni-due.de/" rel="noopener" target="_blank">here</a>.</p>
|
||||
<a href="https://dnssec.vs.uni-due.de/" rel="noopener noreferrer" target="_blank">here</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,7 +197,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
<div class="box box-warning settings-level-expert">
|
||||
<div class="box box-warning settings-level-expert d-none">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="dns.revServers">Conditional forwarding</h3>
|
||||
</div>
|
||||
@@ -219,7 +219,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
whereas an even wider network of 10.0.0.1 - 10.255.255.255 results in <code>10.0.0.0/8</code>.
|
||||
Setting up IPv6 ranges is exactly similar to setting up IPv4 here and fully supported.
|
||||
Feel free to reach out to us on our
|
||||
<a href="https://discourse.pi-hole.net" rel="noopener" target="_blank">Discourse forum</a>
|
||||
<a href="https://discourse.pi-hole.net" rel="noopener noreferrer" target="_blank">Discourse forum</a>
|
||||
in case you need any assistance setting up local host name resolution for your particular system.</p>
|
||||
<p>You can also specify a local domain name (like <code>fritz.box</code>) to ensure queries to
|
||||
devices ending in your local domain name will not leave your network, however, this is optional.
|
||||
@@ -234,12 +234,12 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 settings-level-basic save-button-container">
|
||||
<div class="col-lg-12 save-button-container">
|
||||
<button type="button" class="btn btn-primary save-button"><i class="fa-solid fa-fw fa-floppy-disk"></i> Save & Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings-dns.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings-dns.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+9
-11
@@ -41,12 +41,11 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<input id="Hip" type="text" class="form-control" data-configkeys="hosts" placeholder="Associated IP" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</th>
|
||||
<th>
|
||||
<button type="button" id="btnAdd-host" class="btn btn-primary btn-xs" data-configkeys="hosts"><i class="fa fa-plus"></i></button>
|
||||
<button type="button" id="btnAdd-host" class="btn btn-success btn-xs" data-configkeys="hosts"><i class="fa fa-plus"></i></button>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<button type="button" id="resetButton" class="btn btn-default btn-sm text-red hidden">Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,7 +58,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<div class="col-md-12 col-lg-6">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" id="title-cnameRecords" data-configkeys="dns.cnameRecords">Local CNAME records records</h3>
|
||||
<h3 class="box-title" id="title-cnameRecords" data-configkeys="dns.cnameRecords">Local CNAME records</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -87,12 +86,11 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<input id="Cttl" type="numeric" class="form-control" data-configkeys="cnameRecords" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
|
||||
</th>
|
||||
<th>
|
||||
<button type="button" id="btnAdd-cname" class="btn btn-primary btn-xs" data-configkeys="cnameRecords"><i class="fa fa-plus"></i></button>
|
||||
<button type="button" id="btnAdd-cname" class="btn btn-success btn-xs" data-configkeys="cnameRecords"><i class="fa fa-plus"></i></button>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<button type="button" id="resetButton" class="btn btn-default btn-sm text-red hidden">Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,17 +98,17 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<strong>Note:</strong>
|
||||
<p>The target of a <code>CNAME</code> must be a domain that the Pi-hole already has in its cache or is authoritative for. This is a universal limitation of <code>CNAME</code> records.</p>
|
||||
<p>The reason for this is that Pi-hole will not send additional queries upstream when serving <code>CNAME</code> replies. As consequence, if you set a target that isn't already known, the reply to the client may be incomplete. Pi-hole just returns the information it knows at the time of the query. This results in certain limitations for <code>CNAME</code> targets,
|
||||
for instance, only <em>active</em> DHCP leases work as targets - mere DHCP <i>leases</i> aren't sufficient as they aren't (yet) valid DNS records.</p>
|
||||
for instance, only <em>active</em> DHCP leases work as targets - mere DHCP <em>leases</em> aren't sufficient as they aren't (yet) valid DNS records.</p>
|
||||
<p>Additionally, you can't <code>CNAME</code> external domains (<code>bing.com</code> to <code>google.com</code>) successfully as this could result in invalid SSL certificate errors when the target server does not serve content for the requested domain.</p>
|
||||
<p>Adding/removing local CNAME records will restart the DNS server.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/jquery-confirm/jquery.confirm.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings-dns-records.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/ip-address-sorting.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/jquery-confirm/jquery.confirm.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings-dns-records.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/ip-address-sorting.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+7
-7
@@ -14,7 +14,7 @@ PageTitle = "Privacy Settings"
|
||||
mg.include('scripts/lua/settings_header.lp','r')
|
||||
?>
|
||||
<div class="row">
|
||||
<div class="col-md-6 settings-level-basic">
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="box box-warning">
|
||||
@@ -34,7 +34,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 settings-level-expert">
|
||||
<div class="col-md-12 settings-level-expert d-none">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="database.DBimport database.maxDBdays database.network.expire">Privacy-related database settings</h3>
|
||||
@@ -64,7 +64,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 settings-level-expert">
|
||||
<div class="col-md-6 settings-level-expert d-none">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" data-configkeys="misc.privacylevel">Query Anonymization ("Privacy Level")</h3>
|
||||
@@ -99,13 +99,13 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 settings-level-basic save-button-container">
|
||||
<div class="col-lg-12 save-button-container">
|
||||
<button type="button" class="btn btn-primary save-button"><i class="fa-solid fa-fw fa-floppy-disk"></i> Save & Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings-privacy.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings-privacy.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+42
-90
@@ -34,7 +34,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">RAM:</th>
|
||||
<td><span id="sysinfo-memory-ram"></span> <span id="sysinfo-ram-ftl"></span</td>
|
||||
<td><span id="sysinfo-memory-ram"></span> <span id="sysinfo-ram-ftl"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Swap:</th>
|
||||
@@ -74,7 +74,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<i class="fa fa-sync fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box settings-level-expert">
|
||||
<div class="box settings-level-expert d-none">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">DHCP server metrics</h3>
|
||||
</div>
|
||||
@@ -84,83 +84,57 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<table class="table table-striped table-bordered nowrap">
|
||||
<tbody>
|
||||
<tr title="Client broadcast to locate available servers">
|
||||
<th scope="row">
|
||||
<span>DHCPDISCOVER:</span>
|
||||
</th>
|
||||
<th scope="row">DHCPDISCOVER:</th>
|
||||
<td id="sysinfo-dhcp-discover"> </td>
|
||||
</tr>
|
||||
<tr title="Server to client in response to DHCPDISCOVER with offer of configuration parameters">
|
||||
<th scope="row">
|
||||
<span>DHCPOFFER:</span>
|
||||
</th>
|
||||
<th scope="row">DHCPOFFER:</th>
|
||||
<td id="sysinfo-dhcp-offer"> </td>
|
||||
</tr>
|
||||
<tr title="Client message to servers either 
 (a) requesting offered parameters from one server and implicitly declining offers from all others, 
 (b) confirming correctness of previously allocated address after, e.g., system reboot, or 
 (c) extending the lease on a particular network address">
|
||||
<th scope="row">
|
||||
<span>DHCPREQUEST:</span>
|
||||
</th>
|
||||
<th scope="row">DHCPREQUEST:</th>
|
||||
<td id="sysinfo-dhcp-request"> </td>
|
||||
</tr>
|
||||
<tr title="Server to client with configuration parameters, including committed network address">
|
||||
<th scope="row">
|
||||
<span>DHCPACK:</span>
|
||||
</th>
|
||||
<th scope="row">DHCPACK:</th>
|
||||
<td id="sysinfo-dhcp-ack"> </td>
|
||||
</tr>
|
||||
<tr title="Server to client indicating client's notion of network address is incorrect (e.g., client has moved to new subnet) or client's lease as expired">
|
||||
<th scope="row">
|
||||
<span>DHCPNAK:</span>
|
||||
</th>
|
||||
<th scope="row">DHCPNAK:</th>
|
||||
<td id="sysinfo-dhcp-nak"> </td>
|
||||
</tr>
|
||||
<tr title="Client to server indicating network address is already in use">
|
||||
<th scope="row">
|
||||
<span>DHCPDECLINE:</span>
|
||||
</th>
|
||||
<th scope="row">DHCPDECLINE:</th>
|
||||
<td id="sysinfo-dhcp-decline"> </td>
|
||||
</tr>
|
||||
<tr title="Client to server, asking only for local configuration parameters; client already has externally configured network address">
|
||||
<th scope="row">
|
||||
<span>DHCPINFORM:</span>
|
||||
</th>
|
||||
<th scope="row">DHCPINFORM:</th>
|
||||
<td id="sysinfo-dhcp-inform"> </td>
|
||||
</tr>
|
||||
<tr title="Client to server relinquishing network address and cancelling remaining lease">
|
||||
<th scope="row">
|
||||
<span>DHCPRELEASE:</span>
|
||||
</th>
|
||||
<th scope="row">DHCPRELEASE:</th>
|
||||
<td id="sysinfo-dhcp-release"> </td>
|
||||
</tr>
|
||||
<tr title="Could not provide an answer, e.g., because there are no leases left, the client wants to renew a lease that is outside of our range, or the explicitly requested address is already in use">
|
||||
<th scope="row">
|
||||
<span>DHCPNOANSWER:</span>
|
||||
</th>
|
||||
<th scope="row">DHCPNOANSWER:</th>
|
||||
<td id="sysinfo-dhcp-noanswer"> </td>
|
||||
</tr>
|
||||
<tr title="Processed BOOTP packets">
|
||||
<th scope="row">
|
||||
<span>BOOTP:</span>
|
||||
</th>
|
||||
<th scope="row">BOOTP:</th>
|
||||
<td id="sysinfo-dhcp-bootp"> </td>
|
||||
</tr>
|
||||
<tr title="Processed PXE packets">
|
||||
<th scope="row">
|
||||
<span>PXE:</span>
|
||||
</th>
|
||||
<th scope="row">PXE:</th>
|
||||
<td id="sysinfo-dhcp-pxe"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<span>Allocated / pruned IPv4 leases:</span>
|
||||
</th>
|
||||
<th scope="row">Allocated / pruned IPv4 leases:</th>
|
||||
<td><span id="sysinfo-dhcp-leases-allocated_4"> </span> /
|
||||
<span id="sysinfo-dhcp-leases-pruned_4"> </span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<span>Allocated / pruned IPv6 leases:</span>
|
||||
</th>
|
||||
<th scope="row">Allocated / pruned IPv6 leases:</th>
|
||||
<td><span id="sysinfo-dhcp-leases-allocated_6"> </span> /
|
||||
<span id="sysinfo-dhcp-leases-pruned_6"> </span>
|
||||
</td>
|
||||
@@ -176,7 +150,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="box settings-level-expert" id="cache-metrics">
|
||||
<div class="box settings-level-expert d-none" id="cache-metrics">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">DNS cache metrics</h3>
|
||||
</div>
|
||||
@@ -186,50 +160,38 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<table class="table table-striped table-bordered nowrap">
|
||||
<tbody>
|
||||
<tr title="Total size of the DNS cache">
|
||||
<th scope="row">
|
||||
<span>DNS cache size:</span>
|
||||
</th>
|
||||
<th scope="row">DNS cache size:</th>
|
||||
<td id="sysinfo-dns-cache-size"> </td>
|
||||
</tr>
|
||||
<tr title="Share of the allocated cache that is currently in use (this includes stale/expired entries). If the percentage value is very low, it is advisable to reduce the DNS cache size to optimize performance">
|
||||
<th scope="row">
|
||||
<span>Active cache records:</span>
|
||||
</th>
|
||||
<th scope="row">Active cache records:</th>
|
||||
<td id="cache-utilization"> </td>
|
||||
</tr>
|
||||
<tr title="Number of cache insertions, this number will grow continuously as expiring records naturally make space for new records">
|
||||
<th scope="row">
|
||||
<span>Total cache insertions:</span>
|
||||
</th>
|
||||
<th scope="row">Total cache insertions:</th>
|
||||
<td id="sysinfo-dns-cache-inserted"> </td>
|
||||
</tr>
|
||||
<tr title="Number of cache entries that had to be removed although they are not expired. When this number if larger than zero, you should consider increasing your total cache size" lookatme-text="DNS cache evictions:">
|
||||
<th scope="row">
|
||||
<span>DNS cache evictions:</span>
|
||||
</th>
|
||||
<tr title="Number of cache entries that had to be removed although they are not expired. When this number if larger than zero, you should consider increasing your total cache size">
|
||||
<th scope="row">DNS cache evictions:</th>
|
||||
<td id="sysinfo-dns-cache-evicted"> </td>
|
||||
</tr>
|
||||
<tr title="Number of expired cache entries (they can still be used by the cache optimizer). These entries will only be freed when space is really needed (for efficiency reasons)">
|
||||
<th scope="row">
|
||||
<span>Expired DNS cache entries:</span>
|
||||
</th>
|
||||
<th scope="row">Expired DNS cache entries:</th>
|
||||
<td id="sysinfo-dns-cache-expired"> </td>
|
||||
</tr>
|
||||
<tr title="Number of immortal cache entries that will never expire (e.g. from /etc/hosts or local configuration)">
|
||||
<th scope="row">
|
||||
<span>Immortal DNS cache entries:</span>
|
||||
</th>
|
||||
<th scope="row">Immortal DNS cache entries:</th>
|
||||
<td id="sysinfo-dns-cache-immortal"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- <table class="table table-striped table-bordered nowrap settings-level-expert">
|
||||
<!-- <table class="table table-striped table-bordered nowrap settings-level-expert d-none">
|
||||
<tbody id="dns-cache-table">
|
||||
</tbody>
|
||||
</table> -->
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
See also our <a href="https://docs.pi-hole.net/ftldns/dns-cache/" rel="noopener" target="_blank">DNS cache documentation</a>.<br>
|
||||
See also our <a href="https://docs.pi-hole.net/ftldns/dns-cache/" rel="noopener noreferrer" target="_blank">DNS cache documentation</a>.<br>
|
||||
</div>
|
||||
<div class="col-lg-12" id="cache-metrics-chart">
|
||||
<div style="width:50%">
|
||||
@@ -253,33 +215,23 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<table class="table table-striped table-bordered nowrap">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<span>Local/cache replies:</span>
|
||||
</th>
|
||||
<th scope="row">Local/cache replies:</th>
|
||||
<td id="sysinfo-dns-replies-local"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Forwarded queries:
|
||||
</th>
|
||||
<th scope="row">Forwarded queries:</th>
|
||||
<td id="sysinfo-dns-replies-forwarded"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<span>Cache optimizer replies:</span>
|
||||
</th>
|
||||
<th scope="row">Cache optimizer replies:</th>
|
||||
<td id="sysinfo-dns-replies-optimized"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Unanswered queries:
|
||||
</th>
|
||||
<th scope="row">Unanswered queries:</th>
|
||||
<td id="sysinfo-dns-replies-unanswered"> </td>
|
||||
</tr>
|
||||
<tr class="settings-level-expert">
|
||||
<th scope="row">
|
||||
<span>Authoritative replies:</span>
|
||||
</th>
|
||||
<tr class="settings-level-expert d-none">
|
||||
<th scope="row">Authoritative replies:</th>
|
||||
<td id="sysinfo-dns-replies-auth"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -292,10 +244,10 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 settings-level-expert">
|
||||
<div class="col-md-12 settings-level-expert d-none">
|
||||
<div class="box box-warning">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Actions<span id="actions-title"></span></h3>
|
||||
<h3 class="box-title">Actions</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
@@ -303,23 +255,23 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<button type="button" id="loggingButton" class="btn btn-block button-pad btn-primary"><i class="fa fa-spinner fa-pulse"></i></button>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 col-lg-3">
|
||||
<button type="button" class="btn btn-warning confirm-restartdns btn-block button-pad destructive_action" disabled="true">Restart DNS resolver</button>
|
||||
<button type="button" class="btn btn-warning confirm-restartdns btn-block button-pad destructive_action" disabled>Restart DNS resolver</button>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 col-lg-3">
|
||||
<button type="button" class="btn btn-danger confirm-flusharp btn-block button-pad destructive_action" disabled="true">Flush network table</button>
|
||||
<button type="button" class="btn btn-danger confirm-flusharp btn-block button-pad destructive_action" disabled>Flush network table</button>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 col-lg-3 settings-level-expert">
|
||||
<button type="button" class="btn btn-danger confirm-flushlogs btn-block button-pad destructive_action" disabled="true">Flush logs (last 24 hours)</button>
|
||||
<div class="col-xs-12 col-md-6 col-lg-3 settings-level-expert d-none">
|
||||
<button type="button" class="btn btn-danger confirm-flushlogs btn-block button-pad destructive_action" disabled>Flush logs (last 24 hours)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('vendor/jquery-confirm/jquery.confirm.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/charts.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings-system.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/jquery-confirm/jquery.confirm.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/charts.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings-system.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+10
-10
@@ -44,12 +44,12 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="import-selection">Select what to import</label>
|
||||
<div><input type="checkbox" id="import.config" data-key="config" class="import-select" checked="true"> <label for="import.config"><strong>Configuration</strong></label></div>
|
||||
<div><input type="checkbox" id="import.dhcp_leases" data-key="dhcp_leases" class="import-select" checked="true"> <label for="import.dhcp_leases"><strong>DHCP leases</strong></label></div>
|
||||
<div><input type="checkbox" id="import.gravity.group" data-key="config" class="import-select" checked="true"> <label for="import.gravity.group"><strong>Groups</strong></label></div>
|
||||
<div><input type="checkbox" id="import.gravity.adlist" data-key="config" class="import-select" checked="true"> <label for="import.gravity.adlist"><strong>Lists</strong></label></div>
|
||||
<div><input type="checkbox" id="import.gravity.domainlist" data-key="config" class="import-select" checked="true"> <label for="import.gravity.domainlist"><strong>Domains/Regexes</strong></label></div>
|
||||
<div><input type="checkbox" id="import.gravity.client" data-key="config" class="import-select" checked="true"> <label for="import.gravity.client"><strong>Clients</strong></label></div>
|
||||
<div><input type="checkbox" id="import.config" data-key="config" class="import-select" checked> <label for="import.config"><strong>Configuration</strong></label></div>
|
||||
<div><input type="checkbox" id="import.dhcp_leases" data-key="dhcp_leases" class="import-select" checked> <label for="import.dhcp_leases"><strong>DHCP leases</strong></label></div>
|
||||
<div><input type="checkbox" id="import.gravity.group" data-key="config" class="import-select" checked> <label for="import.gravity.group"><strong>Groups</strong></label></div>
|
||||
<div><input type="checkbox" id="import.gravity.adlist" data-key="config" class="import-select" checked> <label for="import.gravity.adlist"><strong>Lists</strong></label></div>
|
||||
<div><input type="checkbox" id="import.gravity.domainlist" data-key="config" class="import-select" checked> <label for="import.gravity.domainlist"><strong>Domains/Regexes</strong></label></div>
|
||||
<div><input type="checkbox" id="import.gravity.client" data-key="config" class="import-select" checked> <label for="import.gravity.client"><strong>Clients</strong></label></div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-app btn-success" id="submit-import">
|
||||
@@ -89,7 +89,7 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
<div id="modal-import-gravity" style="display:none">
|
||||
<div class="alert alert-info alert-dismissible">
|
||||
<h4><i class="icon fa fa-fw fa-info-circle"></i> Please run gravity</h4>
|
||||
<p>Use <a href='/admin/gravity'>Tools → Update Gravity</a> or run <code>pihole -g</code> to update your lists.</p>
|
||||
<p>Use <a href='<?=webhome?>gravity'>Tools → Update Gravity</a> or run <code>pihole -g</code> to update your lists.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-import-info-message"></div>
|
||||
@@ -101,8 +101,8 @@ mg.include('scripts/lua/settings_header.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings-teleporter.js')?>"></script>
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('vendor/bootstrap-toggle/bootstrap-toggle.min.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings-teleporter.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/settings.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
+8
-15
@@ -289,11 +289,6 @@ td.lookatme {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.strike,
|
||||
.strike span {
|
||||
text-decoration: line-through !important;
|
||||
}
|
||||
|
||||
/* These are needed because AdmintLTE 2.x doesn't support Font Awesome 5.x */
|
||||
.sidebar-menu > li > a > .fab,
|
||||
.sidebar-menu > li > a > .far,
|
||||
@@ -344,7 +339,7 @@ td.lookatme {
|
||||
}
|
||||
|
||||
.user-panel > .image > img {
|
||||
max-width: 52px;
|
||||
max-width: 60px;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
@@ -354,6 +349,7 @@ td.lookatme {
|
||||
line-height: 1.05;
|
||||
flex: 1 1 auto;
|
||||
left: auto;
|
||||
width: 138px;
|
||||
}
|
||||
|
||||
.user-panel > .info > p {
|
||||
@@ -913,6 +909,7 @@ body:not([class*="lcars"])
|
||||
.navbar-nav > .user-menu > .dropdown-menu > li.user-header {
|
||||
height: auto;
|
||||
padding: 15px 15px 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.navbar-nav > .user-menu > .dropdown-menu > li.user-header > img {
|
||||
@@ -1254,14 +1251,6 @@ table.dataTable tbody > tr > .selected {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-level-basic {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-level-expert {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main-footer {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
@@ -1290,7 +1279,7 @@ table.dataTable tbody > tr > .selected {
|
||||
float: none;
|
||||
text-align: center;
|
||||
font-size: 100px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
@@ -1500,6 +1489,10 @@ table.dataTable tbody > tr > .selected {
|
||||
padding-top: 1rem !important;
|
||||
}
|
||||
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.password_background {
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
|
||||
@@ -565,7 +565,10 @@ fieldset[disabled] .form-control {
|
||||
border: 1px solid #353c42;
|
||||
color: #bec5cb;
|
||||
}
|
||||
.box.box-solid.box-info,
|
||||
|
||||
.box.box-solid.box-info {
|
||||
border: 1px solid #367fa9;
|
||||
}
|
||||
.box.box-solid.box-info > .box-header {
|
||||
color: #bec5cb;
|
||||
background-color: #367fa9 !important;
|
||||
@@ -591,17 +594,6 @@ input[type="password"]::-webkit-caps-lock-indicator {
|
||||
background-image: linear-gradient(to right, #114100 0%, #525200 100%);
|
||||
}
|
||||
|
||||
.icheckbox_polaris,
|
||||
.icheckbox_futurico,
|
||||
.icheckbox_minimal-blue {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.iradio_polaris,
|
||||
.iradio_futurico,
|
||||
.iradio_minimal-blue {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Overlay box with spinners as shown during data collection for graphs */
|
||||
.box .overlay,
|
||||
.overlay-wrapper .overlay {
|
||||
|
||||
@@ -5480,9 +5480,6 @@ link-muted {
|
||||
.chartjs-tooltip.left.center .arrow {
|
||||
border-right-color: rgba(140, 130, 115, 0.8);
|
||||
}
|
||||
.strike {
|
||||
text-decoration-color: currentcolor !important;
|
||||
}
|
||||
.text-underline {
|
||||
text-decoration-color: currentcolor;
|
||||
}
|
||||
@@ -5675,23 +5672,6 @@ td.highlight {
|
||||
[class*="icheck-"] > input:first-child:disabled + label::before {
|
||||
box-shadow: none;
|
||||
}
|
||||
.icheck-default
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-default > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(71, 77, 80);
|
||||
}
|
||||
.icheck-default > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-default > input:first-child:checked + label::before {
|
||||
background-color: rgb(38, 41, 43);
|
||||
border-color: rgb(71, 77, 80);
|
||||
}
|
||||
.icheck-default > input:first-child:checked + input[type="hidden"] + label::after,
|
||||
.icheck-default > input:first-child:checked + label::after {
|
||||
border-bottom-color: rgb(123, 114, 101);
|
||||
border-right-color: rgb(123, 114, 101);
|
||||
}
|
||||
.icheck-primary
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
@@ -5704,299 +5684,6 @@ td.highlight {
|
||||
background-color: rgb(41, 98, 146);
|
||||
border-color: rgb(42, 100, 150);
|
||||
}
|
||||
.icheck-success
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-success > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(55, 125, 55);
|
||||
}
|
||||
.icheck-success > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-success > input:first-child:checked + label::before {
|
||||
background-color: rgb(77, 133, 58);
|
||||
border-color: rgb(55, 125, 55);
|
||||
}
|
||||
.icheck-info
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-info > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(28, 115, 140);
|
||||
}
|
||||
.icheck-info > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-info > input:first-child:checked + label::before {
|
||||
background-color: rgb(28, 115, 141);
|
||||
border-color: rgb(28, 115, 140);
|
||||
}
|
||||
.icheck-warning
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-warning > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(154, 96, 13);
|
||||
}
|
||||
.icheck-warning > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-warning > input:first-child:checked + label::before {
|
||||
background-color: rgb(153, 95, 13);
|
||||
border-color: rgb(154, 96, 13);
|
||||
}
|
||||
.icheck-danger
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-danger > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(143, 35, 31);
|
||||
}
|
||||
.icheck-danger > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-danger > input:first-child:checked + label::before {
|
||||
background-color: rgb(148, 35, 32);
|
||||
border-color: rgb(143, 35, 31);
|
||||
}
|
||||
.icheck-turquoise
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-turquoise > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(23, 168, 139);
|
||||
}
|
||||
.icheck-turquoise > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-turquoise > input:first-child:checked + label::before {
|
||||
background-color: rgb(21, 150, 125);
|
||||
border-color: rgb(23, 168, 139);
|
||||
}
|
||||
.icheck-emerland
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-emerland > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(33, 147, 81);
|
||||
}
|
||||
.icheck-emerland > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-emerland > input:first-child:checked + label::before {
|
||||
background-color: rgb(37, 163, 90);
|
||||
border-color: rgb(33, 147, 81);
|
||||
}
|
||||
.icheck-peterriver
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-peterriver > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(26, 99, 148);
|
||||
}
|
||||
.icheck-peterriver > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-peterriver > input:first-child:checked + label::before {
|
||||
background-color: rgb(29, 111, 165);
|
||||
border-color: rgb(26, 99, 148);
|
||||
}
|
||||
.icheck-amethyst
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-amethyst > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(101, 53, 121);
|
||||
}
|
||||
.icheck-amethyst > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-amethyst > input:first-child:checked + label::before {
|
||||
background-color: rgb(113, 59, 135);
|
||||
border-color: rgb(101, 53, 121);
|
||||
}
|
||||
.icheck-wetasphalt
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-wetasphalt > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(75, 106, 136);
|
||||
}
|
||||
.icheck-wetasphalt > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-wetasphalt > input:first-child:checked + label::before {
|
||||
background-color: rgb(42, 58, 75);
|
||||
border-color: rgb(75, 106, 136);
|
||||
}
|
||||
.icheck-greensea
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-greensea > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(24, 176, 146);
|
||||
}
|
||||
.icheck-greensea > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-greensea > input:first-child:checked + label::before {
|
||||
background-color: rgb(18, 128, 106);
|
||||
border-color: rgb(24, 176, 146);
|
||||
}
|
||||
.icheck-nephritis
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-nephritis > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(35, 156, 86);
|
||||
}
|
||||
.icheck-nephritis > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-nephritis > input:first-child:checked + label::before {
|
||||
background-color: rgb(31, 139, 77);
|
||||
border-color: rgb(35, 156, 86);
|
||||
}
|
||||
.icheck-belizehole
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-belizehole > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(34, 106, 153);
|
||||
}
|
||||
.icheck-belizehole > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-belizehole > input:first-child:checked + label::before {
|
||||
background-color: rgb(33, 102, 148);
|
||||
border-color: rgb(34, 106, 153);
|
||||
}
|
||||
.icheck-wisteria
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-wisteria > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(108, 52, 131);
|
||||
}
|
||||
.icheck-wisteria > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-wisteria > input:first-child:checked + label::before {
|
||||
background-color: rgb(114, 54, 138);
|
||||
border-color: rgb(108, 52, 131);
|
||||
}
|
||||
.icheck-midnightblue
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-midnightblue > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(77, 109, 141);
|
||||
}
|
||||
.icheck-midnightblue > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-midnightblue > input:first-child:checked + label::before {
|
||||
background-color: rgb(35, 50, 64);
|
||||
border-color: rgb(77, 109, 141);
|
||||
}
|
||||
.icheck-sunflower
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-sunflower > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(168, 137, 10);
|
||||
}
|
||||
.icheck-sunflower > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-sunflower > input:first-child:checked + label::before {
|
||||
background-color: rgb(192, 156, 11);
|
||||
border-color: rgb(168, 137, 10);
|
||||
}
|
||||
.icheck-carrot
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-carrot > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(158, 84, 18);
|
||||
}
|
||||
.icheck-carrot > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-carrot > input:first-child:checked + label::before {
|
||||
background-color: rgb(178, 94, 20);
|
||||
border-color: rgb(158, 84, 18);
|
||||
}
|
||||
.icheck-alizarin
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-alizarin > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(149, 31, 18);
|
||||
}
|
||||
.icheck-alizarin > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-alizarin > input:first-child:checked + label::before {
|
||||
background-color: rgb(162, 33, 20);
|
||||
border-color: rgb(149, 31, 18);
|
||||
}
|
||||
.icheck-clouds
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-clouds > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(53, 57, 59);
|
||||
}
|
||||
.icheck-clouds > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-clouds > input:first-child:checked + label::before {
|
||||
background-color: rgb(30, 39, 41);
|
||||
border-color: rgb(53, 57, 59);
|
||||
}
|
||||
.icheck-clouds > input:first-child:checked + input[type="hidden"] + label::after,
|
||||
.icheck-clouds > input:first-child:checked + label::after {
|
||||
border-bottom-color: rgb(76, 82, 85);
|
||||
border-right-color: rgb(76, 82, 85);
|
||||
}
|
||||
.icheck-concrete
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-concrete > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(76, 82, 85);
|
||||
}
|
||||
.icheck-concrete > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-concrete > input:first-child:checked + label::before {
|
||||
background-color: rgb(79, 86, 89);
|
||||
border-color: rgb(76, 82, 85);
|
||||
}
|
||||
.icheck-orange
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-orange > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(168, 106, 9);
|
||||
}
|
||||
.icheck-orange > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-orange > input:first-child:checked + label::before {
|
||||
background-color: rgb(191, 121, 10);
|
||||
border-color: rgb(168, 106, 9);
|
||||
}
|
||||
.icheck-pumpkin
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-pumpkin > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(192, 76, 0);
|
||||
}
|
||||
.icheck-pumpkin > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-pumpkin > input:first-child:checked + label::before {
|
||||
background-color: rgb(169, 67, 0);
|
||||
border-color: rgb(192, 76, 0);
|
||||
}
|
||||
.icheck-pomegranate
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-pomegranate > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(151, 45, 34);
|
||||
}
|
||||
.icheck-pomegranate > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-pomegranate > input:first-child:checked + label::before {
|
||||
background-color: rgb(154, 46, 34);
|
||||
border-color: rgb(151, 45, 34);
|
||||
}
|
||||
.icheck-silver
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-silver > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(65, 71, 73);
|
||||
}
|
||||
.icheck-silver > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-silver > input:first-child:checked + label::before {
|
||||
background-color: rgb(58, 63, 66);
|
||||
border-color: rgb(65, 71, 73);
|
||||
}
|
||||
.icheck-asbestos
|
||||
> input:first-child:not(:checked):not(:disabled):hover
|
||||
+ input[type="hidden"]
|
||||
+ label::before,
|
||||
.icheck-asbestos > input:first-child:not(:checked):not(:disabled):hover + label::before {
|
||||
border-color: rgb(82, 89, 92);
|
||||
}
|
||||
.icheck-asbestos > input:first-child:checked + input[type="hidden"] + label::before,
|
||||
.icheck-asbestos > input:first-child:checked + label::before {
|
||||
background-color: rgb(92, 100, 104);
|
||||
border-color: rgb(82, 89, 92);
|
||||
}
|
||||
|
||||
/* Override Style */
|
||||
.vimvixen-hint {
|
||||
|
||||
@@ -264,13 +264,6 @@ td.highlight {
|
||||
background-image: linear-gradient(to right, #e7ffde 0%, #ffffdf 100%);
|
||||
}
|
||||
|
||||
.icheckbox_minimal-blue {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.iradio_minimal-blue {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.datatable-bt,
|
||||
.datatable-bt:active,
|
||||
.datatable-bt:active:focus,
|
||||
|
||||
@@ -752,7 +752,10 @@ fieldset[disabled] .form-control {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.box.box-solid.box-info,
|
||||
.box.box-solid.box-info {
|
||||
border: 1px solid #367fa9;
|
||||
}
|
||||
|
||||
.box.box-solid.box-info > .box-header {
|
||||
color: var(--main-text-color);
|
||||
background-color: #367fa9 !important;
|
||||
@@ -779,17 +782,6 @@ input[type="password"]::-webkit-caps-lock-indicator {
|
||||
background-image: linear-gradient(to right, var(--network-recent) 0%, var(--network-old) 100%);
|
||||
}
|
||||
|
||||
.icheckbox_polaris,
|
||||
.icheckbox_futurico,
|
||||
.icheckbox_minimal-blue {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.iradio_polaris,
|
||||
.iradio_futurico,
|
||||
.iradio_minimal-blue {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Overlay box with spinners as shown during data collection for graphs */
|
||||
.box .overlay,
|
||||
.overlay-wrapper .overlay {
|
||||
|
||||
@@ -304,13 +304,6 @@ a:hover {
|
||||
background: #eb4;
|
||||
}
|
||||
|
||||
.icheckbox_minimal-blue {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.iradio_minimal-blue {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.datatable-bt,
|
||||
.datatable-bt:active,
|
||||
.datatable-bt:active:focus,
|
||||
@@ -374,11 +367,9 @@ select:-webkit-autofill {
|
||||
background: var(--success-color) !important;
|
||||
}
|
||||
|
||||
.small-box > .small-box-footer {
|
||||
color: #fff;
|
||||
}
|
||||
.small-box > .small-box-footer {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Tables and Datatables */
|
||||
|
||||
+100
-45
@@ -5,7 +5,7 @@
|
||||
--sidebar-width: 230px;
|
||||
--gradient-pos: 304px;
|
||||
--primary-color: #48f;
|
||||
--text-color: #9ab;
|
||||
--text-color: #abc;
|
||||
--net-never-color: #000;
|
||||
--net-recent-color: #055;
|
||||
--net-old-color: #125;
|
||||
@@ -35,7 +35,7 @@ html {
|
||||
|
||||
body {
|
||||
font-size: 17px;
|
||||
color: #9ab;
|
||||
color: var(--text-color);
|
||||
background: #000;
|
||||
font-family:
|
||||
Antonio, Oswald, "Myriad Pro Cond", "Roboto Condensed", "Futura Condensed",
|
||||
@@ -101,7 +101,7 @@ pre {
|
||||
line-height: 1em;
|
||||
font-size: 1em;
|
||||
color: #000;
|
||||
background-color: #9ab;
|
||||
background-color: var(--text-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ pre {
|
||||
padding: 10px 3px;
|
||||
border-radius: 12px;
|
||||
background: #181818;
|
||||
color: #9ab;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
td code {
|
||||
@@ -136,7 +136,7 @@ td code {
|
||||
|
||||
kbd {
|
||||
color: #000;
|
||||
background-color: #9ab;
|
||||
background-color: var(--text-color);
|
||||
box-shadow: inset -1px -1px 2px 0 rgba(0, 0, 0, 0.5);
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -176,7 +176,7 @@ th {
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background-color: #9ab;
|
||||
background-color: var(--text-color);
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -189,13 +189,24 @@ th {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.btn-app {
|
||||
padding: 15px 5px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
background: #48f;
|
||||
}
|
||||
|
||||
.btn-app:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
padding: 1px 5px;
|
||||
padding: 0 5px 2px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.btn.btn-box-tool {
|
||||
color: #9ab;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.btn.btn-box-tool:hover,
|
||||
@@ -411,7 +422,7 @@ p.login-box-msg,
|
||||
|
||||
.select2 .select2-selection {
|
||||
background-color: #000;
|
||||
color: #9ab;
|
||||
color: var(--text-color);
|
||||
border: 2px solid #567;
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -424,7 +435,7 @@ p.login-box-msg,
|
||||
}
|
||||
|
||||
.select2-dropdown {
|
||||
background-color: #9ab;
|
||||
background-color: var(--text-color);
|
||||
color: #000;
|
||||
border: none;
|
||||
}
|
||||
@@ -520,15 +531,31 @@ p.login-box-msg,
|
||||
margin: 5px 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
color: #9ab;
|
||||
color: var(--text-color);
|
||||
background: #111111 !important;
|
||||
border: 3px solid #4488ff !important;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 0 2000vmax rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.dropdown-menu > li > a:hover {
|
||||
color: #06c;
|
||||
.dropdown-menu {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.dropdown-menu > li > a:hover,
|
||||
.dropdown-menu > li > a:active,
|
||||
.dropdown-menu > li > a:focus {
|
||||
color: #fff;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.dropdown-menu .bs-actionsbox button {
|
||||
text-align: center;
|
||||
padding: 5px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.dropdown-menu .bs-actionsbox button[disabled] {
|
||||
background: #567;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
@@ -552,19 +579,30 @@ p.login-box-msg,
|
||||
|
||||
.bootstrap-select.bs-container .dropdown-menu,
|
||||
.open > .dropdown-menu {
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
border-width: 2px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.bootstrap-select.bs-container .dropdown-menu.open,
|
||||
.open > .dropdown-menu.open {
|
||||
filter: invert(0.88) hue-rotate(180deg);
|
||||
background: #111;
|
||||
border-color: #567;
|
||||
}
|
||||
|
||||
.open > .dropdown-toggle.btn-default,
|
||||
.open > .dropdown-toggle.btn-default:active {
|
||||
background-color: #353c42;
|
||||
color: #bec5cb;
|
||||
.dropdown-toggle.btn-default,
|
||||
.dropdown-toggle.btn-default:active,
|
||||
.open > .dropdown-toggle.btn-default {
|
||||
padding: 0.4em 1em 0.2em 2em;
|
||||
color: var(--text-color);
|
||||
background-color: #000;
|
||||
border: 2px solid #567 !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.open > .dropdown-toggle.btn-default:hover {
|
||||
background: #567;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/*** Set .dropdown-toggle width to fill the whole table cell ***/
|
||||
@@ -1012,7 +1050,7 @@ p.login-box-msg,
|
||||
}
|
||||
|
||||
.user-panel > .info > span {
|
||||
margin: 0;
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
.user-panel .pull-left.image {
|
||||
@@ -1020,7 +1058,8 @@ p.login-box-msg,
|
||||
}
|
||||
|
||||
.user-panel > .info i {
|
||||
text-shadow: 0 0 1px black;
|
||||
text-shadow: 0 0 1px #fff;
|
||||
margin: 0 2px 0 -2px;
|
||||
}
|
||||
|
||||
.user-panel > .info i.text-orange,
|
||||
@@ -1160,13 +1199,17 @@ footer a:focus {
|
||||
.comment-text,
|
||||
.comment-text .username,
|
||||
.box-info {
|
||||
color: #9ab;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.btn .box-title {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.box.box-solid .box-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.box {
|
||||
margin-bottom: 1.8em;
|
||||
background: #111;
|
||||
@@ -1205,6 +1248,10 @@ table.table-bordered.dataTable td {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.add-new-item {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.box-footer {
|
||||
border-radius: 0;
|
||||
border-top: 4px solid #000;
|
||||
@@ -1248,7 +1295,7 @@ table.table-bordered.dataTable td {
|
||||
.nav-tabs-custom > .nav-tabs > li {
|
||||
margin: 0 2px;
|
||||
border: none;
|
||||
background: #9ab;
|
||||
background: var(--text-color);
|
||||
}
|
||||
|
||||
.nav-tabs-custom > .nav-tabs > li > a {
|
||||
@@ -1420,7 +1467,7 @@ table.dataTable thead .sorting_desc_disabled:after {
|
||||
margin: 0 1px;
|
||||
padding: 3px 4px 4px;
|
||||
min-width: 34px;
|
||||
background: #9ab;
|
||||
background: var(--text-color);
|
||||
color: #000;
|
||||
text-align: center;
|
||||
border: none;
|
||||
@@ -1664,11 +1711,6 @@ table.dataTable {
|
||||
.main-header .navbar .dropdown-menu li.divider {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.dropdown-menu > li > a:hover,
|
||||
.dropdown-menu > li > a:active,
|
||||
.dropdown-menu > li > a:focus {
|
||||
color: #48f;
|
||||
}
|
||||
.main-header .logo {
|
||||
width: 150px;
|
||||
float: left;
|
||||
@@ -1889,15 +1931,6 @@ table.dataTable tbody > tr > .selected td {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
/*** icheckbox ***/
|
||||
.icheckbox_minimal-blue {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.iradio_minimal-blue {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background: #222;
|
||||
}
|
||||
@@ -1946,22 +1979,26 @@ input[type="password"]::-webkit-caps-lock-indicator {
|
||||
}
|
||||
|
||||
.graphs-ticks {
|
||||
color: #9ab;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background-color: #48f;
|
||||
}
|
||||
|
||||
/*--- Used on the status panel ---*/
|
||||
.user-panel .text-green-light {
|
||||
color: #23c027 !important;
|
||||
}
|
||||
.user-panel .text-red {
|
||||
color: #f60d1a !important;
|
||||
}
|
||||
|
||||
/*--- Used in the Query Log table ---*/
|
||||
.text-black {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.text-green-light {
|
||||
color: #7fff00 !important;
|
||||
}
|
||||
|
||||
.text-green {
|
||||
color: #080 !important;
|
||||
}
|
||||
@@ -2119,9 +2156,23 @@ td.highlight {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/*** Tools > Interface page ***/
|
||||
.bstreeview .list-group-item {
|
||||
padding-top: 0.2em;
|
||||
padding-bottom: 0.55em;
|
||||
padding-top: 0.15em;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.list-group > .list-group-item {
|
||||
padding-left: 1.75em !important;
|
||||
}
|
||||
.list-group .list-group > .list-group-item {
|
||||
padding-left: 3.5em !important;
|
||||
}
|
||||
.list-group .list-group .list-group > .list-group-item {
|
||||
padding-left: 5.25em !important;
|
||||
}
|
||||
.list-group .list-group .list-group .list-group > .list-group-item {
|
||||
padding-left: 7em !important;
|
||||
}
|
||||
|
||||
/*** 🖖 ***/
|
||||
@@ -2129,3 +2180,7 @@ td.highlight {
|
||||
--fa: "\f259" !important;
|
||||
--fa--fa: "\f259\f259" !important;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
color: #76808a;
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,6 +33,6 @@ mg.include('scripts/lua/header_authenticated.lp','r')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="<?=pihole.fileversion('scripts/js/taillog.js')?>"></script>
|
||||
<script src="<?=pihole.fileversion('scripts/js/taillog.js')?>"></script>
|
||||
|
||||
<? mg.include('scripts/lua/footer.lp','r')?>
|
||||
|
||||
Vendored
+3
-9
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
-6
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user