Compare commits

...

29 Commits

Author SHA1 Message Date
Henry Whitaker
e863a53b77 Add ziggy things 2021-11-15 23:26:58 +00:00
Henry Whitaker
5bdb7490a4 fix inertia things 2021-11-15 23:14:01 +00:00
Henry Whitaker
68c1ccb23a controller things 2021-11-15 22:35:04 +00:00
Henry Whitaker
b3af5629dd added in inertia things 2021-11-15 22:34:54 +00:00
Henry Whitaker
c9748af04f Remove all react things 2021-11-15 22:00:35 +00:00
Henry Whitaker
3955a2dad0 Merge pull request #705 from henrywhitaker3/fix/failure-graph
Fix bug where failure graph would not update
2021-09-11 09:31:06 +01:00
Henry Whitaker
6905a5aac0 Fix bug where failure graph would not update 2021-09-11 09:29:21 +01:00
Henry Whitaker
b3ba6a8b06 Merge remote-tracking branch 'origin/dev' into dev 2021-09-10 20:18:22 +01:00
Henry Whitaker
1e1a2c9b69 Use uname for arch download instead of env
from #683
2021-09-10 20:18:14 +01:00
Henry Whitaker
1c00beed86 Merge pull request #704 from henrywhitaker3/alpha
Docker image build step
2021-09-10 20:04:17 +01:00
Henry Whitaker
603f618720 Add for master also 2021-09-10 20:03:12 +01:00
Henry Whitaker
fa41bdd40d Move to dev 2021-09-10 20:01:30 +01:00
Henry Whitaker
9d2f1858a0 Fix the things 2021-09-10 19:53:36 +01:00
Henry Whitaker
6c6a12bb5f ugh 2021-09-10 19:51:34 +01:00
Henry Whitaker
5885824c54 Fix secret name 2021-09-10 19:49:26 +01:00
Henry Whitaker
ea78415157 Fix branch selector 2021-09-10 19:47:32 +01:00
Henry Whitaker
efecd28afb Add build step to workflow 2021-09-10 19:47:08 +01:00
Henry Whitaker
8d32d06768 Merge remote-tracking branch 'origin/master' into alpha 2021-09-10 19:26:31 +01:00
Henry Whitaker
58e82dc9a8 Actually pass days to the action 2021-09-10 18:58:02 +01:00
Henry Whitaker
0dff534dfd Also do the arm dockerfile 2021-09-10 18:54:46 +01:00
Henry Whitaker
fba69514b2 undo dumb thing 2021-09-10 18:50:40 +01:00
Henry Whitaker
1fcaa795cd Chmod +x the artsan file 2021-09-10 18:48:36 +01:00
Henry Whitaker
858dcc1f7b Merge pull request #685 from henrywhitaker3/fix/680/last-x-days
#680: Fix issue where the data would refresh before the react state had updated
2021-09-10 18:41:40 +01:00
Henry Whitaker
283de67e17 Merge pull request #686 from henrywhitaker3/feat/sort-out-dockerfiles
Feat/sort out dockerfiles
2021-09-10 18:40:35 +01:00
Henry Whitaker
d92dd33b84 Update install type 2021-09-10 18:38:50 +01:00
Henry Whitaker
0c8e074a86 Fix the eula stuff 2021-09-10 18:38:26 +01:00
Henry Whitaker
0384f61f15 Added dockerfiles to the main branches 2021-09-10 18:30:50 +01:00
Henry Whitaker
98e037bf97 Update version #s 2021-09-10 18:20:03 +01:00
Henry Whitaker
0b1645cda0 Fix issue where the data would refresh before the react state had updated 2021-09-10 18:16:17 +01:00
76 changed files with 214990 additions and 158981 deletions

19
.dockerignore Normal file
View File

@@ -0,0 +1,19 @@
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
.vscode/
_ide_helper.php
.idea
.config
reports/

View File

@@ -1,59 +1,29 @@
name: Dev
name: Build Dev Image
on:
push:
branches: [ dev ]
jobs:
laravel-tests:
build:
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
with:
php-version: '7.4'
- uses: actions/checkout@v2
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Update .env with secrets
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
HEALTHCHECKS_UUID: ${{ secrets.HEALTHCHECKS_UUID }}
run: |
echo SLACK_WEBHOOK=$SLACK_WEBHOOK >> .env
echo TELEGRAM_BOT_TOKEN=$TELEGRAM_BOT_TOKEN >> .env
echo TELEGRAM_CHAT_ID=$TELEGRAM_CHAT_ID >> .env
echo HEALTHCHECKS_UUID=$HEALTHCHECKS_UUID >> .env
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Create Database
run: |
mkdir -p database
touch database/database.sqlite
- name: Generate key
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: php artisan key:generate
- name: Generate JWT key
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: php artisan jwt:secret
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Download Speedtest binary
run: wget https://install.speedtest.net/app/cli/ookla-speedtest-1.0.0-x86_64-linux.tgz -O speedtest.tgz && tar zxvf speedtest.tgz && mv speedtest app/Bin/
- name: Accept EULA
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: php artisan speedtest:eula
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: vendor/bin/phpunit
- name: Checkout
uses: actions/checkout@v2
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v1.2.0
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: henrywhitaker3/speedtest-tracker:dev,henrywhitaker3/speedtest-tracker:dev-arm

29
.github/workflows/laravel-master.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Build Latest Image
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v1.2.0
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: henrywhitaker3/speedtest-tracker:latest,henrywhitaker3/speedtest-tracker:latest-arm

View File

@@ -1,59 +0,0 @@
name: Stable
on:
push:
branches: [ master ]
jobs:
laravel-tests:
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
with:
php-version: '7.4'
- uses: actions/checkout@v2
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Update .env with secrets
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
HEALTHCHECKS_UUID: ${{ secrets.HEALTHCHECKS_UUID }}
run: |
echo SLACK_WEBHOOK=$SLACK_WEBHOOK >> .env
echo TELEGRAM_BOT_TOKEN=$TELEGRAM_BOT_TOKEN >> .env
echo TELEGRAM_CHAT_ID=$TELEGRAM_CHAT_ID >> .env
echo HEALTHCHECKS_UUID=$HEALTHCHECKS_UUID >> .env
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Create Database
run: |
mkdir -p database
touch database/database.sqlite
- name: Generate key
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: php artisan key:generate
- name: Generate JWT key
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: php artisan jwt:secret
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Download Speedtest binary
run: wget https://install.speedtest.net/app/cli/ookla-speedtest-1.0.0-x86_64-linux.tgz -O speedtest.tgz && tar zxvf speedtest.tgz && mv speedtest app/Bin/
- name: Accept EULA
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: php artisan speedtest:eula
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: vendor/bin/phpunit

View File

