Add update lookup and telemetry, Add version and build to app config

This commit is contained in:
Constantin Graf
2024-10-04 21:59:02 +02:00
committed by Gregor Vostrak
parent f147fb9725
commit 2372ee0622
31 changed files with 1195 additions and 680 deletions

View File

@@ -1,4 +1,6 @@
APP_NAME=solidtime APP_NAME=solidtime
APP_VERSION=0.0.0
APP_BUILD=0
VITE_APP_NAME=solidtime VITE_APP_NAME=solidtime
APP_ENV=production APP_ENV=production
APP_DEBUG=false APP_DEBUG=false

View File

@@ -20,15 +20,55 @@ jobs:
steps: steps:
- name: "Check out code" - name: "Check out code"
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
- name: "Get build"
id: build
run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT"
- name: "Get Previous tag (normal push)"
id: previoustag
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
prefix: "v"
- name: "Get version"
id: version
run: |
if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then
if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then
version=$(echo "${{ steps.previoustag.outputs.tag }}" | cut -c 2-)
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
else
echo "ERROR: No previous tag found";
exit 1;
fi
else
version=$(echo "${{ github.ref }}" | cut -c 12-)
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
fi
- name: "Copy .env template for production"
run: |
cp .env.production .env
rm .env.production .env.ci .env.example
- name: "Add version to .env"
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.version.outputs.app_version }}/g' .env
- name: "Add build to .env"
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.build.outputs.build }}/g' .env
- name: "Output .env"
run: cat .env
- name: "Use Node.js" - name: "Use Node.js"
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20.x' node-version: '20.x'
- name: "Copy .env template for production"
run: cp .env.production .env && cat .env
- name: "Checkout billing extension" - name: "Checkout billing extension"
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:

View File

@@ -25,9 +25,49 @@ jobs:
steps: steps:
- name: "Check out code" - name: "Check out code"
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag
- name: "Get build"
id: build
run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT"
- name: "Get Previous tag (normal push)"
id: previoustag
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
prefix: "v"
- name: "Get version"
id: version
run: |
if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then
if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then
version=$(echo "${{ steps.previoustag.outputs.tag }}" | cut -c 2-)
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
else
echo "ERROR: No previous tag found";
exit 1;
fi
else
version=$(echo "${{ github.ref }}" | cut -c 12-)
echo "app_version=${version}" >> "$GITHUB_OUTPUT"
fi
- name: "Copy .env template for production" - name: "Copy .env template for production"
run: cp .env.production .env run: |
cp .env.production .env
rm .env.production .env.ci .env.example
- name: "Add version to .env"
run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.version.outputs.app_version }}/g' .env
- name: "Add build to .env"
run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.build.outputs.build }}/g' .env
- name: "Output .env"
run: cat .env
- name: "Install dependencies" - name: "Install dependencies"
uses: php-actions/composer@v6 uses: php-actions/composer@v6

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\SelfHost;
use App\Service\ApiService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class SelfHostCheckForUpdateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'self-host:check-for-update';
/**
* The console command description.
*
* @var string
*/
protected $description = '';
/**
* Execute the console command.
*/
public function handle(): int
{
$apiService = app(ApiService::class);
$latestVersion = $apiService->checkForUpdate();
if ($latestVersion === null) {
$this->error('Failed to check for update, check the logs for more information.');
return self::FAILURE;
}
// Note: Cache for 13 hours, because the command runs twice daily (every 12 hours).
Cache::put('latest_version', $latestVersion, 60 * 60 * 12);
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\SelfHost;
use App\Service\ApiService;
use Illuminate\Console\Command;
class SelfHostTelemetryCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'self-host:telemetry';
/**
* The console command description.
*
* @var string
*/
protected $description = '';
/**
* Execute the console command.
*/
public function handle(): int
{
$apiService = app(ApiService::class);
$success = $apiService->telemetry();
if (! $success) {
$this->error('Failed to send telemetry data, check the logs for more information.');
return self::FAILURE;
}
return self::SUCCESS;
}
}

View File