@@ -1,6 +1,6 @@
# Speedtest Tracker
[![Docker pulls](https://img.shields.io/docker/pulls/henrywhitaker3/speedtest-tracker?style=flat-square)](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Stable?label=master&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Dev?label=dev&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![last_commit](https://img.shields.io/github/last-commit/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [![issues](https://img.shields.io/github/issues/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [![commit_freq](https://img.shields.io/github/commit-activity/m/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) ![version](https://img.shields.io/badge/version-v1.12.0-success?style=flat-square) [![license](https://img.shields.io/github/license/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE)
[![Docker pulls](https://img.shields.io/docker/pulls/henrywhitaker3/speedtest-tracker?style=flat-square)](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Stable?label=master&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/henrywhitaker3/Speedtest-Tracker/Dev?label=dev&logo=github&style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/actions) [![last_commit](https://img.shields.io/github/last-commit/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [![issues](https://img.shields.io/github/issues/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [![commit_freq](https://img.shields.io/github/commit-activity/m/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) ![version](https://img.shields.io/badge/version-v1.12.2-success?style=flat-square) [![license](https://img.shields.io/github/license/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE)
This program runs a speedtest check every hour and graphs the results. The back-end is written in [Laravel](https://laravel.com/) and the front-end uses [React](https://reactjs.org/). It uses the [Ookla's speedtest cli](https://www.speedtest.net/apps/cli) package to get the data and uses [Chart.js](https://www.chartjs.org/) to plot the results.

View File

@@ -39,6 +39,6 @@ class AcceptEULACommand extends Command
public function handle()
{
$this->info('Acceping EULA');
shell_exec(config('speedtest.home') . ' && timeout 3s ' . app_path() . '/Bin/speedtest --accept-license --accept-gdpr');
shell_exec(config('speedtest.home') . ' && timeout 10s ' . app_path() . '/Bin/speedtest --accept-license --accept-gdpr');
}
}

View File

@@ -27,8 +27,8 @@ class HomepageDataController extends Controller
return [
'latest' => run(GetLatestSpeedtestData::class),
'time' => run(GetSpeedtestTimeData::class),
'fail' => run(GetFailedSpeedtestData::class),
'time' => run(GetSpeedtestTimeData::class, $days),
'fail' => run(GetFailedSpeedtestData::class, $days),
'config' => SettingsHelper::getConfig(),
];
}

View File

@@ -15,7 +15,7 @@ class IntegrationsController extends Controller
{
public function __construct()
{
if((bool)SettingsHelper::get('auth')->value === true) {
if ((bool)SettingsHelper::get('auth')->value === true) {
$this->middleware('auth:api');
}
}
@@ -31,15 +31,15 @@ class IntegrationsController extends Controller
try {
// SettingsHelper::loadIntegrationConfig();
if($method == 'success') {
if ($method == 'success') {
Healthcheck::success();
}
if($method == 'fail') {
if ($method == 'fail') {
Healthcheck::fail();
}
if($method == 'start') {
if ($method == 'start') {
Healthcheck::start();
}
@@ -47,19 +47,19 @@ class IntegrationsController extends Controller
'method' => $methodResp,
'success' => true
], 200);
} catch(InvalidUuidStringException $e) {
} catch (InvalidUuidStringException $e) {
return response()->json([
'method' => $methodResp,
'success' => false,
'error' => 'Invalid UUID'
], 422);
} catch(HealthchecksUuidNotFoundException $e) {
} catch (HealthchecksUuidNotFoundException $e) {
return response()->json([
'method' => $methodResp,
'success' => false,
'error' => 'UUID not found'
], 404);
} catch(Exception $e) {
} catch (Exception $e) {
return response()->json([
'method' => $methodResp,
'success' => false,

View File

@@ -36,6 +36,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HandleInertiaRequests::class,
],
'api' => [

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that's loaded on the first page visit.
*
* @see https://inertiajs.com/server-side-setup#root-template
* @var string
*/
protected $rootView = 'app';
/**
* Determines the current asset version.
*
* @see https://inertiajs.com/asset-versioning
* @param \Illuminate\Http\Request $request
* @return string|null
*/
public function version(Request $request)
{
return parent::version($request);
}
/**
* Defines the props that are shared by default.
*
* @see https://inertiajs.com/shared-data
* @param \Illuminate\Http\Request $request
* @return array
*/
public function share(Request $request)
{
return array_merge(parent::share($request), [
//
]);
}
}

View File

@@ -1,4 +1,16 @@
{
"1.12.2": [
{
"description": "Fixed a bug where the latest X days widget would not update for the failure graph",
"link": ""
}
],
"1.12.1": [
{
"description": "Fixed a bug where the latest X days widget would not update (#680)",
"link": "https://github.com/henrywhitaker3/Speedtest-Tracker/pull/680"
}
],
"1.12.0": [
{
"description": "Added InfluxDB intergation.",

View File

@@ -16,6 +16,7 @@
"guzzlehttp/guzzle": "^7.0.1",
"henrywhitaker3/healthchecks-io": "^1.0",
"henrywhitaker3/laravel-actions": "^1.0",
"inertiajs/inertia-laravel": "^0.4.5",
"influxdata/influxdb-client-php": "^1.12",
"influxdb/influxdb-php": "^1.15",
"laravel-notification-channels/telegram": "^0.5.0",
@@ -23,6 +24,7 @@
"laravel/slack-notification-channel": "^2.0",
"laravel/tinker": "^2.0",
"laravel/ui": "^3.0",
"tightenco/ziggy": "^1.4",
"tymon/jwt-auth": "^1.0"
},
"require-dev": {

136
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "46d75195c9e28db55cd2b086a6846e13",
"content-hash": "3175378417ef75a6f3d9fa7e5834f9b0",
"packages": [
{
"name": "asm89/stack-cors",
@@ -1309,6 +1309,74 @@
},
"time": "2021-02-06T09:50:49+00:00"
},
{
"name": "inertiajs/inertia-laravel",
"version": "v0.4.5",
"source": {
"type": "git",
"url": "https://github.com/inertiajs/inertia-laravel.git",
"reference": "406b15af162e78be5c7793b25aadd5a183eea84b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/406b15af162e78be5c7793b25aadd5a183eea84b",
"reference": "406b15af162e78be5c7793b25aadd5a183eea84b",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": "^5.4|^6.0|^7.0|^8.0",
"php": "^7.2|^8.0"
},
"require-dev": {
"orchestra/testbench": "^4.0|^5.0|^6.0",
"phpunit/phpunit": "^8.0|^9.0",
"roave/security-advisories": "dev-master"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Inertia\\ServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Inertia\\": "src"
},
"files": [
"./helpers.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jonathan Reinink",
"email": "jonathan@reinink.ca",
"homepage": "https://reinink.ca"
}
],
"description": "The Laravel adapter for Inertia.js.",
"keywords": [
"inertia",
"laravel"
],
"support": {
"issues": "https://github.com/inertiajs/inertia-laravel/issues",
"source": "https://github.com/inertiajs/inertia-laravel/tree/v0.4.5"
},
"funding": [
{
"url": "https://github.com/reinink",
"type": "github"
}
],
"time": "2021-10-27T09:37:59+00:00"
},
{
"name": "influxdata/influxdb-client-php",
"version": "1.12.0",
@@ -5561,6 +5629,72 @@
],
"time": "2021-02-18T23:11:19+00:00"
},
{
"name": "tightenco/ziggy",
"version": "v1.4.2",
"source": {
"type": "git",
"url": "https://github.com/tighten/ziggy.git",
"reference": "620c135281062b9f6b53a75b07f99a4339267277"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tighten/ziggy/zipball/620c135281062b9f6b53a75b07f99a4339267277",
"reference": "620c135281062b9f6b53a75b07f99a4339267277",
"shasum": ""
},
"require": {
"laravel/framework": ">=5.4@dev"
},
"require-dev": {
"orchestra/testbench": "^6.0",
"phpunit/phpunit": "^9.2"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Tightenco\\Ziggy\\ZiggyServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Tightenco\\Ziggy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Coulbourne",
"email": "daniel@tighten.co"
},
{
"name": "Jake Bathman",
"email": "jake@tighten.co"
},
{
"name": "Jacob Baker-Kretzmar",
"email": "jacob@tighten.co"
}
],
"description": "Generates a Blade directive exporting all of your named Laravel routes. Also provides a nice route() helper function in JavaScript.",
"homepage": "https://github.com/tighten/ziggy",
"keywords": [
"Ziggy",
"javascript",
"laravel",
"routes"
],
"support": {
"issues": "https://github.com/tighten/ziggy/issues",
"source": "https://github.com/tighten/ziggy/tree/v1.4.2"
},
"time": "2021-10-01T13:55:26+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "2.2.3",

View File

@@ -62,7 +62,7 @@ return [
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
'model' => App\Models\User::class,
],
// 'users' => [

View File

@@ -7,7 +7,7 @@ return [
|--------------------------------------------------------------------------
*/
'version' => '1.12.0',
'version' => '1.12.2',
/*
|--------------------------------------------------------------------------
@@ -15,7 +15,7 @@ return [
|--------------------------------------------------------------------------
*/
'install' => 'manual',
'install' => 'docker',
/*
|--------------------------------------------------------------------------
@@ -23,7 +23,7 @@ return [
|--------------------------------------------------------------------------
*/
'home' => 'HOME=' . base_path() . DIRECTORY_SEPARATOR,
'home' => 'HOME=/config',
/*
|--------------------------------------------------------------------------

11
docker/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM linuxserver/nginx
LABEL maintainer=henrywhitaker3@outlook.com
ENV arch='x86_64'
COPY docker/conf/ /
COPY . /site
EXPOSE 80 443
VOLUME ["/config"]

11
docker/arm.Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM linuxserver/nginx:arm32v7-latest
LABEL maintainer=henrywhitaker3@outlook.com
ENV arch='arm'
COPY docker/conf/ /
COPY . /site
EXPOSE 80 443
VOLUME ["/config"]

View File

@@ -0,0 +1,10 @@
# do daily/weekly/monthly maintenance
# min hour day month weekday command
*/15 * * * * run-parts /etc/periodic/15min
0 * * * * run-parts /etc/periodic/hourly
0 2 * * * run-parts /etc/periodic/daily
0 3 * * 6 run-parts /etc/periodic/weekly
0 5 1 * * run-parts /etc/periodic/monthly
# speedtest cron
* * * * * php /config/www/artisan schedule:run >> /config/log/speedtest/cron.log
# */5 * * * * php /config/www/artisan queue:retry all >> /config/log/speedtest.cron.log

View File

@@ -0,0 +1,30 @@
server {
listen 80 default_server;
listen 443 ssl;
root /config/www/public;
index index.html index.htm index.php;
server_name _;
ssl_certificate /config/keys/cert.crt;
ssl_certificate_key /config/keys/cert.key;
client_max_body_size 0;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# With php5-cgi alone:
fastcgi_pass 127.0.0.1:9000;
# With php5-fpm:
#fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include /etc/nginx/fastcgi_params;
}
}

View File

@@ -0,0 +1,145 @@
#!/usr/bin/with-contenv bash
# # This script sets up the speedtest app
function eulaError()
{
echo "##################################################################################################################################"
echo "##################################################################################################################################"
echo "You haven't accepted the Ookla EULA. Please re-create the container with the environment variable 'OOKLA_EULA_GDPR' set to 'true'."
echo "##################################################################################################################################"
echo "##################################################################################################################################"
exit 1
}
# Do Ookla stuff
if [ -z ${OOKLA_EULA_GDPR+x} ]; then
eulaError
else
if [ $OOKLA_EULA_GDPR != "true" ]; then
eulaError
fi
if [ ! -f /config/www/app/Bin/speedtest ]; then
echo "Ookla GDPR and EULA accepted. Downloading Speedtest CLI."
cd /tmp
if [ $(uname -m) == "x86_64" ]; then
wget https://install.speedtest.net/app/cli/ookla-speedtest-1.0.0-x86_64-linux.tgz -O speedtest.tgz > /dev/null
else
wget https://install.speedtest.net/app/cli/ookla-speedtest-1.0.0-arm-linux.tgz -O speedtest.tgz > /dev/null
fi
tar zxvf speedtest.tgz > /dev/null
cp speedtest /site/app/Bin/
chmod +x /site/artisan
HOME=/config && s6-setuidgid abc /site/app/Bin/speedtest --accept-license --accept-gdpr > /dev/null
HOME=/root
else
HOME=/config && s6-setuidgid abc /site/app/Bin/speedtest --accept-license --accept-gdpr > /dev/null
HOME=/root
fi
fi
# Copy site files to /config
echo "Copying latest site files to config"
cp -rfT /site/ /config/www/
# Check for DB
if [ ! -f /config/speed.db ]; then
echo "Database file not found! Creating empty database"
touch /config/speed.db
else
echo "Database file exists"
chown abc:abc /config/speed.db
fi
# Check for .env
if [ ! -f /config/www/.env ]; then
echo "Env file not found! Creating .env file"
cp /site/.env.example /config/www/.env
else
echo "Env file exists"
fi
if [ ! -f /config/www/.composer-time ]; then
echo 'Removing old packages'
rm -rf /config/www/vendor/
fi
echo 'Updating packages'
apk add composer
cd /config/www && composer install && echo date > /config/www/.composer-time
sed "s,DB_DATABASE=.*,DB_DATABASE=/config/speed.db," -i.bak /config/www/.env
echo "Running database migrations"
php /config/www/artisan migrate
# Check app key exists
if grep -E "APP_KEY=[0-9A-Za-z:+\/=]{1,}" /config/www/.env > /dev/null; then
echo "App key exists"
else
echo "Generating app key"
php /config/www/artisan key:generate
fi
# Check JWT secret exists
if grep -E "JWT_SECRET=[0-9A-Za-z:+\/=]{1,}" /config/www/.env > /dev/null ; then
echo "JWT secret exists"
else
echo "Generating JWT secret"
php /config/www/artisan jwt:secret
fi
if [ -z ${SLACK_WEBHOOK+x} ]; then
echo "Slack webhook is unset"
sed "s,SLACK_WEBHOOK=.*,SLACK_WEBHOOK=," -i.bak /config/www/.env
else
echo "Slack webhook set, updating db"
sed "s,SLACK_WEBHOOK=.*,SLACK_WEBHOOK=$SLACK_WEBHOOK," -i.bak /config/www/.env
php /config/www/artisan speedtest:slack $SLACK_WEBHOOK
fi
if [ -z ${TELEGRAM_BOT_TOKEN+x} ] && [ -z ${TELEGRAM_CHAT_ID+x} ]; then
echo "Telegram chat id and bot token unset"
sed "s,TELEGRAM_BOT_TOKEN=.*,TELEGRAM_BOT_TOKEN=," -i.bak /config/www/.env
sed "s,TELEGRAM_CHAT_ID=.*,TELEGRAM_CHAT_ID=," -i.bak /config/www/.env
else
echo "Telegram chat id and bot token set, updating .env"
sed "s,TELEGRAM_BOT_TOKEN=.*,TELEGRAM_BOT_TOKEN=$TELEGRAM_BOT_TOKEN," -i.bak /config/www/.env
sed "s,TELEGRAM_CHAT_ID=.*,TELEGRAM_CHAT_ID=$TELEGRAM_CHAT_ID," -i.bak /config/www/.env
php /config/www/artisan speedtest:telegram --chat=$TELEGRAM_CHAT_ID --bot=$TELEGRAM_BOT_TOKEN
fi
if [ -z ${BASE_PATH+x} ]; then
echo "Base path is unset"
sed "s,BASE_PATH=.*,BASE_PATH=," -i.bak /config/www/.env
else
echo "Base path set, updating .env"
sed "s,BASE_PATH=.*,BASE_PATH=$BASE_PATH," -i.bak /config/www/.env
fi
if [ -z ${AUTH+x} ]; then
echo "AUTH variable not set. Disabling authentication"
php /config/www/artisan speedtest:auth --disable
else
if [ $AUTH == 'true' ]; then
echo "AUTH variable set. Enabling authentication"
php /config/www/artisan speedtest:auth --enable
else
echo "AUTH variable set, but not to 'true'. Disabling authentication"
php /config/www/artisan speedtest:auth --disable
fi
fi
echo "Clearing old jobs from queue"
php /config/www/artisan queue:clear
mkdir -p /config/log/speedtest
cp /defaults/crontab /etc/crontabs/root
chown -R abc:abc /config
chmod +x /config/www/app/Bin/speedtest
chmod -R 777 /config/www/storage/clockwork

View File

@@ -0,0 +1,439 @@
; Start a new pool named 'www'.
; the variable $pool can be used in any directive and will be replaced by the
; pool name ('www' here)
[www]
; Per pool prefix
; It only applies on the following directives:
; - 'access.log'
; - 'slowlog'
; - 'listen' (unixsocket)
; - 'chroot'
; - 'chdir'
; - 'php_values'
; - 'php_admin_values'
; When not set, the global prefix (or /usr) applies instead.
; Note: This directive can also be relative to the global prefix.
; Default Value: none
;prefix = /path/to/pools/$pool
; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
; will be used.
user = abc
group = abc
; The address on which to accept FastCGI requests.
; Valid syntaxes are:
; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on
; a specific port;
; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
; a specific port;
; 'port' - to listen on a TCP socket to all addresses
; (IPv6 and IPv4-mapped) on a specific port;
; '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = 127.0.0.1:9000
; Set listen(2) backlog.
; Default Value: 511 (-1 on FreeBSD and OpenBSD)
;listen.backlog = 511
; Set permissions for unix socket, if one is used. In Linux, read/write
; permissions must be set in order to allow connections from a web server. Many
; BSD-derived systems allow connections regardless of permissions. The owner
; and group can be specified either by name or by their numeric IDs.
; Default Values: user and group are set as the running user
; mode is set to 0660
;listen.owner = nobody
;listen.group = abc
;listen.mode = 0660
; When POSIX Access Control Lists are supported you can set them using
; these options, value is a comma separated list of user/group names.
; When set, listen.owner and listen.group are ignored
;listen.acl_users =
;listen.acl_groups =
; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect.
; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original
; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address
; must be separated by a comma. If this value is left blank, connections will be
; accepted from any ip address.
; Default Value: any
;listen.allowed_clients = 127.0.0.1
; Specify the nice(2) priority to apply to the pool processes (only if set)
; The value can vary from -19 (highest priority) to 20 (lower priority)
; Note: - It will only work if the FPM master process is launched as root
; - The pool processes will inherit the master process priority
; unless it specified otherwise
; Default Value: no set
; process.priority = -19
; Set the process dumpable flag (PR_SET_DUMPABLE prctl) even if the process user
; or group is differrent than the master process user. It allows to create process
; core dump and ptrace the process for the pool user.
; Default Value: no
; process.dumpable = yes
; Choose how the process manager will control the number of child processes.
; Possible Values:
; static - a fixed number (pm.max_children) of child processes;
; dynamic - the number of child processes are set dynamically based on the
; following directives. With this process management, there will be
; always at least 1 children.
; pm.max_children - the maximum number of children that can
; be alive at the same time.
; pm.start_servers - the number of children created on startup.
; pm.min_spare_servers - the minimum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is less than this
; number then some children will be created.
; pm.max_spare_servers - the maximum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is greater than this
; number then some children will be killed.
; ondemand - no children are created at startup. Children will be forked when
; new requests will connect. The following parameter are used:
; pm.max_children - the maximum number of children that
; can be alive at the same time.
; pm.process_idle_timeout - The number of seconds after which
; an idle process will be killed.
; Note: This value is mandatory.
pm = dynamic
; The number of child processes to be created when pm is set to 'static' and the
; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
; This value sets the limit on the number of simultaneous requests that will be
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
; CGI. The below defaults are based on a server without much resources. Don't
; forget to tweak pm.* to fit your needs.
; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand'
; Note: This value is mandatory.
pm.max_children = 5
; The number of child processes created on startup.
; Note: Used only when pm is set to 'dynamic'
; Default Value: (min_spare_servers + max_spare_servers) / 2
pm.start_servers = 2
; The desired minimum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
pm.min_spare_servers = 1
; The desired maximum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
pm.max_spare_servers = 3
; The number of seconds after which an idle process will be killed.
; Note: Used only when pm is set to 'ondemand'
; Default Value: 10s
;pm.process_idle_timeout = 10s;
; The number of requests each child process should execute before respawning.
; This can be useful to work around memory leaks in 3rd party libraries. For
; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS.
; Default Value: 0
;pm.max_requests = 500
; The URI to view the FPM status page. If this value is not set, no URI will be
; recognized as a status page. It shows the following informations:
; pool - the name of the pool;
; process manager - static, dynamic or ondemand;
; start time - the date and time FPM has started;
; start since - number of seconds since FPM has started;
; accepted conn - the number of request accepted by the pool;
; listen queue - the number of request in the queue of pending
; connections (see backlog in listen(2));
; max listen queue - the maximum number of requests in the queue
; of pending connections since FPM has started;
; listen queue len - the size of the socket queue of pending connections;
; idle processes - the number of idle processes;
; active processes - the number of active processes;
; total processes - the number of idle + active processes;
; max active processes - the maximum number of active processes since FPM
; has started;
; max children reached - number of times, the process limit has been reached,
; when pm tries to start more children (works only for
; pm 'dynamic' and 'ondemand');
; Value are updated in real time.
; Example output:
; pool: www
; process manager: static
; start time: 01/Jul/2011:17:53:49 +0200
; start since: 62636
; accepted conn: 190460
; listen queue: 0
; max listen queue: 1
; listen queue len: 42
; idle processes: 4
; active processes: 11
; total processes: 15
; max active processes: 12
; max children reached: 0
;
; By default the status page output is formatted as text/plain. Passing either
; 'html', 'xml' or 'json' in the query string will return the corresponding
; output syntax. Example:
; http://www.foo.bar/status
; http://www.foo.bar/status?json
; http://www.foo.bar/status?html
; http://www.foo.bar/status?xml
;
; By default the status page only outputs short status. Passing 'full' in the
; query string will also return status for each pool process.
; Example:
; http://www.foo.bar/status?full
; http://www.foo.bar/status?json&full
; http://www.foo.bar/status?html&full
; http://www.foo.bar/status?xml&full
; The Full status returns for each process:
; pid - the PID of the process;
; state - the state of the process (Idle, Running, ...);
; start time - the date and time the process has started;
; start since - the number of seconds since the process has started;
; requests - the number of requests the process has served;
; request duration - the duration in µs of the requests;
; request method - the request method (GET, POST, ...);
; request URI - the request URI with the query string;
; content length - the content length of the request (only with POST);
; user - the user (PHP_AUTH_USER) (or '-' if not set);
; script - the main script called (or '-' if not set);
; last request cpu - the %cpu the last request consumed
; it's always 0 if the process is not in Idle state
; because CPU calculation is done when the request
; processing has terminated;
; last request memory - the max amount of memory the last request consumed
; it's always 0 if the process is not in Idle state
; because memory calculation is done when the request
; processing has terminated;
; If the process is in Idle state, then informations are related to the
; last request the process has served. Otherwise informations are related to
; the current request being served.
; Example output:
; ************************
; pid: 31330
; state: Running
; start time: 01/Jul/2011:17:53:49 +0200
; start since: 63087
; requests: 12808
; request duration: 1250261
; request method: GET
; request URI: /test_mem.php?N=10000
; content length: 0
; user: -
; script: /home/fat/web/docs/php/test_mem.php
; last request cpu: 0.00
; last request memory: 0
;
; Note: There is a real-time FPM status monitoring sample web page available
; It's available in: /usr/share/php7/fpm/status.html
;
; Note: The value must start with a leading slash (/). The value can be
; anything, but it may not be a good idea to use the .php extension or it
; may conflict with a real PHP file.
; Default Value: not set
;pm.status_path = /status
; The ping URI to call the monitoring page of FPM. If this value is not set, no
; URI will be recognized as a ping page. This could be used to test from outside
; that FPM is alive and responding, or to
; - create a graph of FPM availability (rrd or such);
; - remove a server from a group if it is not responding (load balancing);
; - trigger alerts for the operating team (24/7).
; Note: The value must start with a leading slash (/). The value can be
; anything, but it may not be a good idea to use the .php extension or it
; may conflict with a real PHP file.
; Default Value: not set
;ping.path = /ping
; This directive may be used to customize the response of a ping request. The
; response is formatted as text/plain with a 200 response code.
; Default Value: pong
;ping.response = pong
; The access log file
; Default: not set
;access.log = log/php7/$pool.access.log
; The access log format.
; The following syntax is allowed
; %%: the '%' character
; %C: %CPU used by the request
; it can accept the following format:
; - %{user}C for user CPU only
; - %{system}C for system CPU only
; - %{total}C for user + system CPU (default)
; %d: time taken to serve the request
; it can accept the following format:
; - %{seconds}d (default)
; - %{miliseconds}d
; - %{mili}d
; - %{microseconds}d
; - %{micro}d
; %e: an environment variable (same as $_ENV or $_SERVER)
; it must be associated with embraces to specify the name of the env
; variable. Some exemples:
; - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e
; - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e
; %f: script filename
; %l: content-length of the request (for POST request only)
; %m: request method
; %M: peak of memory allocated by PHP
; it can accept the following format:
; - %{bytes}M (default)
; - %{kilobytes}M
; - %{kilo}M
; - %{megabytes}M
; - %{mega}M
; %n: pool name
; %o: output header
; it must be associated with embraces to specify the name of the header:
; - %{Content-Type}o
; - %{X-Powered-By}o
; - %{Transfert-Encoding}o
; - ....
; %p: PID of the child that serviced the request
; %P: PID of the parent of the child that serviced the request
; %q: the query string
; %Q: the '?' character if query string exists
; %r: the request URI (without the query string, see %q and %Q)
; %R: remote IP address
; %s: status (response code)
; %t: server time the request was received
; it can accept a strftime(3) format:
; %d/%b/%Y:%H:%M:%S %z (default)
; The strftime(3) format must be encapsuled in a %{<strftime_format>}t tag
; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t
; %T: time the log has been written (the request has finished)
; it can accept a strftime(3) format:
; %d/%b/%Y:%H:%M:%S %z (default)
; The strftime(3) format must be encapsuled in a %{<strftime_format>}t tag
; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t
; %u: remote user
;
; Default: "%R - %u %t \"%m %r\" %s"
;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"
; The log file for slow requests
; Default Value: not set
; Note: slowlog is mandatory if request_slowlog_timeout is set
;slowlog = log/php7/$pool.slow.log
; The timeout for serving a single request after which a PHP backtrace will be
; dumped to the 'slowlog' file. A value of '0s' means 'off'.
; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
; Default Value: 0
;request_slowlog_timeout = 0
; Depth of slow log stack trace.
; Default Value: 20
;request_slowlog_trace_depth = 20
; The timeout for serving a single request after which the worker process will
; be killed. This option should be used when the 'max_execution_time' ini option
; does not stop script execution for some reason. A value of '0' means 'off'.
; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
; Default Value: 0
;request_terminate_timeout = 0
; The timeout set by 'request_terminate_timeout' ini option is not engaged after
; application calls 'fastcgi_finish_request' or when application has finished and
; shutdown functions are being called (registered via register_shutdown_function).
; This option will enable timeout limit to be applied unconditionally
; even in such cases.
; Default Value: no
;request_terminate_timeout_track_finished = no
; Set open file descriptor rlimit.
; Default Value: system defined value
;rlimit_files = 1024
; Set max core size rlimit.
; Possible Values: 'unlimited' or an integer greater or equal to 0
; Default Value: system defined value
;rlimit_core = 0
; Chroot to this directory at the start. This value must be defined as an
; absolute path. When this value is not set, chroot is not used.
; Note: you can prefix with '$prefix' to chroot to the pool prefix or one
; of its subdirectories. If the pool prefix is not set, the global prefix
; will be used instead.
; Note: chrooting is a great security feature and should be used whenever
; possible. However, all PHP paths will be relative to the chroot
; (error_log, sessions.save_path, ...).
; Default Value: not set
;chroot =
; Chdir to this directory at the start.
; Note: relative path can be used.
; Default Value: current directory or / when chroot
;chdir = /var/www
; Redirect worker stdout and stderr into main error log. If not set, stdout and
; stderr will be redirected to /dev/null according to FastCGI specs.
; Note: on highloaded environement, this can cause some delay in the page
; process time (several ms).
; Default Value: no
;catch_workers_output = yes
; Decorate worker output with prefix and suffix containing information about
; the child that writes to the log and if stdout or stderr is used as well as
; log level and time. This options is used only if catch_workers_output is yes.
; Settings to "no" will output data as written to the stdout or stderr.
; Default value: yes
;decorate_workers_output = no
; Clear environment in FPM workers
; Prevents arbitrary environment variables from reaching FPM worker processes
; by clearing the environment in workers before env vars specified in this
; pool configuration are added.
; Setting to "no" will make all environment variables available to PHP code
; via getenv(), $_ENV and $_SERVER.
; Default Value: yes
clear_env = no
; Limits the extensions of the main script FPM will allow to parse. This can
; prevent configuration mistakes on the web server side. You should only limit
; FPM to .php extensions to prevent malicious users to use other extensions to
; execute php code.
; Note: set an empty value to allow all extensions.
; Default Value: .php
;security.limit_extensions = .php .php3 .php4 .php5 .php7
; Pass environment variables like LD_LIBRARY_PATH. All $VARIABLEs are taken from
; the current environment.
; Default Value: clean env
;env[HOSTNAME] = $HOSTNAME
;env[PATH] = /usr/local/bin:/usr/bin:/bin
;env[TMP] = /tmp
;env[TMPDIR] = /tmp
;env[TEMP] = /tmp
; Additional php.ini defines, specific to this pool of workers. These settings
; overwrite the values previously defined in the php.ini. The directives are the
; same as the PHP SAPI:
; php_value/php_flag - you can set classic ini defines which can
; be overwritten from PHP call 'ini_set'.
; php_admin_value/php_admin_flag - these directives won't be overwritten by
; PHP call 'ini_set'
; For php_*flag, valid values are on, off, 1, 0, true, false, yes or no.
; Defining 'extension' will load the corresponding shared extension from
; extension_dir. Defining 'disable_functions' or 'disable_classes' will not
; overwrite previously defined php.ini values, but will append the new value
; instead.
; Note: path INI options can be relative and will be expanded with the prefix
; (pool, global or /usr)
; Default Value: nothing is defined by default except the values in php.ini and
; specified at startup with the -d argument
;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com
;php_flag[display_errors] = off
;php_admin_value[error_log] = /var/log/php7/$pool.error.log
;php_admin_flag[log_errors] = on
;php_admin_value[memory_limit] = 32M

View File

@@ -0,0 +1,3 @@
#!/usr/bin/with-contenv bash
exec s6-setuidgid abc php /config/www/artisan queue:work --timeout=120 >> /config/log/speedtest/queue.log

23621
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,38 +2,32 @@
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "npm run development -- --watch",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
"production": "mix --production"
},
"devDependencies": {
"@babel/preset-react": "^7.12.13",
"autoprefixer": "^10.4.0",
"axios": "^0.21",
"bootstrap": "^4.6.0",
"cross-env": "^7.0",
"jquery": "^3.5",
"laravel-mix": "^5.0.9",
"laravel-mix": "^6.0.27",
"lodash": "^4.17.21",
"popper.js": "^1.12",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"resolve-url-loader": "^3.1.2",
"resolve-url-loader": "^4.0.0",
"sass": "^1.32.8",
"sass-loader": "^10.1.1"
"sass-loader": "^10.1.1",
"tailwindcss": "^2.0.1",
"vue": "^2.5.17",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.10"
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.13.0",
"chart.js": "^2.9.4",
"csv-file-validator": "^1.10.1",
"js-cookie": "^2.2.1",
"react-beautiful-dnd": "^13.1.0",
"react-bootstrap": "^1.5.1",
"react-chartjs-2": "^2.11.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-toastify": "^7.0.3"
"@inertiajs/inertia": "^0.10.1",
"@inertiajs/inertia-vue": "^0.7.2",
"@inertiajs/progress": "^0.2.6",
"dayjs": "^1.10.7",
"laravel-mix-tailwind": "^0.1.2"
}
}

198498
public/css/app.css vendored

File diff suppressed because it is too large Load Diff

146541
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,138 +0,0 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/* @license
Papa Parse
v5.2.0
https://github.com/mholt/PapaParse
License: MIT
*/
/*!
* Bootstrap v4.5.3 (https://getbootstrap.com/)
* Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
/*!
Copyright (c) 2017 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*!
* Chart.js v2.9.4
* https://www.chartjs.org
* (c) 2020 Chart.js Contributors
* Released under the MIT License
*/
/*!
* JavaScript Cookie v2.2.1
* https://github.com/js-cookie/js-cookie
*
* Copyright 2006, 2015 Klaus Hartl & Fagner Brack
* Released under the MIT license
*/
/*!
* Sizzle CSS Selector Engine v2.3.5
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://js.foundation/
*
* Date: 2020-03-14
*/
/*!
* jQuery JavaScript Library v3.5.1
* https://jquery.com/
*
* Includes Sizzle.js
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2020-05-04T22:49Z
*/
/**
* @license
* Lodash <https://lodash.com/>
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/** @license React v0.19.1
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.14.0
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.14.0
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**!
* @fileOverview Kickass library to create and place poppers near their reference elements.
* @version 1.16.1
* @license
* Copyright (c) 2016 Federico Zivolo and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
//! moment.js
//! moment.js locale configuration

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

227
public/js/resources_js_Pages_Home_vue.js vendored Normal file
View File

@@ -0,0 +1,227 @@
"use strict";
(self["webpackChunk"] = self["webpackChunk"] || []).push([["resources_js_Pages_Home_vue"],{
/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/Pages/Home.vue?vue&type=script&lang=js&":
/*!******************************************************************************************************************************************************************************************************!*\
!*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/Pages/Home.vue?vue&type=script&lang=js& ***!
\******************************************************************************************************************************************************************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
//
//
//
//
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({});
/***/ }),
/***/ "./resources/js/Pages/Home.vue":
/*!*************************************!*\
!*** ./resources/js/Pages/Home.vue ***!
\*************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var _Home_vue_vue_type_template_id_6a63e488___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./Home.vue?vue&type=template&id=6a63e488& */ "./resources/js/Pages/Home.vue?vue&type=template&id=6a63e488&");
/* harmony import */ var _Home_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Home.vue?vue&type=script&lang=js& */ "./resources/js/Pages/Home.vue?vue&type=script&lang=js&");
/* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! !../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ "./node_modules/vue-loader/lib/runtime/componentNormalizer.js");
/* normalize component */
;
var component = (0,_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__["default"])(
_Home_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__["default"],
_Home_vue_vue_type_template_id_6a63e488___WEBPACK_IMPORTED_MODULE_0__.render,
_Home_vue_vue_type_template_id_6a63e488___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns,
false,
null,
null,
null
)
/* hot reload */
if (false) { var api; }
component.options.__file = "resources/js/Pages/Home.vue"
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (component.exports);
/***/ }),
/***/ "./resources/js/Pages/Home.vue?vue&type=script&lang=js&":
/*!**************************************************************!*\
!*** ./resources/js/Pages/Home.vue?vue&type=script&lang=js& ***!
\**************************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_5_0_rules_0_use_0_node_modules_vue_loader_lib_index_js_vue_loader_options_Home_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Home.vue?vue&type=script&lang=js& */ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/Pages/Home.vue?vue&type=script&lang=js&");
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_node_modules_babel_loader_lib_index_js_clonedRuleSet_5_0_rules_0_use_0_node_modules_vue_loader_lib_index_js_vue_loader_options_Home_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__["default"]);
/***/ }),
/***/ "./resources/js/Pages/Home.vue?vue&type=template&id=6a63e488&":
/*!********************************************************************!*\
!*** ./resources/js/Pages/Home.vue?vue&type=template&id=6a63e488& ***!
\********************************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "render": () => (/* reexport safe */ _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_Home_vue_vue_type_template_id_6a63e488___WEBPACK_IMPORTED_MODULE_0__.render),
/* harmony export */ "staticRenderFns": () => (/* reexport safe */ _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_Home_vue_vue_type_template_id_6a63e488___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns)
/* harmony export */ });
/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_Home_vue_vue_type_template_id_6a63e488___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Home.vue?vue&type=template&id=6a63e488& */ "./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/Pages/Home.vue?vue&type=template&id=6a63e488&");
/***/ }),
/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/Pages/Home.vue?vue&type=template&id=6a63e488&":
/*!***********************************************************************************************************************************************************************************************************!*\
!*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/Pages/Home.vue?vue&type=template&id=6a63e488& ***!
\***********************************************************************************************************************************************************************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "render": () => (/* binding */ render),
/* harmony export */ "staticRenderFns": () => (/* binding */ staticRenderFns)
/* harmony export */ });
var render = function () {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", [_vm._v("Hello")])
}
var staticRenderFns = []
render._withStripped = true
/***/ }),
/***/ "./node_modules/vue-loader/lib/runtime/componentNormalizer.js":
/*!********************************************************************!*\
!*** ./node_modules/vue-loader/lib/runtime/componentNormalizer.js ***!
\********************************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ normalizeComponent)
/* harmony export */ });
/* globals __VUE_SSR_CONTEXT__ */
// IMPORTANT: Do NOT use ES2015 features in this file (except for modules).
// This module is a runtime utility for cleaner component module output and will
// be included in the final webpack user bundle.
function normalizeComponent (
scriptExports,
render,
staticRenderFns,
functionalTemplate,
injectStyles,
scopeId,
moduleIdentifier, /* server only */
shadowMode /* vue-cli only */
) {
// Vue.extend constructor export interop
var options = typeof scriptExports === 'function'
? scriptExports.options
: scriptExports
// render functions
if (render) {
options.render = render
options.staticRenderFns = staticRenderFns
options._compiled = true
}
// functional template
if (functionalTemplate) {
options.functional = true
}
// scopedId
if (scopeId) {
options._scopeId = 'data-v-' + scopeId
}
var hook
if (moduleIdentifier) { // server build
hook = function (context) {
// 2.3 injection
context =
context || // cached call
(this.$vnode && this.$vnode.ssrContext) || // stateful
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
// 2.2 with runInNewContext: true
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
context = __VUE_SSR_CONTEXT__
}
// inject component styles
if (injectStyles) {
injectStyles.call(this, context)
}
// register component module identifier for async chunk inferrence
if (context && context._registeredComponents) {
context._registeredComponents.add(moduleIdentifier)
}
}
// used by ssr in case component is cached and beforeCreate
// never gets called
options._ssrRegister = hook
} else if (injectStyles) {
hook = shadowMode
? function () {
injectStyles.call(
this,
(options.functional ? this.parent : this).$root.$options.shadowRoot
)
}
: injectStyles
}
if (hook) {
if (options.functional) {
// for template-only hot-reload because in that case the render fn doesn't
// go through the normalizer
options._injectStyles = hook
// register for functional component in vue file
var originalRender = options.render
options.render = function renderWithStyleInjection (h, context) {
hook.call(context)
return originalRender(h, context)
}
} else {
// inject component registration as beforeCreate hook
var existing = options.beforeCreate
options.beforeCreate = existing
? [].concat(existing, hook)
: [hook]
}
}
return {
exports: scriptExports,
options: options
}
}
/***/ })
}]);

View File

@@ -1,4 +1,4 @@
{
"/js/app.js": "/js/app.js",
"/css/app.css": "/css/app.css"
"/js/app.js": "/js/app.js?id=3f160e1be04f20c83775",
"/css/app.css": "/css/app.css?id=56fcadeb3d7b8e0e21d6"
}

View File

@@ -0,0 +1,12 @@
<template>
<div>
Hello
<!-- <div>{{ relativeDate('2021-10-15 00:00:00') }}</div> -->
</div>
</template>
<script>
export default {
}
</script>

32
resources/js/app.js vendored
View File

@@ -1,15 +1,23 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes React and other helpers. It's a great starting point while
* building robust, powerful web applications using React + Laravel.
*/
require("./bootstrap");
require('./bootstrap');
// Import modules...
import Vue from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue';
/**
* Next, we will create a fresh React component instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
import { InertiaProgress } from "@inertiajs/progress";
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
require('./index');
Vue.use(InertiaProgress);
createInertiaApp({
resolve: name => require(`./Pages/${name}`),
setup({ el, App, props }) {
new Vue({
render: h => h(App, props),
}).$mount(el)
},
});
InertiaProgress.init({ color: "#4B5563" });

View File

@@ -1,18 +1,5 @@
window._ = require('lodash');
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
try {
window.Popper = require('popper.js').default;
window.$ = window.jQuery = require('jquery');
require('bootstrap');
} catch (e) {}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
@@ -37,5 +24,5 @@ window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// encrypted: true
// forceTLS: true
// });

View File

@@ -1,70 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Row, Col} from 'react-bootstrap';
import SessionsTable from './SessionsTable';
import ResetPassword from './ResetPassword';
export default class Authentication extends Component {
constructor(props) {
super(props)
this.state = {
showCollapse: false,
showModal: false
}
}
toggleCollapse = () => {
if(this.state.showCollapse) {
this.setState({
showCollapse: false
});
} else {
this.setState({
showCollapse: true
});
}
}
toggleModal = () => {
if(this.state.showModal) {
this.setState({
showModal: false
});
} else {
this.setState({
showModal: true
});
}
}
render() {
var showCollapse = this.state.showCollapse;
var showModal = this.state.showModal;
if( (window.config.auth == true && window.authenticated == true)) {
return (
<div>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<ResetPassword />
</Col>
</Row>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<SessionsTable />
</Col>
</Row>
</div>
);
} else {
return (
<></>
);
}
}
}
if (document.getElementById('Authentication')) {
ReactDOM.render(<Authentication />, document.getElementById('Authentication'));
}

View File

@@ -1,111 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Container, Row, Col, Collapse, Button, Modal, Form } from 'react-bootstrap';
import SessionsTable from './SessionsTable';
import Axios from 'axios';
import { toast } from 'react-toastify';
export default class ResetPassword extends Component {
constructor(props) {
super(props)
this.state = {
showModal: false,
currentPassword: '',
newPassword: '',
newPasswordConfirmation: '',
logoutDevices: false
}
}
toggleModal = () => {
if(this.state.showModal) {
this.setState({
showModal: false
});
} else {
this.setState({
showModal: true
});
}
}
updateTextField = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
updateCheckbox = (e) => {
this.setState({
[e.target.id]: e.target.checked
});
}
changePassword = (e) => {
e.preventDefault();
var data = {
currentPassword: this.state.currentPassword,
newPassword: this.state.newPassword,
newPassword_confirmation: this.state.newPasswordConfirmation,
logoutDevices: this.state.logoutDevices
}
var url = 'api/auth/change-password?token=' + window.token;
Axios.post(url, data)
.then((resp) => {
toast.success('Password updated');
this.toggleModal();
if(this.state.logoutDevices == true) {
location.reload(true);
}
})
.catch((err) => {
if(err.response) {
for(var key in err.response.data.error) {
toast.error(err.response.data.error[key][0]);
}
}
})
}
render() {
var showModal = this.state.showModal;
return (
<div>
<Button variant="primary" onClick={this.toggleModal} className="mb-3">Change password</Button>
<Modal show={showModal} onHide={this.toggleModal}>
<Modal.Header closeButton>
<Modal.Title>Change password</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={this.changePassword}>
<Form.Group controlId="currentPassword">
<Form.Label>Current password</Form.Label>
<Form.Control type="password" onInput={this.updateTextField} required />
</Form.Group>
<Form.Group controlId="newPassword">
<Form.Label>New Password</Form.Label>
<Form.Control type="password" onInput={this.updateTextField} required />
</Form.Group>
<Form.Group controlId="newPasswordConfirmation">
<Form.Label>Confirm New Password</Form.Label>
<Form.Control type="password" onInput={this.updateTextField} required />
</Form.Group>
<Button variant="primary" type="submit" className="d-inline-block">Change password</Button>
<Form.Group controlId="logoutDevices" className="d-inline-block ml-2">
<Form.Check type="checkbox" label="Log everywhere out" onInput={this.updateCheckbox} />
</Form.Group>
</Form>
</Modal.Body>
</Modal>
</div>
);
}
}
if (document.getElementById('ResetPassword')) {
ReactDOM.render(<ResetPassword />, document.getElementById('ResetPassword'));
}

View File

@@ -1,67 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Container, Row, Col, Table } from 'react-bootstrap';
import Axios from 'axios';
export default class SessionsTable extends Component {
constructor(props) {
super(props)
this.state = {
sessions: []
}
}
componentDidMount() {
this.getSessions();
}
getSessions = () => {
var url = 'api/auth/sessions?token=' + window.token;
Axios.get(url)
.then((resp) => {
this.setState({
sessions: resp.data.response
})
})
}
render() {
var sessions = this.state.sessions;
return (
<Container className="mb-4">
<Row>
<Col sm={{ span: 12 }} className="mb-3 text-center">
<h5>Login Sessions</h5>
<Table responsive>
<thead>
<tr>
<th>IP</th>
<th>Expires</th>
<th>Created at</th>
</tr>
</thead>
<tbody>
{sessions.map((e,i) => {
return(
<tr key={i}>
<td>{e.ip}</td>
<td>{new Date(e.expires * 1000).toLocaleDateString() + ' ' + new Date(e.expires * 1000).toLocaleTimeString()}</td>
<td>{e.created_at}</td>
</tr>
)
})}
</tbody>
</Table>
</Col>
</Row>
</Container>
);
}
}
if (document.getElementById('SessionsTable')) {
ReactDOM.render(<SessionsTable />, document.getElementById('SessionsTable'));
}

View File

@@ -1,42 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Button, Dropdown, DropdownButton } from 'react-bootstrap';
import { toast } from 'react-toastify';
import Axios from 'axios';
export default class Backup extends Component {
backup = (format) => {
var url = 'api/backup?format=' + format + '&token=' + window.token;
toast.info('Your backup has started downloading...');
Axios.get(url, {
responseType: 'blob'
})
.then((resp) => {
var a = document.createElement('a');
a.href = url;
a.download = "";
document.body.appendChild(a);
a.click();
a.remove();
toast.success('Backup downloaded');
})
.catch((err) => {
console.log(err);
})
}
render() {
return (
<DropdownButton title="Backup" variant="primary" className="m-2 d-inline-block">
<Dropdown.Item href="#" onClick={() => { this.backup('json') }}>JSON</Dropdown.Item>
<Dropdown.Item href="#" onClick={() => { this.backup('csv') }}>CSV</Dropdown.Item>
</DropdownButton>
);
}
}
if (document.getElementById('Backup')) {
ReactDOM.render(<Backup />, document.getElementById('Backup'));
}

View File

@@ -1,140 +0,0 @@
import React, { Component, version } from 'react';
import ReactDOM from 'react-dom';
import Axios from 'axios';
import { Modal, Collapse, Button } from 'react-bootstrap';
export default class Changelog extends Component {
constructor(props) {
super(props)
this.state = {
changelog: {},
modal: false,
loading: true,
hidden: false,
}
}
componentDidMount = () => {
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
this.getChangelog();
}
}
getChangelog = () => {
Axios.get('api/update/changelog?token=' + window.token)
.then((resp) => {
this.setState({
changelog: resp.data.data,
loading: false
});
})
}
showModal = () => {
this.setState({
modal: true,
});
}
hideModal = () => {
this.setState({
modal: false,
});
}
toggleHidden = () => {
var hidden = this.state.hidden;
if(hidden) {
this.setState({
hidden: false
});
} else {
this.setState({
hidden: true
});
}
}
versionList = (key, data) => {
return (
<div key={key}>
<h5>Version: {key}</h5>
<ul>
{data.map((e,i) => {
if(e.link == '') {
return <li key={key.split('.').join() + i}>{e.description}</li>
} else {
return <li key={key + i}><a href={e.link} target="_blank" rel="noopener noreferer">{e.description}</a></li>
}
})}
</ul>
</div>
);
}
makeChangelog() {
var changelog = this.state.changelog;
var versionsVis = [];
var versionsHid = [];
var i = 0;
for(var key in changelog) {
if(i <= 5) {
versionsVis.push(this.versionList(key, changelog[key]));
} else {
versionsHid.push(this.versionList(key, changelog[key]));
}
i++;
}
return {
visible: versionsVis,
hidden: versionsHid
};
}
render() {
var show = this.state.modal;
var loading = this.state.loading;
var showHidden = this.state.hidden;
if(loading) {
return <></>
} else {
var changelog = this.makeChangelog();
return (
<div className="text-muted ml-1 d-inline-block">
<i className="ti-link mouse" onClick={this.showModal} />
<Modal show={show} onHide={this.hideModal} animation={true}>
<Modal.Body>
<h3>Changelog:</h3>
{changelog.visible}
{changelog.hidden.length > 5 &&
<>
<Collapse in={showHidden}>
<div>
{changelog.hidden}
</div>
</Collapse>
<div className="w-100 text-center">
{showHidden ?
<Button variant="primary" className="mx-auto mouse" onClick={this.toggleHidden}>Show less</Button>
:
<Button variant="primary" className="mx-auto mouse" onClick={this.toggleHidden}>Show more</Button>
}
</div>
</>
}
</Modal.Body>
</Modal>
</div>
);
}
}
}
if (document.getElementById('Changelog')) {
ReactDOM.render(<Changelog />, document.getElementById('Changelog'));
}

View File

@@ -1,37 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Container } from 'react-bootstrap';
import { Row } from 'react-bootstrap';
import { Col } from 'react-bootstrap';
import Backup from './Backup';
import Restore from './Restore';
export default class DataRow extends Component {
render() {
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
return (
<Container className="mb-4">
<Row>
<Col sm={{ span: 12 }} className="text-center">
<p>Use these buttons to backup/restore your data</p>
</Col>
</Row>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<Backup />
<Restore />
</Col>
</Row>
</Container>
);
} else {
return (
<></>
)
}
}
}
if (document.getElementById('DataRow')) {
ReactDOM.render(<DataRow />, document.getElementById('DataRow'));
}

View File

@@ -1,209 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Button, Modal, Form, Tooltip, OverlayTrigger, Dropdown, DropdownButton } from 'react-bootstrap';
import { toast } from 'react-toastify';
import Axios from 'axios';
import CSVFileValidator from 'csv-file-validator';
export default class Restore extends Component {
constructor(props) {
super(props);
this.state = {
show: false,
data: null,
uploadReady: false,
filename: 'Upload your backup',
format: 'json'
};
}
showModal = (format) => {
this.setState({
show: true,
format: format
});
}
hideModal = () => {
this.setState({
show: false
});
}
readFile = (e, format) => {
var file = e.target.files[0];
var reader = new FileReader();
reader.readAsText(file, 'UTF-8');
reader.onload = function(evt) {
var data = evt.target.result.trim();
if(format == 'csv') {
var csv = data.substr(45);
var config = {
headers: [
{
name: "id",
inputName: 'id',
required: false,
},
{
name: "ping",
inputName: 'ping',
required: true,
requiredError: function (headerName, rowNumber, columnNumber) {
return `${headerName} is required in the ${rowNumber} row / ${columnNumber} column`
}
},
{
name: "upload",
inputName: 'upload',
required: true,
requiredError: function (headerName, rowNumber, columnNumber) {
return `${headerName} is required in the ${rowNumber} row / ${columnNumber} column`
}
},
{
name: "download",
inputName: 'download',
required: true,
requiredError: function (headerName, rowNumber, columnNumber) {
return `${headerName} is required in the ${rowNumber} row / ${columnNumber} column`
}
},
{
name: "created_at",
inputName: 'created_at',
required: false,
},
{
name: "server_id",
inputName: 'server_id',
required: false,
},
{
name: "server_name",
inputName: 'server_name',
required: false,
},
{
name: "server_host",
inputName: 'server_host',
required: false,
},
{
name: "url",
inputName: 'url',
required: false,
},
{
name: "scheduled",
inputName: 'scheduled',
required: false,
},
{
name: "failed",
inputName: 'failed',
required: false,
},
{
name: "updated_at",
inputName: 'updated_at',
required: false,
}
]
};
CSVFileValidator(csv, config)
.then((e) => {
if(e.inValidMessages.length > 0) {
toast.error('Your upload file is not valid ' + format.toUpperCase());
} else {
this.setState({
data: data,
uploadReady: true,
filename: file.name
});
}
})
.catch((e) => {
toast.error('Your upload file is not valid ' + format.toUpperCase());
})
} else {
try {
var data = JSON.parse(data);
this.setState({
data: data,
uploadReady: true,
filename: file.name
});
} catch(e) {
console.log(e);
toast.error('Your upload file is not valid ' + format.toUpperCase());
}
}
}.bind(this)
reader.onerror = function (evt) {
toast.error('Something went wrong parsing your backup file.');
}
}
uploadFile = () => {
var data = { data: this.state.data, format: this.state.format };
var url = 'api/restore?token=' + window.token;
Axios.post(url, data)
.then((resp) => {
toast.success('Your data is being restored...');
this.setState({
show: false,
data: null,
uploadReady: false,
filename: 'Upload your backup'
});
})
.catch((err) => {
console.log(err);
})
}
render() {
var show = this.state.show;
var uploadReady = this.state.uploadReady;
var filename = this.state.filename;
return (
<>
<DropdownButton variant="secondary" title="Restore" className="m-2 d-inline-block">
<Dropdown.Item href="#" onClick={() => { this.showModal('json') }}>JSON</Dropdown.Item>
<Dropdown.Item href="#" onClick={() => { this.showModal('csv') }}>CSV</Dropdown.Item>
</DropdownButton>
<Modal show={show} onHide={this.hideModal} animation={true}>
<Modal.Header closeButton>
<Modal.Title>Restore from a backup</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Upload your {this.state.format.toUpperCase()} backup file here:</p>
<Form.File
id="restoreFileInput"
label={"Upload " + this.state.format.toUpperCase() + " file"}
className="mb-3"
custom
>
<Form.File.Input onChange={(e) => { this.readFile(e, this.state.format) }} />
<Form.File.Label data-browse="Choose file">
{filename}
</Form.File.Label>
</Form.File>
{uploadReady === true &&
<Button variant="secondary" onClick={this.uploadFile}>Restore</Button>
}
</Modal.Body>
</Modal>
</>
);
}
}
if (document.getElementById('Restore')) {
ReactDOM.render(<Restore />, document.getElementById('Restore'));
}

View File

@@ -1,89 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Link } from "react-router-dom";
import { Container } from 'react-bootstrap';
import { Row } from 'react-bootstrap';
import { Col } from 'react-bootstrap';
import { Button } from 'react-bootstrap';
export default class ErrorPage extends Component {
constructor(props) {
super(props);
var colour = '';
var message = false;
switch(this.props.code.toString()[0]) {
case 2:
case '2':
colour = 'success';
break;
case 4:
case '4':
colour = 'danger';
break;
case 5:
case '5':
default:
colour = 'warning';
break;
}
switch(this.props.code) {
case '400':
message = 'Bad request'
break;
case '401':
message = 'You aren\'t authenticated';
break;
case '403':
message = 'You aren\'t authorised to view this page';
break;
case '404':
message = 'Page not found';
break;
case '405':
message = 'Method not allowed'
break;
case '413':
message = 'Request too large'
break;
case '422':
message = 'Your request was unprocessable'
break;
}
this.state = {
code: this.props.code,
colour: colour,
message: message
};
}
render() {
const colour = this.state.colour;
const code = this.state.code;
const message = this.state.message;
return (
<Container fluid>
<Row className="fullscreen text-center align-items-center">
<Col
lg={{ span: 2, offset: 5}}
md={{ span: 4, offset: 4}}
sm={{ span: 4, offset: 4}}
xs={{ span: 12}}
>
<h1 className={'text-' + colour + ' mb-0'}>{code}</h1>
{message &&
<p className={colour + '-text mt-0 mb-2'}>{message}</p>
}
<Link to="/" className={'waves-effect waves-' + colour + ' btn ' + colour}><Button variant={colour}>Go home</Button></Link>
</Col>
</Row>
</Container>
);
}
}
if (document.getElementById('errorpage')) {
ReactDOM.render(<ErrorPage />, document.getElementById('errorpage'));
}

View File

@@ -1,23 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
export default class Index extends Component {
constructor(props) {
super(props)
this.state = {
}
}
render() {
return (
<div>
</div>
);
}
}
if (document.getElementById('main')) {
ReactDOM.render(<Index />, document.getElementById('main'));
}

View File

@@ -1,392 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Axios from 'axios';
import { Spinner, Container, Row, Form, Card } from 'react-bootstrap';
import { Line, Bar } from 'react-chartjs-2';
import { Col } from 'react-bootstrap';
import { toast } from 'react-toastify';
export default class HistoryGraph extends Component {
constructor(props) {
super(props)
this.state = {
days: props.days,
time: props.dlUl,
fail: props.fail,
config: props.config,
duData: {},
duOptions: {},
pingData: {},
pingOptions: {},
failData: {},
failOptions: {},
loading: true,
interval: null,
graph_ul_dl_enabled: true,
graph_ul_dl_width: 6,
graph_failure_enabled: true,
graph_failure_width: 6,
graph_ping_enabled: true,
graph_ping_width: 6,
firstUpdate: false,
}
}
componentDidMount = () => {
}
componentDidUpdate() {
if(
this.state.time != this.props.dlUl ||
this.state.fail != this.props.fail ||
this.state.config != this.props.config ||
this.state.days != this.props.days
) {
this.setState({
time: this.props.dlUl,
fail: this.props.fail,
config: this.props.config,
days: this.props.days
});
if(this.state.config !== null) {
this.processData();
}
}
if(
!this.state.firstUpdate &&
this.state.config !== null
) {
this.processData();
this.setState({
firstUpdate: true,
});
}
}
processData() {
this.processConfig();
this.processDlUlPing();
this.processFailure();
}
processConfig() {
this.setState({
graph_ul_dl_enabled: Boolean(Number(this.state.config.graphs.download_upload_graph_enabled.value)),
graph_ul_dl_width: this.state.config.graphs.download_upload_graph_width.value,
graph_ping_enabled: Boolean(Number(this.state.config.graphs.ping_graph_enabled.value)),
graph_ping_width: this.state.config.graphs.ping_graph_width.value,
graph_failure_enabled: Boolean(Number(this.state.config.graphs.failure_graph_enabled.value)),
graph_failure_width: this.state.config.graphs.failure_graph_width.value,
});
}
processDlUlPing() {
let days = this.state.days;
var duData = {
labels: [],
datasets:[
{
data: [],
label: 'Download',
borderColor: "#fca503",
fill: false,
},
{
data: [],
label: 'Upload',
borderColor: "#3e95cd",
fill: false,
}
],
};
var duOptions = {
maintainAspectRatio: false,
responsive: true,
tooltips: {
callbacks: {
label: (item) => `${item.yLabel} Mbit/s`,
},
},
title: {
display: false,
text: 'Speedtests results for the last ' + days + ' days',
},
scales: {
xAxes: [{
display: false,
scaleLabel: {
display: true,
labelString: 'DateTime'
}
}],
},
elements: {
point:{
radius: 0,
hitRadius: 8
}
}
};
var pingData = {
labels: [],
datasets:[
{
data: [],
label: 'Ping',
borderColor: "#07db71",
fill: false,
},
],
};
var pingOptions = {
maintainAspectRatio: false,
responsive: true,
tooltips: {
callbacks: {
label: (item) => `${item.yLabel} ms`,
},
},
title: {
display: false,
text: 'Ping results for the last ' + days + ' days',
},
scales: {
xAxes: [{
display: false,
scaleLabel: {
display: true,
labelString: 'DateTime'
}
}],
},
elements: {
point:{
radius: 0,
hitRadius: 8
}
}
}
this.state.time.forEach(e => {
var download = {
t: new Date(e.created_at),
y: e.download,
};
var upload = {
t: new Date(e.created_at),
y: e.upload,
};
var ping = {
t: new Date(e.created_at),
y: parseFloat(e.ping)
}
duData.datasets[0].data.push(download);
duData.datasets[1].data.push(upload);
pingData.datasets[0].data.push(ping);
duData.labels.push(new Date(e.created_at).toLocaleString());
pingData.labels.push(new Date(e.created_at).toLocaleString());
});
this.setState({
duData: duData,
duOptions: duOptions,
pingData: pingData,
pingOptions: pingOptions,
loading: false,
});
}
processFailure() {
let days = this.state.days;
var failData = {
labels: [],
datasets: [
{
data: [],
label: 'Successful',
backgroundColor: '#07db71'
},
{
data: [],
label: 'Failed',
backgroundColor: '#E74C3C'
},
],
};
var failOptions = {
maintainAspectRatio: false,
responsive: true,
tooltips: {
callbacks: {
label: (item) => `${item.yLabel} speedtests`,
},
},
scales: {
xAxes: [{
stacked: true
}],
yAxes: [{
stacked: true
}]
}
};
this.state.fail.forEach(e => {
var success = {x: e.date, y: e.success};
var fail = {x: e.date, y: e.failure};
failData.datasets[0].data.push(success);
failData.datasets[1].data.push(fail);
failData.labels.push(new Date(e.date).toLocaleString([], {year: '2-digit', month:'2-digit', day:'2-digit'}));
})
this.setState({
failData: failData,
failOptions: failOptions
});
}
updateDays = (e) => {
var days = e.target.value;
if(days) {
toast.info('Showing results for the last ' + days + ' days');
this.props.updateDays(days);
}
}
render() {
var loading = this.state.loading;
var duData = this.state.duData;
var duOptions = this.state.duOptions;
var pingData = this.state.pingData;
var pingOptions = this.state.pingOptions;
var failData = this.state.failData;
var failOptions = {
maintainAspectRatio: false,
responsive: true,
tooltips: {
callbacks: {
label: (item) => `${item.yLabel} speedtests`,
},
},
scales: {
xAxes: [{
stacked: true,
gridLines: {
display: false
}
}],
yAxes: [{
stacked: true,
ticks: {
stepSize: 1
}
}]
}
};
var days = this.state.days;
var graph_ul_dl_enabled = this.state.graph_ul_dl_enabled;
var graph_ul_dl_width = this.state.graph_ul_dl_width;
var graph_ping_enabled = this.state.graph_ping_enabled;
var graph_ping_width = this.state.graph_ping_width;
var graph_failure_enabled = this.state.graph_failure_enabled;
var graph_failure_width = this.state.graph_failure_width;
var dlClasses = 'my-2 home-graph ';
var pingClasses = 'my-2 home-graph ';
var failureClasses = 'my-2 home-graph ';
if(graph_ul_dl_enabled == true) {
//
} else {
dlClasses += 'd-none ';
}
if(graph_ping_enabled == true) {
//
} else {
pingClasses += 'd-none ';
}
if(graph_failure_enabled == true) {
//
} else {
failureClasses += 'd-none ';
}
if(loading) {
return (
<div>
<Spinner animation="grow" />
</div>
)
} else {
return (
<Container className="mb-4 mt-1" fluid>
<Row>
<Col
lg={{ span: graph_ul_dl_width }}
md={{ span: graph_ul_dl_width }}
sm={{ span: 12 }}
xs={{ span: 12 }}
className={dlClasses}
>
<Card className="shadow-sm">
<Card.Body>
<Line data={duData} options={duOptions} height={440} />
</Card.Body>
</Card>
</Col>
<Col
lg={{ span: graph_ping_width }}
md={{ span: graph_ping_width }}
sm={{ span: 12 }}
xs={{ span: 12 }}
className={pingClasses}
>
<Card className="shadow-sm">
<Card.Body>
<Line data={pingData} options={pingOptions} height={440} />
</Card.Body>
</Card>
</Col>
<Col
lg={{ span: graph_failure_width }}
md={{ span: graph_failure_width }}
sm={{ span: 12 }}
xs={{ span: 12 }}
className={failureClasses}
>
<Card className="shadow-sm h-100">
<Card.Body className="w-100 h-100">
<Bar data={failData} options={failOptions} height={440} />
</Card.Body>
</Card>
</Col>
</Row>
<Row>
<Col sm={{ span: 12 }}>
<div className="text-center">
<div className="d-inline-flex align-items-center mb-2">
<h4 className="d-inline mb-0">Show results for the last</h4>
<Form.Control id="duDaysInput" className="d-inline-block mx-2" defaultValue={days} onInput={this.updateDays}></Form.Control>
<h4 className="d-inline mb-0">days</h4>
</div>
</div>
</Col>
</Row>
</Container>
);
}
}
}
if (document.getElementById('HistoryGraph')) {
ReactDOM.render(<HistoryGraph />, document.getElementById('HistoryGraph'));
}

View File

@@ -1,164 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Axios from 'axios';
import Widget from './Widget';
import { Container, Row, Spinner } from 'react-bootstrap';
import { Col } from 'react-bootstrap';
import { Button } from 'react-bootstrap';
import { toast } from 'react-toastify';
export default class LatestResults extends Component {
constructor(props) {
super(props)
this.state = {
data: props.data,
interval: null,
loading: true,
}
}
componentDidUpdate() {
if(this.state.data !== this.props.data) {
this.setState({
data: this.props.data,
loading: false,
});
}
}
componentWillUnmount() {
clearInterval(this.state.interval);
}
newScan = () => {
var url = 'api/speedtest/run?token=' + window.token;
Axios.get(url)
.then((resp) => {
toast.info('A test has been queued. This page will refresh when the test has finished.');
})
.catch((err) => {
if(err.response) {
if(err.response.status == 429) {
toast.error('You are doing that too much. Try again later.');
}
console.log(err.response);
} else {
console.log(err.data);
}
})
}
render() {
var loading = this.state.loading;
var data = this.state.data;
if(loading && data !== false) {
return (
<Container fluid>
<Row>
<Col sm={{ span: 12 }}>
<Spinner animation="grow" />
</Col>
</Row>
</Container>
);
} else if(data === false) {
if( (window.config.auth == true && window.authenticated == true) || window.config.auth == false) {
return (
<Container fluid>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<div>
<Button variant="primary" onClick={this.newScan}>Start your first test!</Button>
</div>
</Col>
</Row>
</Container>
);
} else if(window.config.auth == true && window.authenticated == false) {
return (
<Container fluid>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<div>
<p>Please login to run the first test</p>
</div>
</Col>
</Row>
</Container>
);
}
} else {
return (
<Container fluid>
<Row>
<Col sm={{ span: 12 }} className="text-center mb-2">
<div>
{(window.config.auth == true && window.authenticated == true) || window.config.auth == false ?
<div>
<Button className="d-inline-block mx-3 mb-2" variant="primary" onClick={this.newScan}>Test again</Button>
<p className="text-muted mb-0 d-inline-block">Last test performed at: {new Date(data.data.created_at).toLocaleString()}</p>
</div>
:
<div>
<p className="text-muted mb-0 d-inline-block">Last test performed at: {new Date(data.data.created_at).toLocaleString()}</p>
</div>
}
</div>
</Col>
</Row>
<Row>
<Col
lg={{ span: 4 }}
md={{ span: 4 }}
sm={{ span: 12 }}
className="my-2"
>
<Widget
title="Ping"
data={data}
failed={data.data.failed}
unit="ms"
icon="ping"
/>
</Col>
<Col
lg={{ span: 4 }}
md={{ span: 4 }}
sm={{ span: 12 }}
className="my-2"
>
<Widget
title="Download"
data={data}
failed={data.data.failed}
unit="Mbit/s"
icon="dl"
/>
</Col>
<Col
lg={{ span: 4 }}
md={{ span: 4 }}
sm={{ span: 12 }}
className="my-2"
>
<Widget
title="Upload"
data={data}
failed={data.data.failed}
unit="Mbit/s"
icon="ul"
/>
</Col>
</Row>
</Container>
);
}
}
}
if (document.getElementById('LatestResults')) {
ReactDOM.render(<LatestResults />, document.getElementById('LatestResults'));
}

View File

@@ -1,162 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
export default class TableRow extends Component {
constructor(props) {
super(props)
this.state = {
data: this.props.data,
show: false,
}
}
toggleShow = () => {
var show = this.state.show;
if(show) {
this.setState({
show: false
});
} else {
this.setState({
show: true
});
}
}
delete = (id) => {
var url = 'api/speedtest/delete/' + id + '?token=' + window.token;
Axios.delete(url)
.then((resp) => {
console.log(resp);
toast.success('Speedtest deleted');
})
.catch((err) => {
if(err.response.status == 404) {
toast.warning('Speedtest not found');
} else {
toast.error('Something went wrong');
}
})
this.props.refresh();
this.toggleShow();
}
getDataFields = () => {
let allFields = this.props.allFields;
let data = this.state.data;
let processedFields = [];
for(var key in allFields) {
let field = allFields[key];
let value = data[key];
if(field.type === 'date') {
value = new Date(value).toLocaleString();
} else if(field.type === 'bool') {
value = Boolean(value) ? field.if_true : field.if_false
}
let final = {
name: key,
key: field.alias,
value: value,
type: field.type
};
processedFields.push(final);
}
let visible = [];
let inModal = [];
window.config.tables.visible_columns.forEach(column => {
visible.push(processedFields.find(x => x.name == column));
});
inModal = processedFields.filter(el => {
return !visible.includes(el);
});
return {
visible: visible,
modal: inModal
};
}
render() {
var e = this.state.data;
var show = this.state.show;
var fields = this.getDataFields();
if(e.failed != true) {
return (
<tr>
{fields.visible.map((e, i) => {
return (
<td key={i}>{e.value}</td>
);
})}
{e.server_host != null ?
<td>
<span onClick={this.toggleShow} className="ti-arrow-top-right mouse"></span>
<Modal show={show} onHide={this.toggleShow}>
<Modal.Header closeButton>
<Modal.Title>More info</Modal.Title>
</Modal.Header>
<Modal.Body className="text-center">
{fields.modal.map((e, i) => {
if(e.type === 'url') {
return (
<p key={i}>{e.key}: <a href={e.value} target="_blank" rel="noopener noreferer">Speedtest.net</a></p>
);
} else {
return (
<p key={i}>{e.key}: {e.value}</p>
);
}
})}
<Button variant="danger" onClick={() => { this.delete(e.id) }}>Delete</Button>
</Modal.Body>
</Modal>
</td>
:
<td></td>
}
</tr>
);
} else {
return (
<tr>
{fields.visible.map((e, i) => {
console.log(e);
if(e.name === 'created_at') {
return <td key={i}>{new Date(e.value).toLocaleString()}</td>
} else if (e.name === 'id') {
return <td key={i}>{e.value}</td>
}
return (
<td key={i}><span className="ti-close text-danger"></span></td>
);
})}
{(window.config.auth && window.authenticated) || !window.config.auth ?
<td><Button variant="danger" onClick={() => { this.delete(e.id) }}>Delete</Button></td>
:
<td></td>
}
</tr>
);
}
}
}
if (document.getElementById('tableRow')) {
ReactDOM.render(<TableRow />, document.getElementById('tableRow'));
}

View File

@@ -1,189 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Axios from 'axios';
import { Container, Row, Table, Col, Collapse, Button } from 'react-bootstrap';
import TableRow from './TableRow';
export default class TestsTable extends Component {
constructor(props) {
super(props)
this.state = {
page: 1,
lastPage: 1,
data: [],
showTable: false,
refresh: true,
interval: null,
allFields: {
id: {
type: 'int',
alias: 'ID'
},
created_at: {
type: 'date',
alias: 'Time'
},
download: {
type: 'float',
alias: 'Download (Mbit/s)'
},
upload: {
type: 'float',
alias: 'Upload (Mbit/s)'
},
ping: {
type: 'float',
alias: 'Ping (ms)'
},
server_id: {
type: 'int',
alias: 'Server ID'
},
server_name: {
type: 'string',
alias: 'Name'
},
server_host: {
type: 'string',
alias: 'Host'
},
url: {
type: 'url',
alias: 'URL'
},
scheduled: {
type: 'bool',
alias: 'Type',
if_true: 'scheduled',
if_false: 'manual'
}
}
}
}
componentDidMount() {
this.getData();
var int = setInterval(this.getData, 10000);
this.setState({
interval: int
});
}
componentWillUnmount() {
clearInterval(this.state.interval);
}
getData = (page = this.state.page, refresh = true) => {
var url = 'api/speedtest/?page=' + page;
Axios.get(url)
.then((resp) => {
var data = resp.data.data.data;
if(!refresh) {
data = this.state.data.concat(data);
}
var page = resp.data.data.current_page;
var lastPage = resp.data.data.last_page;
this.setState({
data: data,
page: page,
lastPage: lastPage,
refresh: refresh
});
})
.catch((err) => {
console.log(err);
})
}
getMoreData = () => {
var page = this.state.page;
page = page + 1;
if(this.state.refresh) {
clearInterval(this.state.interval);
}
this.getData(page, false);
}
toggleCollapse = () => {
var show = this.state.showTable;
if(show) {
this.setState({
showTable: false
});
} else {
this.setState({
showTable: true
});
}
}
render() {
var page = this.state.page;
var lastPage = this.state.lastPage;
var data = this.state.data;
var show = this.state.showTable;
var refresh = this.state.refresh;
let allFields = this.state.allFields;
if(data.length > 0) {
return (
<div>
<Container className="mb-4 mt-4 px-5">
<Row>
<Col sm={{ span: 12 }} className="mb-3 text-center">
<div>
<h4 className="d-inline mr-2">All tests</h4>
<span className="text-muted">Auto refresh: {(refresh) ? 'On' : 'Off'}</span>
</div>
</Col>
</Row>
<Row>
<Col sm={{ span: 12 }} id="testsTable">
<Table responsive>
<thead>
<tr>
{window.config.tables.visible_columns.map((e, i) => {
return (
<th key={i}>{allFields[e].alias}</th>
);
})}
<th>More</th>
</tr>
</thead>
<tbody>
{data.map((e,i) => {
return (
<TableRow key={e.id} data={e} allFields={allFields} refresh={this.getData} />
);
})}
</tbody>
</Table>
</Col>
</Row>
{page < lastPage &&
<Row>
<Col sm={{ span: 12 }} className="text-center">
<Button variant="primary" onClick={this.getMoreData}>Show more</Button>
</Col>
</Row>
}
</Container>
</div>
);
} else {
return (
<>
</>
)
}
}
}
if (document.getElementById('TestsTable')) {
ReactDOM.render(<TestsTable />, document.getElementById('TestsTable'));
}

View File

@@ -1,150 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Card } from 'react-bootstrap';
export default class Widget extends Component {
constructor(props) {
super(props)
this.state = {
title: this.props.title,
unit: this.props.unit,
icon: this.props.icon,
failed: this.props.failed,
data: this.props.data
}
}
parseData(title, data) {
var returnData = {};
if(title == 'Ping') {
returnData.value = parseFloat(data.data.ping).toFixed(1);
if(window.config.widgets.show_average) {
returnData.avg = parseFloat(data.average.ping).toFixed(1);
}
if(window.config.widgets.show_max) {
returnData.max = parseFloat(data.maximum.ping).toFixed(1);
}
if(window.config.widgets.show_min) {
returnData.min = parseFloat(data.minimum.ping).toFixed(1);
}
}
if(title == 'Upload') {
returnData.value = parseFloat(data.data.upload).toFixed(1);
if(window.config.widgets.show_average) {
returnData.avg = parseFloat(data.average.upload).toFixed(1);
}
if(window.config.widgets.show_max) {
returnData.max = parseFloat(data.maximum.upload).toFixed(1);
}
if(window.config.widgets.show_min) {
returnData.min = parseFloat(data.minimum.upload).toFixed(1);
}
}
if(title == 'Download') {
returnData.value = parseFloat(data.data.download).toFixed(1);
if(window.config.widgets.show_average) {
returnData.avg = parseFloat(data.average.download).toFixed(1);
}
if(window.config.widgets.show_max) {
returnData.max = parseFloat(data.maximum.download).toFixed(1);
}
if(window.config.widgets.show_min) {
returnData.min = parseFloat(data.minimum.download).toFixed(1);
}
}
return returnData;
}
componentDidUpdate = () => {
if(this.props.title != this.state.title || this.props.data != this.state.data || this.props.unit != this.state.unit || this.props.icon != this.state.icon || this.props.failed != this.state.failed) {
this.setState({
title: this.props.title,
unit: this.props.unit,
icon: this.props.icon,
failed: this.props.failed,
data: this.props.data
});
}
}
render() {
var title = this.state.title;
var unit = this.state.unit;
var icon = this.state.icon;
var failed = Boolean(Number(this.state.failed));
var data = this.parseData(title, this.state.data);
switch(icon) {
case 'ping':
icon = <span className="ti-pulse icon text-success"></span>;
break;
case 'dl':
icon = <span className="ti-download icon text-warning"></span>;
break;
case 'ul':
icon = <span className="ti-upload icon text-primary"></span>;
break;
}
return (
<Card className="widget-card shadow-sm">
<Card.Body>
<div>
<div>
<div className="d-flex align-items-center justify-content-between">
<h4>{title}</h4>
{icon}
</div>
<div className="text-truncate">
<h3 className="d-inline">{(!failed) ? data.value : <span className="ti-close text-danger"></span> }</h3>
<p className="d-inline ml-2">{unit} (current)</p>
</div>
{window.config.widgets.show_average &&
<div className="text-muted text-truncate">
<h5 className="d-inline">{data.avg}</h5>
<p className="d-inline ml-2">{unit} (average)</p>
</div>
}
{window.config.widgets.show_max &&
<div className="text-muted text-truncate">
<h5 className="d-inline">{data.max}</h5>
<p className="d-inline ml-2">{unit} (maximum)</p>
</div>
}
{window.config.widgets.show_min &&
<div className="text-muted text-truncate">
<h5 className="d-inline">{data.min}</h5>
<p className="d-inline ml-2">{unit} (minimum)</p>
</div>
}
</div>
</div>
</Card.Body>
</Card>
);
}
}
if (document.getElementById('Widget')) {
ReactDOM.render(<Widget />, document.getElementById('Widget'));
}

View File

@@ -1,24 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Container, Row } from 'react-bootstrap';
import { Col } from 'react-bootstrap';
import Version from './Version';
export default class Footer extends Component {
render() {
return (
<Container>
<Row>
<Col sm={{ span: 12 }} className="text-center">
<Version />
<p className="text-muted">See the code on <a href="https://github.com/henrywhitaker3/Speedtest-Tracker" target="_blank" rel="noopener noreferrer">GitHub</a></p>
</Col>
</Row>
</Container>
);
}
}
if (document.getElementById('Footer')) {
ReactDOM.render(<Footer />, document.getElementById('Footer'));
}

View File

@@ -1,85 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import HistoryGraph from '../Graphics/HistoryGraph';
import LatestResults from '../Graphics/LatestResults';
import Footer from './Footer';
import DataRow from '../Data/DataRow';
import TestsTable from '../Graphics/TestsTable';
import Login from '../Login';
import Authentication from '../Authentication/Authentication';
import Navbar from '../Navbar';
import axios from 'axios';
export default class HomePage extends Component {
constructor(props) {
super(props)
this.state = {
latest: null,
time: null,
fail: null,
config: null,
days: 7,
interval: null,
}
}
componentDidMount = () => {
this.getData();
var interval = setInterval(this.getData, 10000);
this.setState({
interval: interval,
});
}
componentWillUnmount() {
clearInterval(this.state.interval);
}
updateDays = (days) => {
this.setState({ days: days });
this.getData();
}
getData = () => {
axios.get('api/speedtest/home/' + this.state.days)
.then((resp) => {
this.setState({
latest: resp.data.latest,
time: resp.data.time,
fail: resp.data.fail,
config: resp.data.config
});
})
.catch((err) => {
console.log(err);
})
}
render() {
let latest = this.state.latest;
let time = this.state.time;
let fail = this.state.fail;
let config = this.state.config;
let days = this.state.days;
return (
<div>
<Navbar />
<div className="my-4">
{(window.config.auth == true && window.authenticated == false) &&
<Login />
}
<LatestResults data={latest} />
<HistoryGraph updateDays={this.updateDays} dlUl={time} fail={fail} config={config} days={days} />
</div>
<Footer />
</div>
);
}
}
if (document.getElementById('homePage')) {
ReactDOM.render(<HomePage />, document.getElementById('homePage'));
}

View File

@@ -1,152 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Axios from 'axios';
import { toast } from 'react-toastify';
import { Modal, ProgressBar } from 'react-bootstrap';
import { Button } from 'react-bootstrap';
import Changelog from '../Data/Changelog';
export default class Version extends Component {
constructor(props) {
super(props)
this.state = {
version: document.querySelector('meta[name="version"]').content,
update: false,
modalShow: false,
changelog: [],
showProgress: false,
updateProgress: 0,
};
}
componentDidMount() {
// this.checkForUpdates();
}
checkForUpdates = () => {
var url = 'api/update/check';
Axios.get(url)
.then((resp) => {
var update = resp.data.update;
if(update !== false) {
toast.info('A new version of Speedtest Tracker is available (v' + update.version + '). Go to the bottom of the page to update.');
this.setState({
update: update.version,
changelog: update.changelog,
});
}
})
.catch((err) => {
console.log(err);
});
}
showModal = () => {
this.setState({
modalShow: true
});
}
hideModal = () => {
this.setState({
modalShow: false
});
}
updateApp = () => {
this.setState({
showProgress: true,
updateProgress: 0,
});
toast.info('Downloading update');
Axios.get('api/update/download')
.then((resp) => {
this.setState({
updateProgress: 50,
});
toast.info('Extracting update');
Axios.get('api/speedtest/extract')
.then((resp) => {
this.setState({
updateProgress: 75,
});
toast.info('Applying update');
Axios.get('api/update/move')
.then((resp) => {
this.setState({
updateProgress: 100,
});
toast.success('Update successful. Refreshing your page...');
setTimeout(function() {
location.reload(true);
}, 5000);
})
})
})
.catch((err) => {
toast.error('Something went wrong...');
})
}
render() {
var version = this.state.version;
var update = this.state.update;
var modalShow = this.state.modalShow;
var changelog = this.state.changelog;
var showProgress = this.state.showProgress;
var updateProgress = this.state.updateProgress;
if(update === false) {
return (
<div>
<p className="text-muted mb-0 d-inline-block">Speedtest Tracker Version: {version}</p>
<Changelog />
</div>
);
} else {
return (
<div>
<p className="text-muted mb-0 d-inline">Speedtest Tracker Version: {version} - </p>
<a href="#!" className="mb-0 d-inline" onClick={this.showModal}>New version available - v{update}</a>
<Modal show={modalShow} onHide={this.hideModal} animation={true}>
<Modal.Header closeButton>
<Modal.Title>Update to v{update}</Modal.Title>
</Modal.Header>
<Modal.Body>
<h5>Changelog:</h5>
<ul>
{changelog.map((e, i) => {
if(e.link == '') {
return (
<li key={i}>{e.description}</li>
);
} else {
return (
<li key={i}><a href={e.link} target="_blank" rel="noopener noreferer">{e.description}</a></li>
)
}
})}
</ul>
{showProgress &&
<div>
<p>Update progress:</p>
<ProgressBar animated now={updateProgress} />
</div>
}
{!showProgress &&
<Button variant="primary" onClick={this.updateApp}>Update</Button>
}
</Modal.Body>
</Modal>
</div>
);
}
}
}
if (document.getElementById('Version')) {
ReactDOM.render(<Version />, document.getElementById('Version'));
}

View File

@@ -1,54 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Spinner from 'react-bootstrap/Spinner';
import { Container } from 'react-bootstrap';
import { Row } from 'react-bootstrap';
import { Col } from 'react-bootstrap';
export default class Loader extends Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
if(this.props.small) {
return (
<Container fluid>
<Row className="text-center align-items-center">
<Col
lg={{ span: 2, offset: 5}}
md={{ span: 4, offset: 4}}
sm={{ span: 4, offset: 4}}
xs={{ span: 12}}
>
<Spinner animation="grow" size="lg"/>
</Col>
</Row>
</Container>
)
}
return (
<Container fluid>
<Row className="fullscreen text-center align-items-center">
<Col
lg={{ span: 2, offset: 5}}
md={{ span: 4, offset: 4}}
sm={{ span: 4, offset: 4}}
xs={{ span: 12}}
>
<Spinner animation="grow" size="lg"/>
</Col>
</Row>
</Container>
);
}
}
if (document.getElementById('loader')) {
ReactDOM.render(<Loader />, document.getElementById('loader'));
}

View File

@@ -1,97 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Container, Row, Form, Toast, Modal } from 'react-bootstrap';
import { Col } from 'react-bootstrap';
import { Button } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
import Cookies from 'js-cookie';
export default class Login extends Component {
constructor(props) {
super(props)
this.state = {
loginEmailInput: '',
loginPasswordInput: ''
}
}
updateTextField = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
login = (e) => {
e.preventDefault();
var data = {
email: this.state.loginEmailInput,
password: this.state.loginPasswordInput
}
var url = 'api/auth/login';
Axios.post(url, data)
.then((resp) => {
var token = resp.data.access_token;
var expires = (resp.data.expires_in / 60) / 24;
Cookies.set('auth', token, { expires: expires })
window.location.reload(true);
})
.catch((err) => {
toast.error('Something went wrong logging in.');
})
}
toggleShow = () => {
if(this.state.show) {
this.setState({
show: false
})
} else {
this.setState({
show: true
})
}
}
render() {
var show = this.state.show;
return (
<Container>
<Row>
<Col
xs={{ span: 12 }}
className="pb-2 text-center"
>
<Button variant="primary" onClick={this.toggleShow}>Login</Button>
<Modal show={show} onHide={this.toggleShow}>
<Modal.Header closeButton>
<Modal.Title>Login</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={this.login}>
<Form.Group controlId="loginEmailInput">
<Form.Label>Email address</Form.Label>
<Form.Control type="email" placeholder="admin@admin.com" onInput={this.updateTextField} required />
</Form.Group>
<Form.Group controlId="loginPasswordInput">
<Form.Label>Password</Form.Label>
<Form.Control type="password" onInput={this.updateTextField} required />
</Form.Group>
<Button variant="primary" type="submit">Login</Button>
</Form>
</Modal.Body>
</Modal>
</Col>
</Row>
</Container>
);
}
}
if (document.getElementById('login')) {
ReactDOM.render(<Login />, document.getElementById('login'));
}

View File

@@ -1,81 +0,0 @@
import React, { Component } from 'react';
import {Nav, Navbar as BootstrapNavbar, NavLink as BootstrapNavLink} from 'react-bootstrap';
import ReactDOM from 'react-dom';
import { Link, NavLink } from 'react-router-dom';
export default class Navbar extends Component {
constructor(props) {
super(props)
this.state = {
brand: {
name: window.config.name,
url: window.config.base
},
}
}
generatePagesArray() {
var pages = [
{
name: 'Home',
url: window.config.base,
authRequired: false
},
{
name: 'All Tests',
url: window.config.base + 'speedtests',
authRequired: false
},
{
name: 'Settings',
url: window.config.base + 'settings',
authRequired: true
},
]
return pages;
}
generateLinks = () => {
var pages = this.generatePagesArray();
return pages.map(page => {
if(
page.authRequired === false ||
(
page.authRequired === true &&
window.config.auth &&
window.authenticated
) ||
(
page.authRequired === true &&
window.config.auth === false
)
) {
return <BootstrapNavLink key={page.url} as={NavLink} to={page.url}>{page.name}</BootstrapNavLink>;
}
});
}
render() {
var brand = this.state.brand;
var pages = this.generateLinks();
return (
<BootstrapNavbar variant="dark" bg="dark" expand="sm">
<BootstrapNavbar.Brand as={Link} to={brand.url}><img style={{width: '15%'}} src={window.config.base + 'files/icons/fav/android-icon-192x192.png'} /> {brand.name}</BootstrapNavbar.Brand>
<BootstrapNavbar.Toggle aria-controls="basic-navbar-nav" />
<BootstrapNavbar.Collapse id="basic-navbar-nav">
<Nav className="ml-auto">
{pages}
</Nav>
</BootstrapNavbar.Collapse>
</BootstrapNavbar>
);
}
}
if (document.getElementById('navbar')) {
ReactDOM.render(<Navbar />, document.getElementById('navbar'));
}

View File

@@ -1,320 +0,0 @@
import Axios from 'axios';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Footer from '../Home/Footer';
import Loader from '../Loader';
import Navbar from '../Navbar';
import SettingsTabs from './SettingsTabs';
export default class SettingsIndex extends Component {
constructor(props) {
super(props)
this.state = {
data: null,
loading: true,
}
}
getData = () => {
var url = 'api/settings/?token=' + window.token;
Axios.get(url)
.then((resp) => {
this.setState({
data: this.sortSettings(resp.data),
loading: false,
});
})
.catch((err) => {
//
})
}
sortSettings = (data) => {
return {
General: [
{
obj: data.app_name,
type: 'text',
},
{
obj: data.schedule_enabled,
type: 'checkbox',
},
{
obj: data.schedule,
type: 'text',
},
{
obj: data.server,
type: 'text',
},
{
obj: data.show_average,
type: 'checkbox',
},
{
obj: data.show_max,
type: 'checkbox',
},
{
obj: data.show_min,
type: 'checkbox',
}
],
Tables: [
{
obj: data.visible_columns,
type: 'list'
},
{
obj: data.hidden_columns,
type: 'list'
}
],
Graphs: [
{
obj: data.download_upload_graph_enabled,
type: 'checkbox',
hideDescription: true
},
{
obj: data.download_upload_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: data.ping_graph_enabled,
type: 'checkbox',
hideDescription: true
},
{
obj: data.ping_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: data.failure_graph_enabled,
type: 'checkbox',
hideDescription: true
},
{
obj: data.failure_graph_width,
type: 'select',
options: [
{
name: 'Full-width',
'value': 12
},
{
name: 'Half-width',
'value': 6
}
],
},
{
obj: data.show_failed_tests_on_graph,
type: 'checkbox',
},
],
Notifications: [
{
obj: data.slack_webhook,
type: 'text'
},
{
obj: data.telegram_bot_token,
type: 'text'
},
{
obj: data.telegram_chat_id,
type: 'text'
},
{
type: 'btn-get',
url: 'api/settings/test-notification?token=' + window.token,
btnType: 'primary',
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: 'Test notifications',
description: 'After saving your updated notification settings, use this to check your settings are correct.'
}
},
{
obj: data.speedtest_notifications,
type: 'checkbox'
},
{
obj: data.speedtest_overview_notification,
type: 'checkbox'
},
{
obj: data.speedtest_overview_time,
type: 'number',
min: 0,
max: 23,
},
// Add handling for title stuff
{
obj: data.threshold_alert_percentage,
type: 'number',
min: 0,
max: 100
},
{
obj: data.threshold_alert_absolute_notifications,
type: 'checkbox'
},
{
obj: data.threshold_alert_absolute_download,
type: 'number'
},
{
obj: data.threshold_alert_absolute_upload,
type: 'number'
},
{
obj: data.threshold_alert_absolute_ping,
type: 'number'
},
],
healthchecks: [
{
obj: data.healthchecks_enabled,
type: 'checkbox'
},
{
obj: data.healthchecks_server_url,
type: 'text'
},
{
obj: data.healthchecks_uuid,
type: 'text'
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Test healthchecks.io integration",
description: ""
},
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Start",
description: ""
},
type: 'btn-get',
url: 'api/settings/test-healthchecks/start?token=' + window.token,
btnType: 'outline-success',
inline: true,
earlyReturn: true,
classes: 'mr-2'
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Success",
description: ""
},
type: 'btn-get',
url: 'api/settings/test-healthchecks/success?token=' + window.token,
btnType: 'success',
text: 'Success',
inline: true,
earlyReturn: true,
classes: 'mr-2'
},
{
obj: {
id: (Math.floor(Math.random() * 10000) + 1),
name: "Fail",
description: ""
},
type: 'btn-get',
url: 'api/settings/test-healthchecks/fail?token=' + window.token,
btnType: 'danger',
text: 'Fail',
inline: true,
earlyReturn: true,
classes: 'mr-2'
},
],
influxdb: [
{
obj: data.influx_db_enabled,
type: 'checkbox'
},
{
obj: data.influx_db_host,
type: 'text'
},
{
obj: data.influx_db_port,
type: 'number'
},
{
obj: data.influx_db_database,
type: 'text'
},
{
obj: data.influx_db_username,
type: 'text',
autoComplete: false,
},
{
obj: data.influx_db_password,
type: 'password',
autoComplete: false,
}
],
};
}
componentDidMount() {
this.getData();
}
render() {
var data = this.state.data;
var loading = this.state.loading;
return (
<div>
<Navbar />
<div className="container my-5">
{loading ?
<Loader />
:
<SettingsTabs data={data} refreshConfig={this.props.refreshConfig} />
}
</div>
<Footer />
</div>
);
}
}
if (document.getElementById('settingsIndex')) {
ReactDOM.render(<SettingsIndex />, document.getElementById('settingsIndex'));
}

View File

@@ -1,202 +0,0 @@
import React, { Component } from 'react';
import { Form } from 'react-bootstrap';
import ReactDOM from 'react-dom';
export default class SettingsInput extends Component {
constructor(props) {
super(props)
this.state = {
type: this.props.type,
name: this.props.name,
displayName: (this.props.name) ? this.formatName(this.props.name) : '',
value: (this.props.value) ? this.props.value : '',
classes: this.props.classes,
id: this.props.id,
label: (this.props.label) ? this.props.label : false,
readonly: true,
description: (this.props.description) ? this.props.description : false,
options: this.props.options ? this.props.options : [],
hideDescription: this.props.hideDescription ? true : false,
min: this.props.min ? this.props.min : null,
max: this.props.max ? this.props.max : null,
url: this.props.url,
inline: this.props.inline ? 'd-inline-block' : 'd-block',
btnType: this.props.btnType,
earlyReturn: this.props.earlyReturn ? true : false,
autoComplete: String(this.props.autoComplete ? true : Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 7)),
}
}
componentDidMount() {
this.setState({
readonly: this.isReadOnly()
});
}
formatName(name) {
name = name.split('_').join(' ');
return name.charAt(0).toUpperCase() + name.slice(1);
}
handleInput = (evt) => {
var val = evt.target.value;
if(this.state.type === 'checkbox') {
val = evt.target.checked;
}
this.props.handler(
this.state.name,
val
);
this.setState({
value: val
});
}
isReadOnly = () => {
if(window.config.editable[this.state.name] == false) {
return true;
}
return false;
}
generateNumberInput(disabled) {
return <Form.Control
name={this.state.name}
type={this.state.type}
defaultValue={this.state.value}
disabled={disabled}
min={this.state.min}
max={this.state.max}
onInput={this.handleInput}
autoComplete={this.state.autoComplete} />
}
generateSelectInput(disabled) {
return (
<Form.Control
as="select"
name={this.state.name}
type={this.state.type}
defaultValue={this.state.value}
disabled={disabled}
onInput={this.handleInput}
>
{this.state.options.map((option,i) => {
return <option key={i} value={option.value}>{option.name}</option>
})}
</Form.Control>
);
}
generateCheckboxInput(disabled) {
return <Form.Control
custom
className="ml-2"
name={this.state.name}
type={this.state.type}
defaultChecked={this.state.value}
disabled={disabled}
onInput={this.handleInput} />
}
generateTextInput(disabled) {
return <Form.Control
name={this.state.name}
type={this.state.type}
defaultValue={this.state.value}
disabled={disabled}
onInput={this.handleInput}
autoComplete={this.state.autoComplete} />
}
generatePasswordInput(disabled) {
return <Form.Control
name={this.state.name}
type={this.state.type}
defaultValue={this.state.value}
disabled={disabled}
onInput={this.handleInput}
autoComplete={this.state.autoComplete} />
}
generateButtonGetInput() {
var url = this.state.url;
return (
<button
type="button"
className={"btn btn-" + this.state.btnType + ' ' + this.state.inline + ' ' + this.state.classes}
onClick={() => {
window.axios.get(url)
}}
>{this.state.displayName}</button>
);
}
generateInput = () => {
var disabled = (this.state.readonly) ? true : false;
var input = null;
if(this.state.type === 'number') {
input = this.generateNumberInput(disabled);
}
if(this.state.type === 'select') {
input = this.generateSelectInput(disabled);
}
if(this.state.type === 'checkbox') {
input = this.generateCheckboxInput(disabled);
}
if(this.state.type === 'text') {
input = this.generateTextInput(disabled);
}
if(this.state.type === 'password') {
input = this.generatePasswordInput(disabled);
}
if(this.state.type === 'btn-get') {
input = this.generateButtonGetInput();
}
if(this.state.earlyReturn) {
return input;
}
return (
<Form.Group controlId={this.state.id}>
{this.state.label &&
<Form.Label style={{fontSize: '1.25rem'}}>{this.formatName(this.state.name)}</Form.Label>
}
{input}
{this.state.description && !this.state.hideDescription &&
<p className="mt-1 text-muted" dangerouslySetInnerHTML={{ __html: this.state.description }}></p>
}
{this.state.readonly &&
<Form.Text className="text-muted">This setting is defined as an env variable and is not editable.</Form.Text>
}
</Form.Group>
);
}
render() {
var input = this.generateInput();
return input;
}
}
if (document.getElementById('SettingsInput')) {
ReactDOM.render(<SettingsInput />, document.getElementById('SettingsInput'));
}

View File

@@ -1,192 +0,0 @@
import Axios from 'axios';
import React, { Component } from 'react';
import { Nav, Tab, Tabs } from 'react-bootstrap';
import ReactDOM from 'react-dom';
import { toast } from 'react-toastify';
import SettingsInput from './SettingsInput';
import ResetSettings from './tabs/ResetSettings';
import BackupSettings from './tabs/BackupSettings';
import GeneralSettings from './tabs/GeneralSettings';
import GraphsSettings from './tabs/GraphsSettings';
import HealthchecksSettings from './tabs/HealthchecksSettings';
import NotificationsSettings from './tabs/NotificationsSettings';
import Authentication from '../Authentication/Authentication';
import TableSettings from './tabs/TableSettings';
import InfluxDBSettings from './tabs/InfluxDBSettings';
export default class SettingsTabs extends Component {
constructor(props) {
super(props)
this.state = {
tab: "General",
data: this.props.data
}
}
generateTabs = () => {
var tabs = [
'General',
'Graphs',
'Tables',
'Notifications',
'healthchecks.io',
'InfluxDB',
'Reset',
'Backup/Restore',
];
if(window.config.auth) {
tabs.push('Authentication');
}
return tabs.map((tab) => {
return <Tab key={tab} eventKey={tab} title={tab} />
});
}
switchTab = (tab) => {
this.setState({
tab: tab
});
}
save = (settings, name) => {
var url = 'api/settings/bulk?token=' + window.token;
var data = [];
settings.forEach(e => {
if(e.type !== 'btn-get') {
var res = {
name: e.obj.name,
value: e.obj.value
};
data.push(res);
}
});
data = {
data: data
};
Axios.post(url, data)
.then((resp) => {
toast.success(name + ' settings updated');
Axios.get('api/settings/config')
.then((resp) => {
window.config = resp.data;
})
})
.catch((err) => {
if(err.response.status == 422) {
toast.error('Your input was invalid');
} else {
toast.error('Something went wrong')
}
})
}
generateInputs = (settings, handler) => {
return settings.map((setting) => {
return <SettingsInput
key={setting.obj.id}
name={setting.obj.name}
id={setting.obj.id}
type={setting.type}
value={setting.obj.value}
description={setting.obj.description}
handler={handler}
label={setting.obj.name}
description={setting.obj.description}
options={setting.type == 'select' ? setting.options : []}
hideDescription={setting.hideDescription ? setting.hideDescription : false}
min={setting.min ? setting.min : false}
max={setting.max ? setting.max : false}
btnType={setting.btnType}
inline={setting.inline}
url={setting.url}
earlyReturn={setting.earlyReturn ? true : false}
classes={setting.classes ? setting.classes : ''}
autoComplete={setting.autoComplete ? true : false}
/>
})
}
getTabContent = () => {
var data = this.state.data;
switch(this.state.tab) {
case 'General':
return <GeneralSettings
data={data.General}
generateInputs={this.generateInputs}
save={this.save} />
case 'Graphs':
return <GraphsSettings
data={data.Graphs}
generateInputs={this.generateInputs}
save={this.save} />
case 'Tables':
return <TableSettings
data={data.Tables}
refreshConfig={this.props.refreshConfig}
save={this.save} />
case 'Notifications':
return <NotificationsSettings
data={data.Notifications}
generateInputs={this.generateInputs}
save={this.save} />
case 'healthchecks.io':
return <HealthchecksSettings
data={data.healthchecks}
generateInputs={this.generateInputs}
save={this.save} />
case 'InfluxDB':
return <InfluxDBSettings
data={data.influxdb}
generateInputs={this.generateInputs}
save={this.save} />
case 'Reset':
return <ResetSettings
data={data.healthchecks}
generateInputs={this.generateInputs}
save={this.save} />
case 'Backup/Restore':
return <BackupSettings
data={data.healthchecks}
generateInputs={this.generateInputs}
save={this.save} />
case 'Authentication':
return <Authentication
data={data.healthchecks}
generateInputs={this.generateInputs}
save={this.save} />
}
}
render() {
var tabs = this.generateTabs();
var activeTab = this.state.tab;
var tabContent = this.getTabContent();
return (
<div>
<Tabs
variant="tabs"
onSelect={(tab) => { this.switchTab(tab) }}
activeKey={activeTab}
>
{tabs}
</Tabs>
<div className="mt-3">
{tabContent}
</div>
</div>
);
}
}
if (document.getElementById('settingsTabs')) {
ReactDOM.render(<SettingsTabs />, document.getElementById('settingsTabs'));
}

View File

@@ -1,26 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
import DataRow from '../../Data/DataRow';
export default class BackupSettings extends Component {
constructor(props) {
super(props)
this.state = {
}
}
render() {
return (
<Tab.Content>
<DataRow />
</Tab.Content>
);
}
}
if (document.getElementById('BackupSettings')) {
ReactDOM.render(<BackupSettings />, document.getElementById('BackupSettings'));
}

View File

@@ -1,46 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
export default class GeneralSettings extends Component {
constructor(props) {
super(props)
this.state = {
data: this.props.data
}
}
inputHandler = (name, val) => {
var settings = this.state.data;
var i = 0;
settings.forEach(ele => {
if(ele.obj.name == name) {
ele.obj.value = val;
}
settings[i] = ele;
i++;
});
this.setState({
data: settings
});
}
render() {
var settings = this.props.generateInputs(this.state.data, this.inputHandler);
return (
<Tab.Content>
{settings}
<div className="mt-3">
<button className="btn btn-primary" onClick={() => { this.props.save(this.state.data, 'General') }}>Save</button>
</div>
</Tab.Content>
);
}
}
if (document.getElementById('GeneralSettings')) {
ReactDOM.render(<GeneralSettings />, document.getElementById('GeneralSettings'));
}

View File

@@ -1,48 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
import SettingsInput from '../SettingsInput';
export default class GraphsSettings extends Component {
constructor(props) {
super(props)
this.state = {
data: this.props.data
}
}
inputHandler = (name, val) => {
var settings = this.state.data;
var i = 0;
settings.forEach(ele => {
if(ele.obj.name == name) {
ele.obj.value = val;
}
settings[i] = ele;
i++;
});
this.setState({
data: settings
});
}
render() {
var settings = this.props.generateInputs(this.state.data, this.inputHandler);
return (
<Tab.Content>
{settings}
<div className="mt-3">
<button className="btn btn-primary" onClick={() => { this.props.save(this.state.data, 'General') }}>Save</button>
</div>
</Tab.Content>
);
}
}
if (document.getElementById('GraphsSettings')) {
ReactDOM.render(<GraphsSettings />, document.getElementById('GraphsSettings'));
}

View File

@@ -1,48 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
import SettingsInput from '../SettingsInput';
export default class HealthchecksSettings extends Component {
constructor(props) {
super(props)
this.state = {
data: this.props.data
}
}
inputHandler = (name, val) => {
var settings = this.state.data;
var i = 0;
settings.forEach(ele => {
if(ele.obj.name == name) {
ele.obj.value = val;
}
settings[i] = ele;
i++;
});
this.setState({
data: settings
});
}
render() {
var settings = this.props.generateInputs(this.state.data, this.inputHandler);
return (
<Tab.Content>
{settings}
<div className="mt-3">
<button className="btn btn-primary" onClick={() => { this.props.save(this.state.data, 'healthchecks.io') }}>Save</button>
</div>
</Tab.Content>
);
}
}
if (document.getElementById('HealthchecksSettings')) {
ReactDOM.render(<HealthchecksSettings />, document.getElementById('HealthchecksSettings'));
}

View File

@@ -1,50 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
export default class InfluxDBSettings extends Component {
constructor(props) {
super(props)
this.state = {
data: this.props.data,
}
}
inputHandler = (name, val) => {
var settings = this.state.data;
var i = 0;
settings.forEach(ele => {
if(ele.obj.name == name) {
ele.obj.value = val;
}
settings[i] = ele;
i++;
});
this.setState({
data: settings
});
}
render() {
var settings = this.props.generateInputs(this.state.data, this.inputHandler);
return (
<Tab.Content>
<form onSubmit={(e) => { e.preventDefault() }} autoComplete="off">
<input type="text" autoComplete="username" style={{ display: 'none' }} />
<input type="password" name="password" autoComplete="passoword" style={{ display: 'none' }} />
{settings}
</form>
<div className="mt-3">
<button className="btn btn-primary" onClick={() => { this.props.save(this.state.data, 'healthchecks.io') }}>Save</button>
</div>
</Tab.Content>
);
}
}
if (document.getElementById('InfluxDBSettings')) {
ReactDOM.render(<InfluxDBSettings />, document.getElementById('InfluxDBSettings'));
}

View File

@@ -1,46 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
export default class NotificationsSettings extends Component {
constructor(props) {
super(props)
this.state = {
data: this.props.data
}
}
inputHandler = (name, val) => {
var settings = this.state.data;
var i = 0;
settings.forEach(ele => {
if(ele.obj.name == name) {
ele.obj.value = val;
}
settings[i] = ele;
i++;
});
this.setState({
data: settings
});
}
render() {
var settings = this.props.generateInputs(this.state.data, this.inputHandler);
return (
<Tab.Content>
{settings}
<div className="mt-3">
<button className="btn btn-primary" onClick={() => { this.props.save(this.state.data, 'Notifications') }}>Save</button>
</div>
</Tab.Content>
);
}
}
if (document.getElementById('NotificationsSettings')) {
ReactDOM.render(<NotificationsSettings />, document.getElementById('NotificationsSettings'));
}

View File

@@ -1,48 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Button } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
export default class ResetSettings extends Component {
constructor(props) {
super(props)
this.state = {
}
}
deleteAll = () => {
var url = 'api/speedtest/delete/all';
Axios.delete(url)
.then((resp) => {
toast.success('All speedtests have been deleted.');
this.toggleShow();
})
.catch((err) => {
if(err.response.data.error == undefined) {
toast.error('Something went wrong.');
}
toast.error(err.response.data.error);
})
}
render() {
var show = this.state.show;
const title = 'Reset Speedtests';
return (
<>
<h4>Clear all speedtests</h4>
<p className="text-muted">If using SQLite, a backup of the database will be stored in the location of the current database.</p>
<Button onClick={this.deleteAll} variant="danger">Delete all</Button>
</>
);
}
}
if (document.getElementById('ResetSettings')) {
ReactDOM.render(<ResetSettings />, document.getElementById('ResetSettings'));
}

View File

@@ -1,123 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button, Tab } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
export default class TableSettings extends Component {
constructor(props) {
super(props)
this.state = {
visible: this.props.data[0],
hidden: this.props.data[1],
}
}
handleOnDragEnd = (result) => {
if (!result.destination) return;
let visible = this.state.visible;
let hidden = this.state.hidden;
let from = result.source.droppableId == 'visibleColumns' ? visible.obj.value : hidden.obj.value;
let to = result.destination.droppableId == 'visibleColumns' ? visible.obj.value : hidden.obj.value;
let [reorderedItem] = from.splice(result.source.index, 1);
to.splice(result.destination.index, 0, reorderedItem);
this.setState({
visible: visible,
hidden: hidden
});
}
save = () => {
var url = 'api/settings/bulk?token=' + window.token;
Axios.post(url, {
data: [
{
name: 'visible_columns',
value: this.state.visible.obj.value
},
{
name: 'hidden_columns',
value: this.state.hidden.obj.value
}
],
})
.then((resp) => {
toast.success('Table settings updated');
this.props.refreshConfig();
})
.catch((err) => {
toast.error('Something went wrong');
console.log(err);
})
}
render() {
let visible = this.state.visible;
let hidden = this.state.hidden;
return (
<Tab.Content>
<div>
<p>{visible.obj.description}</p>
<DragDropContext onDragEnd={this.handleOnDragEnd}>
<div className="card pt-4 pb-2 px-4 mb-4">
<h4>Visible Columns</h4>
<Droppable droppableId="visibleColumns">
{(provided) => (
<ul className="visibleColumns pl-0" {...provided.droppableProps} ref={provided.innerRef}>
{visible.obj.value.map((e, i) => {
return (
<Draggable draggableId={e} index={i} key={e}>
{(provided) => (
<li className="card bg-secondary py-2 px-3 my-2" key={e} ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>{e}</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</Droppable>
</div>
<div className="card pt-4 pb-2 px-4">
<h4>Hidden Columns</h4>
<Droppable droppableId="hiddenColumns pl-0">
{(provided) => (
<ul className="hiddenColumns pl-0" {...provided.droppableProps} ref={provided.innerRef}>
{hidden.obj.value.map((e, i) => {
return (
<Draggable draggableId={e} index={i} key={e}>
{(provided) => (
<li className="card bg-secondary py-2 px-3 my-2" key={e} ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>{e}</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</Droppable>
</div>
</DragDropContext>
<div className="mt-3">
<button className="btn btn-primary" onClick={() => { this.save() }}>Save</button>
</div>
</div>
</Tab.Content>
);
}
}
if (document.getElementById('TableSettings')) {
ReactDOM.render(<TableSettings />, document.getElementById('TableSettings'));
}

View File

@@ -1,29 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import TestsTable from './Graphics/TestsTable';
import Footer from './Home/Footer';
import Navbar from './Navbar';
export default class SpeedtestsPage extends Component {
constructor(props) {
super(props)
this.state = {
}
}
render() {
return (
<div>
<Navbar />
<TestsTable />
<Footer />
</div>
);
}
}
if (document.getElementById('SpeedtestsPage')) {
ReactDOM.render(<SpeedtestsPage />, document.getElementById('SpeedtestsPage'));
}

125
resources/js/index.js vendored
View File

@@ -1,125 +0,0 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route, Redirect, useHistory } from 'react-router-dom';
import Axios from 'axios';
import ErrorPage from './components/ErrorPage';
import Loader from './components/Loader';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import HomePage from './components/Home/HomePage';
import Cookies from 'js-cookie';
import SettingsIndex from './components/Settings/SettingsIndex';
import SpeedtestsPage from './components/SpeedtestsPage';
export default class Index extends Component {
constructor(props) {
super(props)
this.state = {
loading: true,
redirect: false,
}
}
componentDidMount = () => {
this.getConfig();
}
getConfig() {
var url = 'api/settings/config';
Axios.get(url)
.then((resp) => {
window.config = resp.data;
if(window.config.auth === true) {
var authCookie = Cookies.get('auth');
if(authCookie == undefined) {
window.authenticated = false;
this.setState({
loading: false,
redirect: true,
});
} else {
var url = 'api/auth/me?token=' + authCookie;
Axios.get(url)
.then((resp) => {
window.authenticated = true;
window.token = authCookie;
})
.catch((err) => {
Cookies.remove('auth');
window.authenticated = false;
})
.finally(() => {
this.setState({
loading: false,
redirect: true,
});
})
}
} else {
this.setState({
loading: false,
redirect: true,
});
}
})
}
render() {
var loading = this.state.loading;
var redirect = this.state.redirect;
var baseSet = this.isset(window.config);
if(loading) {
return (
<Loader />
);
} else {
if(baseSet && window.config.base) {
return (
<BrowserRouter>
<Route render={(props) => (<ToastContainer />)} />
<Switch>
<Route exact path={window.config.base} render={(props) => (
<div>
<HomePage />
</div>
)} />
<Route exact path={window.config.base + 'speedtests'} render={(props) => (
<div>
<SpeedtestsPage />
</div>
)} />
<Route exact path={window.config.base + 'settings'} render={(props) => (
<div>
<SettingsIndex refreshConfig={this.getConfig} />
</div>
)} />
<Route exact path={window.config.base + "error/:code"} render={(props) => ( <ErrorPage code={props.match.params.code} /> )} />
<Route render={(props) => (<ErrorPage code="404" />)} />
</Switch>
</BrowserRouter>
);
} else {
return (
<Loader />
);
}
}
}
isset(v) {
if(typeof v !== "undefined" || v !== null) {
return true;
} else {
return false;
}
}
}
if (document.getElementById('main')) {
ReactDOM.render(<Index />, document.getElementById('main'));
}

View File

@@ -1,8 +1,3 @@
// Fonts
@import url('https://fonts.googleapis.com/css?family=Nunito');
// Variables
@import 'variables';
// Bootstrap
@import '~bootstrap/scss/bootstrap';
// Base tailwind files
@import "tailwindcss/base";
@import "tailwindcss/components";

View File

@@ -0,0 +1 @@
@import "tailwindcss/utilities";

View File

@@ -7,35 +7,36 @@
<meta name="author" content="Henry Whitaker">
<meta name="version" content="{{ config('speedtest.version', 'Unknown') }}">
<link href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/themify/themify-icons.css" rel="stylesheet">
<link rel="stylesheet" href="{{ App\Helpers\SettingsHelper::getBase() }}files/css/bootstrap.dark.min.css">
<link rel="stylesheet" href="{{ App\Helpers\SettingsHelper::getBase() }}files/css/main.css?v={{ str_replace('.', '-', config('speedtest.version')) }}">
<link href="/icons/themify/themify-icons.css" rel="stylesheet">
<link rel="apple-touch-icon" sizes="57x57" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/favicon-16x16.png">
<link rel="manifest" href="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/manifest.json">
<link rel="apple-touch-icon" sizes="57x57" href="/icons/fav/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/icons/fav/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/icons/fav/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/icons/fav/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/icons/fav/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/icons/fav/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/icons/fav/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/icons/fav/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/icons/fav/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/fav/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/icons/fav/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/icons/fav/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/icons/fav/favicon-16x16.png">
<link rel="manifest" href="/icons/fav/manifest.json">
<meta name="msapplication-TileColor" content="#303030">
<meta name="msapplication-TileImage" content="{{ App\Helpers\SettingsHelper::getBase() }}files/icons/fav/ms-icon-144x144.png">
<meta name="msapplication-TileImage" content="/icons/fav/ms-icon-144x144.png">
<meta name="theme-color" content="#303030">
<title>{{ $title }}</title>
</head>
<body>
<div id="main"></div>
<title>{{ config('app.name') }}</title>
<script src="{{ App\Helpers\SettingsHelper::getBase() }}files/js/jquery.min.js"></script>
<script src="{{ App\Helpers\SettingsHelper::getBase() }}files/js/popper.min.js"></script>
<script src="{{ App\Helpers\SettingsHelper::getBase() }}files/js/app.js?v={{ str_replace('.', '-', config('speedtest.version')) }}"></script>
<!-- Styles -->
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
@routes
<script src="{{ mix('js/app.js') }}" defer></script>
</head>
<body class="font-sans antialiased">
@inertia
</body>
</html>

View File

@@ -4,6 +4,7 @@ use App\Helpers\SpeedtestHelper;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\BackupController;
use App\Http\Controllers\HomepageDataController;
use App\Http\Controllers\IntegrationsController;
use App\Http\Controllers\SettingsController;
use App\Http\Controllers\SpeedtestController;
use App\Http\Controllers\UpdateController;
@@ -78,9 +79,9 @@ Route::group([
], function () {
Route::get('/config', [SettingsController::class, 'config'])
->name('settings.config');
Route::get('/test-notification', 'IntegrationsController@testNotification')
Route::get('/test-notification', [IntegrationsController::class, 'testNotification'])
->name('settings.test_notification');
Route::get('/test-healthchecks/{method}', 'IntegrationsController@testHealthchecks')
Route::get('/test-healthchecks/{method}', [IntegrationsController::class, 'testHealthchecks'])
->name('settings.test_notification');
Route::get('/', [SettingsController::class, 'index'])
->name('settings.index');

View File

@@ -1,9 +1,11 @@
<?php
use App\Helpers\SettingsHelper;
use GuzzleHttp\Psr7\MimeType;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
/*
|--------------------------------------------------------------------------
@@ -16,20 +18,6 @@ use Illuminate\Support\Facades\Route;
|
*/
Route::get(SettingsHelper::getBase() . 'files/{path?}', function ($file) {
$fileP = explode('?', $file)[0];
$fileP = public_path() . '/' . $fileP;
if (file_exists($fileP)) {
$contents = File::get($fileP);
$mime = \GuzzleHttp\Psr7\mimetype_from_filename($fileP);
return Response::make(File::get($fileP), 200, ['Content-type' => $mime]);
} else {
abort(404);
}
})->where('path', '.*')
->name('files');
Route::get('/{path?}', function () {
return view('app', ['title' => SettingsHelper::get('app_name')->value]);
})->where('path', '^((?!\/api\/).)*$')
->name('react');
Route::get('/', function () {
return Inertia::render('Home');
});

11
tailwind.config.js vendored Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

9
webpack.config.js vendored Normal file
View File

@@ -0,0 +1,9 @@
const path = require('path');
module.exports = {
resolve: {
alias: {
'@': path.resolve('resources/js'),
},
},
};

22
webpack.mix.js vendored
View File

@@ -1,4 +1,7 @@
const mix = require('laravel-mix');
const mix = require("laravel-mix");
require("laravel-mix-tailwind");
/*
|--------------------------------------------------------------------------
@@ -11,5 +14,18 @@ const mix = require('laravel-mix');
|
*/
mix.react('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');
mix.js("resources/js/app.js", "public/js")
.vue()
.webpackConfig(require("./webpack.config"))
.version();
mix.sass("resources/sass/app.scss", "public/css")
.options({
processCssUrls: false,
imgLoaderOptions: {
enabled: false,
},
})
.sass("resources/sass/tailwind-utilities.scss", "public/css/app.css")
.tailwind("./tailwind.config.js")
.version();