@@ -17,6 +17,14 @@ class Kernel extends ConsoleKernel
$schedule->command('time-entry:send-still-running-mails') $schedule->command('time-entry:send-still-running-mails')
->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails')) ->when(fn (): bool => config('scheduling.tasks.time_entry_send_still_running_mails'))
->everyTenMinutes(); ->everyTenMinutes();
$schedule->command('self-hosting:check-for-update')
->when(fn (): bool => config('scheduling.tasks.self_hosting_check_for_update'))
->twiceDaily();
$schedule->command('self-hosting:telemetry')
->when(fn (): bool => config('scheduling.tasks.self_hosting_telemetry'))
->twiceDaily();
} }
/** /**

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use Filament\Widgets\Widget;
use Illuminate\Support\Facades\Cache;
class ServerOverview extends Widget
{
protected static string $view = 'filament.widgets.server-overview';
/**
* @return array<string, mixed>
*/
protected function getViewData(): array
{
$currentVersion = Cache::get('latest_version', null);
$needsUpdate = false;
if ($currentVersion !== null && version_compare($currentVersion, config('app.version')) > 0) {
$needsUpdate = true;
}
return [
'version' => config('app.version'),
'build' => config('app.build'),
'environment' => config('app.env'),
'currentVersion' => $currentVersion,
'needsUpdate' => $needsUpdate,
];
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Providers\Filament; namespace App\Providers\Filament;
use App\Filament\Widgets\ActiveUserOverview; use App\Filament\Widgets\ActiveUserOverview;
use App\Filament\Widgets\ServerOverview;
use App\Filament\Widgets\TimeEntriesCreated; use App\Filament\Widgets\TimeEntriesCreated;
use App\Filament\Widgets\TimeEntriesImported; use App\Filament\Widgets\TimeEntriesImported;
use App\Filament\Widgets\UserRegistrations; use App\Filament\Widgets\UserRegistrations;
@@ -44,11 +45,13 @@ class AdminPanelProvider extends PanelProvider
]) ])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets') ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([ ->widgets([
ServerOverview::class,
ActiveUserOverview::class, ActiveUserOverview::class,
UserRegistrations::class, UserRegistrations::class,
TimeEntriesCreated::class, TimeEntriesCreated::class,
TimeEntriesImported::class, TimeEntriesImported::class,
]) ])
->viteTheme('resources/css/filament/admin/theme.css')
->plugins([ ->plugins([
EnvironmentIndicatorPlugin::make() EnvironmentIndicatorPlugin::make()
->color(fn () => match (App::environment()) { ->color(fn () => match (App::environment()) {

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Models\Audit;
use App\Models\Client;
use App\Models\Organization;
use App\Models\Project;
use App\Models\ProjectMember;
use App\Models\Task;
use App\Models\TimeEntry;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\Http;
use Log;
class ApiService
{
private const string API_URL = 'https://app.solidtime.io/api';
public function checkForUpdate(): ?string
{
try {
$response = Http::asJson()
->timeout(3)
->connectTimeout(2)
->post(self::API_URL.'/check-for-update', [
'version' => config('app.version'),
'build' => config('app.build'),
'url' => config('app.url'),
]);
if ($response->status() === 200 && isset($response->json()['version']) && is_string($response->json()['version'])) {
return $response->json()['version'];
} else {
Log::warning('Failed to check for update', [
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
}
} catch (\Throwable $e) {
Log::warning('Failed to check for update', [
'message' => $e->getMessage(),
]);
return null;
}
}
public function telemetry(): bool
{
try {
$response = Http::asJson()
->timeout(3)
->connectTimeout(2)
->post(self::API_URL.'/telemetry', [
'version' => config('app.version'),
'build' => config('app.build'),
'url' => config('app.url'),
// telemetry data
'user_count' => User::count(),
'organization_count' => Organization::count(),
'audit_count' => Audit::count(),
'project_count' => Project::count(),
'project_member_count' => ProjectMember::count(),
'client_count' => Client::count(),
'task_count' => Task::count(),
'time_entry_count' => TimeEntry::count(),
]);
if ($response->status() === 200) {
return true;
} else {
Log::warning('Failed send telemetry data', [
'status' => $response->status(),
'body' => $response->body(),
]);
return false;
}
} catch (Exception $e) {
Log::warning('Failed send telemetry data', [
'message' => $e->getMessage(),
]);
return false;
}
}
}

View File

@@ -2,7 +2,6 @@
"name": "solidtime-io/solidtime", "name": "solidtime-io/solidtime",
"type": "project", "type": "project",
"description": "An open-source time-tracking app", "description": "An open-source time-tracking app",
"version": "0.0.1",
"keywords": [], "keywords": [],
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"require": { "require": {
@@ -29,7 +28,7 @@
"spatie/temporary-directory": "^2.2", "spatie/temporary-directory": "^2.2",
"stechstudio/filament-impersonate": "^3.8", "stechstudio/filament-impersonate": "^3.8",
"tightenco/ziggy": "^2.1.0", "tightenco/ziggy": "^2.1.0",
"tpetry/laravel-postgresql-enhanced": "^1.0.0", "tpetry/laravel-postgresql-enhanced": "^2.0.0",
"wikimedia/composer-merge-plugin": "^2.1.0" "wikimedia/composer-merge-plugin": "^2.1.0"
}, },
"require-dev": { "require-dev": {

1172
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,11 @@ return [
| |
*/ */
'name' => env('APP_NAME', 'solidtime'), 'name' => 'solidtime',
'version' => env('APP_VERSION'),
'build' => env('APP_BUILD'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@@ -6,5 +6,7 @@ return [
'tasks' => [ 'tasks' => [
'time_entry_send_still_running_mails' => (bool) env('SCHEDULING_TASK_TIME_ENTRY_SEND_STILL_RUNNING_MAILS', true), 'time_entry_send_still_running_mails' => (bool) env('SCHEDULING_TASK_TIME_ENTRY_SEND_STILL_RUNNING_MAILS', true),
'self_hosting_check_for_update' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_CHECK_FOR_UPDATE', true),
'self_hosting_telemetry' => (bool) env('SCHEDULING_TASK_SELF_HOSTING_TELEMETRY', true),
], ],
]; ];

66
package-lock.json generated
View File

@@ -1,5 +1,5 @@
{ {
"name": "html", "name": "solidtime",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@@ -28,19 +28,19 @@
"devDependencies": { "devDependencies": {
"@inertiajs/vue3": "^1.0.0", "@inertiajs/vue3": "^1.0.0",
"@playwright/test": "^1.41.1", "@playwright/test": "^1.41.1",
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.2", "@tailwindcss/typography": "^0.5.15",
"@types/node": "^20.11.5", "@types/node": "^20.11.5",
"@vitejs/plugin-vue": "^4.5.0", "@vitejs/plugin-vue": "^4.5.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.20",
"axios": "^1.6.4", "axios": "^1.6.4",
"eslint-plugin-unused-imports": "^3.1.0", "eslint-plugin-unused-imports": "^3.1.0",
"laravel-vite-plugin": "^1.0.0", "laravel-vite-plugin": "^1.0.0",
"openapi-zod-client": "^1.16.2", "openapi-zod-client": "^1.16.2",
"postcss": "^8.4.14", "postcss": "^8.4.47",
"postcss-nesting": "^12.1.0", "postcss-nesting": "^12.1.5",
"tailwindcss": "^3.1.0", "tailwindcss": "^3.4.13",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-plugin-checker": "^0.7.2", "vite-plugin-checker": "^0.7.2",
@@ -1639,24 +1639,22 @@
} }
}, },
"node_modules/@tailwindcss/forms": { "node_modules/@tailwindcss/forms": {
"version": "0.5.7", "version": "0.5.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz",
"integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==", "integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"mini-svg-data-uri": "^1.2.3" "mini-svg-data-uri": "^1.2.3"
}, },
"peerDependencies": { "peerDependencies": {
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20"
} }
}, },
"node_modules/@tailwindcss/typography": { "node_modules/@tailwindcss/typography": {
"version": "0.5.14", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.14.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz",
"integrity": "sha512-ZvOCjUbsJBjL9CxQBn+VEnFpouzuKhxh2dH8xMIWHILL+HfOYtlAkWcyoon8LlzE53d2Yo6YO6pahKKNW3q1YQ==", "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"lodash.castarray": "^4.4.0", "lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6", "lodash.isplainobject": "^4.0.6",
@@ -1664,7 +1662,7 @@
"postcss-selector-parser": "6.0.10" "postcss-selector-parser": "6.0.10"
}, },
"peerDependencies": { "peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders" "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20"
} }
}, },
"node_modules/@tanstack/match-sorter-utils": { "node_modules/@tanstack/match-sorter-utils": {
@@ -2649,7 +2647,6 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"browserslist": "^4.23.3", "browserslist": "^4.23.3",
"caniuse-lite": "^1.0.30001646", "caniuse-lite": "^1.0.30001646",
@@ -4892,10 +4889,9 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
"license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@@ -5012,9 +5008,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.41", "version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -5029,11 +5025,10 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.1", "picocolors": "^1.1.0",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.1"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@@ -5175,7 +5170,6 @@
"url": "https://opencollective.com/csstools" "url": "https://opencollective.com/csstools"
} }
], ],
"license": "MIT-0",
"dependencies": { "dependencies": {
"@csstools/selector-resolve-nested": "^1.1.0", "@csstools/selector-resolve-nested": "^1.1.0",
"@csstools/selector-specificity": "^3.1.1", "@csstools/selector-specificity": "^3.1.1",
@@ -5657,10 +5651,9 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -5880,10 +5873,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.10", "version": "3.4.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",

View File

@@ -13,19 +13,19 @@
"devDependencies": { "devDependencies": {
"@inertiajs/vue3": "^1.0.0", "@inertiajs/vue3": "^1.0.0",
"@playwright/test": "^1.41.1", "@playwright/test": "^1.41.1",
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.2", "@tailwindcss/typography": "^0.5.15",
"@types/node": "^20.11.5", "@types/node": "^20.11.5",
"@vitejs/plugin-vue": "^4.5.0", "@vitejs/plugin-vue": "^4.5.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.20",
"axios": "^1.6.4", "axios": "^1.6.4",
"eslint-plugin-unused-imports": "^3.1.0", "eslint-plugin-unused-imports": "^3.1.0",
"laravel-vite-plugin": "^1.0.0", "laravel-vite-plugin": "^1.0.0",
"openapi-zod-client": "^1.16.2", "openapi-zod-client": "^1.16.2",
"postcss": "^8.4.14", "postcss": "^8.4.47",
"postcss-nesting": "^12.1.0", "postcss-nesting": "^12.1.5",
"tailwindcss": "^3.1.0", "tailwindcss": "^3.4.13",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-plugin-checker": "^0.7.2", "vite-plugin-checker": "^0.7.2",

View File

@@ -0,0 +1,10 @@
import preset from '../../../../vendor/filament/filament/tailwind.config.preset'
export default {
presets: [preset],
content: [
'./app/Filament/**/*.php',
'./resources/views/filament/**/*.blade.php',
'./vendor/filament/**/*.blade.php',
],
}

View File

@@ -0,0 +1,3 @@
@import '/vendor/filament/filament/resources/css/theme.css';
@config 'tailwind.config.js';

View File

@@ -0,0 +1,28 @@
<x-filament-widgets::widget>
<x-filament::section>
<div>
<span class="text-gray-950 font-bold">Version</span> <span>v{{ $version }}</span><br>
<span class="text-gray-950 font-bold">Build</span> {{ $build }}
</div>
@if ($currentVersion !== null)
<div class="mt-4 inline-flex items-center justify-center gap-1">
@if ($needsUpdate)
<span>
<x-filament::icon
icon="heroicon-o-exclamation-triangle"
class="h-5 w-5 text-orange-500 dark:text-orange-400"
/>
</span>
<span>Update available (v{{ $currentVersion }})</span>
@else
<x-filament::icon
icon="heroicon-o-check-circle"
class="h-5 w-5 text-green-500 dark:text-green-400"
/>
<span>Current version</span>
@endif
</div>
@endif
</x-filament::section>
</x-filament-widgets::widget>

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Console\Commands\SelfHost;
use App\Console\Commands\SelfHost\SelfHostCheckForUpdateCommand;
use App\Service\ApiService;
use Cache;
use Illuminate\Console\Command;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use Tests\TestCase;
#[CoversClass(SelfHostCheckForUpdateCommand::class)]
#[CoversClass(ApiService::class)]
#[UsesClass(SelfHostCheckForUpdateCommand::class)]
class SelfHostCheckForUpdateCommandTest extends TestCase
{
public function test_checks_for_update_and_saves_version_in_cache(): void
{
// Arrange
Http::fake([
'https://app.solidtime.io/api/check-for-update' => Http::response(['version' => '1.2.3'], 200),
]);
// Act
$exitCode = $this->withoutMockingConsoleOutput()->artisan('self-host:check-for-update');
// Assert
$this->assertSame(Command::SUCCESS, $exitCode);
$output = Artisan::output();
$this->assertSame('1.2.3', Cache::get('latest_version'));
}
public function test_checks_for_update_fails_gracefully_if_response_has_error_status_code(): void
{
// Arrange
Http::fake([
'https://app.solidtime.io/api/check-for-update' => Http::response(null, 500),
]);
// Act
$exitCode = $this->withoutMockingConsoleOutput()->artisan('self-host:check-for-update');
// Assert
$this->assertSame(Command::FAILURE, $exitCode);
$output = Artisan::output();
$this->assertStringContainsString('Failed to check for update, check the logs for more information.', $output);
}
public function test_checks_for_update_fails_gracefully_if_timeout_happens(): void
{
// Arrange
Http::fake([
'https://app.solidtime.io/api/check-for-update' => function (): void {
throw new ConnectionException('Connection timed out');
},
]);
// Act
$exitCode = $this->withoutMockingConsoleOutput()->artisan('self-host:check-for-update');
// Assert
$this->assertSame(Command::FAILURE, $exitCode);
$output = Artisan::output();
$this->assertStringContainsString('Failed to check for update, check the logs for more information.', $output);
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Console\Commands\SelfHost;
use App\Console\Commands\SelfHost\SelfHostTelemetryCommand;
use App\Service\ApiService;
use Illuminate\Console\Command;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use Tests\TestCase;
#[CoversClass(SelfHostTelemetryCommand::class)]
#[CoversClass(ApiService::class)]
#[UsesClass(SelfHostTelemetryCommand::class)]
class SelfHostTelemetryCommandTest extends TestCase
{
public function test_telemetry_sends_data_to_telemetry_endpoint_of_solidtime_cloud(): void
{
// Arrange
Http::fake([
'https://app.solidtime.io/api/telemetry' => Http::response(['success' => true], 200),
]);
// Act
$exitCode = $this->withoutMockingConsoleOutput()->artisan('self-host:telemetry');
// Assert
$this->assertSame(Command::SUCCESS, $exitCode);
$output = Artisan::output();
$this->assertSame('', $output);
}
public function test_telemetry_sends_fails_gracefully_if_response_has_error_status_code(): void
{
// Arrange
Http::fake([
'https://app.solidtime.io/api/telemetry' => Http::response(null, 500),
]);
// Act
$exitCode = $this->withoutMockingConsoleOutput()->artisan('self-host:telemetry');
// Assert
$this->assertSame(Command::FAILURE, $exitCode);
$output = Artisan::output();
$this->assertStringContainsString('Failed to send telemetry data, check the logs for more information.', $output);
}
public function test_telemetry_sends_fails_gracefully_if_timeout_happens(): void
{
// Arrange
Http::fake([
'https://app.solidtime.io/api/telemetry' => function (): void {
throw new ConnectionException('Connection timed out');
},
]);
// Act
$exitCode = $this->withoutMockingConsoleOutput()->artisan('self-host:telemetry');
// Assert
$this->assertSame(Command::FAILURE, $exitCode);
$output = Artisan::output();
$this->assertStringContainsString('Failed to send telemetry data, check the logs for more information.', $output);
}
}

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Unit\Filament; namespace Tests\Unit\Filament\Resources;
use App\Filament\Resources\AuditResource; use App\Filament\Resources\AuditResource;
use App\Models\Audit; use App\Models\Audit;
@@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Livewire; use Livewire\Livewire;
use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesClass;
use Tests\Unit\Filament\FilamentTestCase;
#[UsesClass(AuditResource::class)] #[UsesClass(AuditResource::class)]
class AuditResourceTest extends FilamentTestCase class AuditResourceTest extends FilamentTestCase

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Unit\Filament; namespace Tests\Unit\Filament\Resources;
use App\Filament\Resources\ClientResource; use App\Filament\Resources\ClientResource;
use App\Models\Client; use App\Models\Client;
@@ -10,6 +10,7 @@ use App\Models\User;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Livewire\Livewire; use Livewire\Livewire;
use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesClass;
use Tests\Unit\Filament\FilamentTestCase;
#[UsesClass(ClientResource::class)] #[UsesClass(ClientResource::class)]
class ClientResourceTest extends FilamentTestCase class ClientResourceTest extends FilamentTestCase

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Unit\Filament; namespace Tests\Unit\Filament\Resources;
use App\Filament\Resources\FailedJobResource; use App\Filament\Resources\FailedJobResource;
use App\Filament\Resources\FailedJobResource\Pages\ViewFailedJobs; use App\Filament\Resources\FailedJobResource\Pages\ViewFailedJobs;
@@ -11,6 +11,7 @@ use App\Models\User;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Livewire\Livewire; use Livewire\Livewire;
use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesClass;
use Tests\Unit\Filament\FilamentTestCase;
#[UsesClass(FailedJobResource::class)] #[UsesClass(FailedJobResource::class)]
class FailedJobResourceTest extends FilamentTestCase class FailedJobResourceTest extends FilamentTestCase

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Unit\Filament; namespace Tests\Unit\Filament\Resources;
use App\Filament\Resources\OrganizationResource; use App\Filament\Resources\OrganizationResource;
use App\Models\Organization; use App\Models\Organization;
@@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Config;
use Livewire\Livewire; use Livewire\Livewire;
use Mockery\MockInterface; use Mockery\MockInterface;
use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesClass;
use Tests\Unit\Filament\FilamentTestCase;
#[UsesClass(OrganizationResource::class)] #[UsesClass(OrganizationResource::class)]
class OrganizationResourceTest extends FilamentTestCase class OrganizationResourceTest extends FilamentTestCase

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Unit\Filament; namespace Tests\Unit\Filament\Resources;
use App\Filament\Resources\ProjectResource; use App\Filament\Resources\ProjectResource;
use App\Models\Project; use App\Models\Project;
@@ -10,6 +10,7 @@ use App\Models\User;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Livewire\Livewire; use Livewire\Livewire;
use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesClass;
use Tests\Unit\Filament\FilamentTestCase;
#[UsesClass(ProjectResource::class)] #[UsesClass(ProjectResource::class)]
class ProjectResourceTest extends FilamentTestCase class ProjectResourceTest extends FilamentTestCase

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Unit\Filament; namespace Tests\Unit\Filament\Resources;
use App\Filament\Resources\TagResource; use App\Filament\Resources\TagResource;
use App\Models\Tag; use App\Models\Tag;
@@ -10,6 +10,7 @@ use App\Models\User;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Livewire\Livewire; use Livewire\Livewire;
use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesClass;
use Tests\Unit\Filament\FilamentTestCase;
#[UsesClass(TagResource::class)] #[UsesClass(TagResource::class)]
class TagResourceTest extends FilamentTestCase class TagResourceTest extends FilamentTestCase

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Unit\Filament; namespace Tests\Unit\Filament\Resources;
use App\Filament\Resources\TaskResource; use App\Filament\Resources\TaskResource;
use App\Models\Task; use App\Models\Task;
@@ -10,6 +10,7 @@ use App\Models\User;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Livewire\Livewire; use Livewire\Livewire;
use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesClass;
use Tests\Unit\Filament\FilamentTestCase;
#[UsesClass(TaskResource::class)] #[UsesClass(TaskResource::class)]
class TaskResourceTest extends FilamentTestCase class TaskResourceTest extends FilamentTestCase

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Unit\Filament; namespace Tests\Unit\Filament\Resources;
use App\Filament\Resources\TimeEntryResource; use App\Filament\Resources\TimeEntryResource;
use App\Models\TimeEntry; use App\Models\TimeEntry;
@@ -10,6 +10,7 @@ use App\Models\User;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Livewire\Livewire; use Livewire\Livewire;
use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesClass;
use Tests\Unit\Filament\FilamentTestCase;
#[UsesClass(TimeEntryResource::class)] #[UsesClass(TimeEntryResource::class)]
class TimeEntryResourceTest extends FilamentTestCase class TimeEntryResourceTest extends FilamentTestCase

View File

@@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Unit\Filament; namespace Tests\Unit\Filament\Resources;
use App\Exceptions\Api\CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers; use App\Exceptions\Api\CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers;
use App\Filament\Resources\TimeEntryResource; use App\Filament\Resources\TimeEntryResource;
@@ -13,6 +13,7 @@ use Illuminate\Support\Facades\Config;
use Livewire\Livewire; use Livewire\Livewire;
use Mockery\MockInterface; use Mockery\MockInterface;
use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesClass;
use Tests\Unit\Filament\FilamentTestCase;
#[UsesClass(TimeEntryResource::class)] #[UsesClass(TimeEntryResource::class)]
class UserResourceTest extends FilamentTestCase class UserResourceTest extends FilamentTestCase

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Filament\Widgets;
use App\Filament\Widgets\ServerOverview;
use App\Models\User;
use Cache;
use Illuminate\Support\Facades\Config;
use Livewire\Livewire;
use PHPUnit\Framework\Attributes\UsesClass;
use Tests\Unit\Filament\FilamentTestCase;
#[UsesClass(ServerOverview::class)]
class ServerOverviewWidgetTest extends FilamentTestCase
{
protected function setUp(): void
{
parent::setUp();
Config::set('auth.super_admins', ['admin@example.com']);
$user = User::factory()->withPersonalOrganization()->create([
'email' => 'admin@example.com',
]);
$this->actingAs($user);
}
public function test_shows_version_and_build_it_no_information_about_the_current_version_exists(): void
{
// Arrange
Config::set('app.version', '1.0.0');
Config::set('app.build', 'ABC123');
Cache::forget('latest_version');
// Act
$response = Livewire::test(ServerOverview::class);
// Assert
$response->assertSuccessful();
$response->assertSee('1.0.0');
$response->assertSee('ABC123');
$response->assertDontSee('Update available');
$response->assertDontSee('Current version');
}
public function test_show_version_is_current_when_the_latest_version_is_the_same_as_the_current_version(): void
{
// Arrange
Config::set('app.version', '1.0.0');
Config::set('app.build', 'ABC123');
Cache::put('latest_version', '1.0.0');
// Act
$response = Livewire::test(ServerOverview::class);
// Assert
$response->assertSuccessful();
$response->assertSee('1.0.0');
$response->assertSee('ABC123');
$response->assertDontSee('Update available');
$response->assertSee('Current version');
}
public function test_shows_update_available(): void
{
// Arrange
Config::set('app.version', '1.0.0');
Config::set('app.build', 'ABC123');
Cache::put('latest_version', '1.0.1');
// Act
$response = Livewire::test(ServerOverview::class);
// Assert
$response->assertSuccessful();
$response->assertSee('1.0.0');
$response->assertSee('ABC123');
$response->assertSee('Update available');
$response->assertDontSee('Current version');
$response->assertSee('1.0.1');
}
}

View File

@@ -8,7 +8,7 @@ import {
} from './vite-module-loader.js'; } from './vite-module-loader.js';
async function getConfig() { async function getConfig() {
const paths = ['resources/js/app.ts', 'resources/css/app.css']; const paths = ['resources/js/app.ts', 'resources/css/app.css', 'resources/css/filament/admin/theme.css'];
const modulePaths = await collectModuleAssetsPaths('extensions'); const modulePaths = await collectModuleAssetsPaths('extensions');
const additionalPlugins = await collectModulePlugins('extensions'); const additionalPlugins = await collectModulePlugins('extensions');