chore: restructure views and add Livewire components for dashboard functionality

- remove flux dashboard
- add DTOs for the connecter services

Deps:
- spatie/laravel-data
- blade-icons (lucide-icons)
- tailwind-merge
This commit is contained in:
kushal-saha 2026-05-08 07:20:59 +00:00
parent 79c3751d5f
commit 4a5436a3b4
24 changed files with 1415 additions and 193 deletions

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ConnectedServices;
use Illuminate\Support\Collection;
/**
* A collection of connected services.
*
* @extends Collection<int, ConnectedServiceData>
*/
final class ConnectedServiceCollectionData extends Collection {}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ConnectedServices;
use App\Enums\ConnectionStatus;
use App\Enums\ConnectionTechTypes;
use Livewire\Wireable;
use Spatie\LaravelData\Concerns\WireableData;
use Spatie\LaravelData\Data;
final class ConnectedServiceData extends Data implements Wireable
{
use WireableData;
public function __construct(
public string $name,
public string $icon,
public ConnectionTechTypes $type,
public string $connectionService,
public ConnectionStatus $status,
) {}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Data\Ui\ConnectedServices;
use Livewire\Wireable;
use Spatie\LaravelData\Concerns\WireableData;
use Spatie\LaravelData\Data;
final class ConnectedServicesData extends Data implements Wireable
{
use WireableData;
public function __construct(
public ConnectedServiceCollectionData $services,
) {}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Enums;
enum ConnectionStatus: string
{
case Connected = 'connected';
case Disconnected = 'disconnected';
case Pending = 'pending';
case Error = 'error';
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Enums;
enum ConnectionTechTypes: string
{
case OIDC = 'oidc';
case SAML = 'saml';
}

View File

@ -10,11 +10,14 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.3", "php": "^8.3",
"gehrisandro/tailwind-merge-laravel": "^1.4",
"laravel/fortify": "^1.34", "laravel/fortify": "^1.34",
"laravel/framework": "^13.7", "laravel/framework": "^13.7",
"laravel/tinker": "^3.0", "laravel/tinker": "^3.0",
"livewire/flux": "^2.13.1", "livewire/flux": "^2.13.1",
"livewire/livewire": "^4.1" "livewire/livewire": "^4.1",
"mallardduck/blade-lucide-icons": "^1.26",
"spatie/laravel-data": "^4.22"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.24", "fakerphp/faker": "^1.24",
@ -104,4 +107,4 @@
}, },
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true "prefer-stable": true
} }

686
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "d3f7e7c18d3f8f9bc93bbbe0115479c5", "content-hash": "2e46083521ccb7753e903364bb87f40b",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@ -61,6 +61,87 @@
}, },
"time": "2026-04-05T21:06:35+00:00" "time": "2026-04-05T21:06:35+00:00"
}, },
{
"name": "blade-ui-kit/blade-icons",
"version": "1.10.0",
"source": {
"type": "git",
"url": "https://github.com/driesvints/blade-icons.git",
"reference": "74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/driesvints/blade-icons/zipball/74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a",
"reference": "74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a",
"shasum": ""
},
"require": {
"illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
"illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
"illuminate/view": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
"php": "^7.4|^8.0",
"symfony/console": "^5.3|^6.0|^7.0|^8.0",
"symfony/finder": "^5.3|^6.0|^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.5.1",
"orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
"phpunit/phpunit": "^9.0|^10.5|^11.0"
},
"bin": [
"bin/blade-icons-generate"
],
"type": "library",
"extra": {
"laravel": {
"providers": [
"BladeUI\\Icons\\BladeIconsServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"BladeUI\\Icons\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dries Vints",
"homepage": "https://driesvints.com"
}
],
"description": "A package to easily make use of icons in your Laravel Blade views.",
"homepage": "https://github.com/driesvints/blade-icons",
"keywords": [
"blade",
"icons",
"laravel",
"svg"
],
"support": {
"issues": "https://github.com/driesvints/blade-icons/issues",
"source": "https://github.com/driesvints/blade-icons"
},
"funding": [
{
"url": "https://github.com/sponsors/driesvints",
"type": "github"
},
{
"url": "https://www.paypal.com/paypalme/driesvints",
"type": "paypal"
}
],
"time": "2026-04-23T19:03:45+00:00"
},
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.14.8", "version": "0.14.8",
@ -732,6 +813,147 @@
], ],
"time": "2025-12-03T09:33:47+00:00" "time": "2025-12-03T09:33:47+00:00"
}, },
{
"name": "gehrisandro/tailwind-merge-laravel",
"version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/gehrisandro/tailwind-merge-laravel.git",
"reference": "4dfc54d2f1c148b87a7f9803b3bae74c3437e7d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/gehrisandro/tailwind-merge-laravel/zipball/4dfc54d2f1c148b87a7f9803b3bae74c3437e7d9",
"reference": "4dfc54d2f1c148b87a7f9803b3bae74c3437e7d9",
"shasum": ""
},
"require": {
"gehrisandro/tailwind-merge-php": "^v1.1.0",
"guzzlehttp/guzzle": "^7.5.1",
"laravel/framework": "^12.0|^13.0",
"php": "^8.2.0"
},
"require-dev": {
"laravel/pint": "^1.13.8",
"orchestra/testbench": "^10.0|^11.0",
"pestphp/pest": "^3.7|^4.4",
"pestphp/pest-plugin-arch": "^3.0|^4.0",
"pestphp/pest-plugin-type-coverage": "^3.3|^4.0",
"phpstan/phpstan": "^1.10.55|^2.1",
"rector/rector": "^0.19|^2.0",
"symfony/var-dumper": "^6.4.2|^7.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"TailwindMerge\\Laravel\\TailwindMergeServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"TailwindMerge\\Laravel\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sandro Gehri",
"email": "sandrogehri@gmail.com"
}
],
"description": "TailwindMerge for Laravel merges multiple Tailwind CSS classes by automatically resolving conflicts between them",
"keywords": [
"classes",
"laravel",
"merge",
"php",
"tailwindcss"
],
"support": {
"issues": "https://github.com/gehrisandro/tailwind-merge-laravel/issues",
"source": "https://github.com/gehrisandro/tailwind-merge-laravel/tree/v1.4.0"
},
"funding": [
{
"url": "https://github.com/gehrisandro",
"type": "github"
}
],
"time": "2026-03-21T06:54:49+00:00"
},
{
"name": "gehrisandro/tailwind-merge-php",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/gehrisandro/tailwind-merge-php.git",
"reference": "400040c89bc958ed130699ee4db0213f689cd498"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/gehrisandro/tailwind-merge-php/zipball/400040c89bc958ed130699ee4db0213f689cd498",
"reference": "400040c89bc958ed130699ee4db0213f689cd498",
"shasum": ""
},
"require": {
"php": "^8.2.0",
"psr/simple-cache": "^3.0"
},
"require-dev": {
"laravel/pint": "^1.13.8",
"nunomaduro/collision": "^v8.9.1",
"pestphp/pest": "^v4.4.2",
"pestphp/pest-plugin-type-coverage": "^v4.0.3",
"phpstan/phpstan": "^2.1.42",
"rector/rector": "^2.3.9",
"symfony/var-dumper": "^v7.4.6"
},
"type": "library",
"autoload": {
"files": [
"src/TailwindMerge.php"
],
"psr-4": {
"TailwindMerge\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sandro Gehri",
"email": "sandrogehri@gmail.com"
}
],
"description": "TailwindMerge for PHP merges multiple Tailwind CSS classes by automatically resolving conflicts between them",
"keywords": [
"classes",
"merge",
"php",
"tailwindcss"
],
"support": {
"issues": "https://github.com/gehrisandro/tailwind-merge-php/issues",
"source": "https://github.com/gehrisandro/tailwind-merge-php/tree/v1.2.0"
},
"funding": [
{
"url": "https://github.com/gehrisandro",
"type": "github"
}
],
"time": "2026-03-21T06:39:15+00:00"
},
{ {
"name": "graham-campbell/result-type", "name": "graham-campbell/result-type",
"version": "v1.1.4", "version": "v1.1.4",
@ -2452,6 +2674,66 @@
], ],
"time": "2026-05-01T00:46:07+00:00" "time": "2026-05-01T00:46:07+00:00"
}, },
{
"name": "mallardduck/blade-lucide-icons",
"version": "1.26.19",
"source": {
"type": "git",
"url": "https://github.com/mallardduck/blade-lucide-icons.git",
"reference": "a6e164dca10d29fd67ca332b9ef78bf2b35e0df8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mallardduck/blade-lucide-icons/zipball/a6e164dca10d29fd67ca332b9ef78bf2b35e0df8",
"reference": "a6e164dca10d29fd67ca332b9ef78bf2b35e0df8",
"shasum": ""
},
"require": {
"blade-ui-kit/blade-icons": "^1.8",
"php": "^8.1"
},
"require-dev": {
"ext-dom": "*",
"ext-fileinfo": "*",
"orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0",
"phpunit/phpunit": "^9.0|^10.0|^11.0",
"spatie/phpunit-snapshot-assertions": "^4.2.14|^5.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"MallardDuck\\LucideIcons\\BladeLucideIconsServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"MallardDuck\\LucideIcons\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dan Pock"
}
],
"description": "A package to easily make use of Lucide icons in your Laravel Blade views.",
"homepage": "https://github.com/mallardduck/blade-lucide-icons",
"keywords": [
"LucideIcons",
"blade",
"laravel"
],
"support": {
"issues": "https://github.com/mallardduck/blade-lucide-icons/issues",
"source": "https://github.com/mallardduck/blade-lucide-icons/tree/1.26.19"
},
"time": "2026-04-30T00:36:39+00:00"
},
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "3.10.0", "version": "3.10.0",
@ -3032,6 +3314,78 @@
}, },
"time": "2025-09-24T15:06:41+00:00" "time": "2025-09-24T15:06:41+00:00"
}, },
{
"name": "phpdocumentor/reflection",
"version": "6.6.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/Reflection.git",
"reference": "c8d36446027506a005103d57265ba5ea56beabfc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/c8d36446027506a005103d57265ba5ea56beabfc",
"reference": "c8d36446027506a005103d57265ba5ea56beabfc",
"shasum": ""
},
"require": {
"composer-runtime-api": "^2",
"nikic/php-parser": "~4.18 || ^5.0",
"php": "8.1.*|8.2.*|8.3.*|8.4.*|8.5.*",
"phpdocumentor/reflection-common": "^2.1",
"phpdocumentor/reflection-docblock": "^5",
"phpdocumentor/type-resolver": "^1.4",
"symfony/polyfill-php80": "^1.28",
"webmozart/assert": "^1.7"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"doctrine/coding-standard": "^13.0",
"eliashaeussler/phpunit-attributes": "^1.8",
"mikey179/vfsstream": "~1.2",
"mockery/mockery": "~1.6.0",
"phpspec/prophecy-phpunit": "^2.4",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-webmozart-assert": "^1.2",
"phpunit/phpunit": "^10.5.53",
"psalm/phar": "^6.0",
"rector/rector": "^1.0.0",
"squizlabs/php_codesniffer": "^3.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-5.x": "5.3.x-dev",
"dev-6.x": "6.0.x-dev"
}
},
"autoload": {
"files": [
"src/php-parser/Modifiers.php"
],
"psr-4": {
"phpDocumentor\\": "src/phpDocumentor"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Reflection library to do Static Analysis for PHP Projects",
"homepage": "http://www.phpdoc.org",
"keywords": [
"phpDocumentor",
"phpdoc",
"reflection",
"static analysis"
],
"support": {
"issues": "https://github.com/phpDocumentor/Reflection/issues",
"source": "https://github.com/phpDocumentor/Reflection/tree/6.6.0"
},
"time": "2026-04-12T18:07:10+00:00"
},
{ {
"name": "phpdocumentor/reflection-common", "name": "phpdocumentor/reflection-common",
"version": "2.2.0", "version": "2.2.0",
@ -4070,6 +4424,228 @@
}, },
"time": "2025-12-14T04:43:48+00:00" "time": "2025-12-14T04:43:48+00:00"
}, },
{
"name": "spatie/laravel-data",
"version": "4.22.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-data.git",
"reference": "ec254c0ebc3f3b37515cd7449e2dbb10588e606b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-data/zipball/ec254c0ebc3f3b37515cd7449e2dbb10588e606b",
"reference": "ec254c0ebc3f3b37515cd7449e2dbb10588e606b",
"shasum": ""
},
"require": {
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"php": "^8.1",
"phpdocumentor/reflection": "^6.0",
"spatie/laravel-package-tools": "^1.9.0",
"spatie/php-structure-discoverer": "^2.0"
},
"require-dev": {
"fakerphp/faker": "^1.14",
"friendsofphp/php-cs-fixer": "^3.0",
"inertiajs/inertia-laravel": "^2.0|^3.0",
"livewire/livewire": "^3.0|^4.0",
"mockery/mockery": "^1.6",
"nesbot/carbon": "^2.63|^3.0",
"orchestra/testbench": "^8.37.0|^9.16|^10.9|^11.0",
"pestphp/pest": "^2.36|^3.8|^4.3",
"pestphp/pest-plugin-laravel": "^2.4|^3.0|^4.0",
"pestphp/pest-plugin-livewire": "^2.1|^3.0|^4.0",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.1",
"spatie/invade": "^1.0",
"spatie/laravel-typescript-transformer": "^2.5",
"spatie/pest-plugin-snapshots": "^2.1",
"spatie/test-time": "^1.2"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\LaravelData\\LaravelDataServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\LaravelData\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ruben Van Assche",
"email": "ruben@spatie.be",
"role": "Developer"
}
],
"description": "Create unified resources and data transfer objects",
"homepage": "https://github.com/spatie/laravel-data",
"keywords": [
"laravel",
"laravel-data",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-data/issues",
"source": "https://github.com/spatie/laravel-data/tree/4.22.1"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-04-27T07:30:53+00:00"
},
{
"name": "spatie/laravel-package-tools",
"version": "1.93.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-package-tools.git",
"reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7",
"reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7",
"shasum": ""
},
"require": {
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"php": "^8.1"
},
"require-dev": {
"mockery/mockery": "^1.5",
"orchestra/testbench": "^8.0|^9.2|^10.0|^11.0",
"pestphp/pest": "^2.1|^3.1|^4.0",
"phpunit/php-code-coverage": "^10.0|^11.0|^12.0",
"phpunit/phpunit": "^10.5|^11.5|^12.5",
"spatie/pest-plugin-test-time": "^2.2|^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\LaravelPackageTools\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "Tools for creating Laravel packages",
"homepage": "https://github.com/spatie/laravel-package-tools",
"keywords": [
"laravel-package-tools",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-package-tools/issues",
"source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-02-21T12:49:54+00:00"
},
{
"name": "spatie/php-structure-discoverer",
"version": "2.4.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/php-structure-discoverer.git",
"reference": "10cd4e0018450d23e2bd8f8472569ad0c445c0fc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/10cd4e0018450d23e2bd8f8472569ad0c445c0fc",
"reference": "10cd4e0018450d23e2bd8f8472569ad0c445c0fc",
"shasum": ""
},
"require": {
"illuminate/collections": "^11.0|^12.0|^13.0",
"php": "^8.3",
"spatie/laravel-package-tools": "^1.92.7",
"symfony/finder": "^6.0|^7.3.5|^8.0"
},
"require-dev": {
"amphp/parallel": "^2.3.2",
"illuminate/console": "^11.0|^12.0|^13.0",
"nunomaduro/collision": "^7.0|^8.8.3",
"orchestra/testbench": "^9.5|^10.8|^11.0",
"pestphp/pest": "^3.8|^4.0",
"pestphp/pest-plugin-laravel": "^3.2|^4.0",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^1.2.1",
"phpstan/phpstan-phpunit": "^1.4.2",
"spatie/laravel-ray": "^1.43.1"
},
"suggest": {
"amphp/parallel": "When you want to use the Parallel discover worker"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\StructureDiscoverer\\StructureDiscovererServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\StructureDiscoverer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ruben Van Assche",
"email": "ruben@spatie.be",
"role": "Developer"
}
],
"description": "Automatically discover structures within your PHP application",
"homepage": "https://github.com/spatie/php-structure-discoverer",
"keywords": [
"discover",
"laravel",
"php",
"php-structure-discoverer"
],
"support": {
"issues": "https://github.com/spatie/php-structure-discoverer/issues",
"source": "https://github.com/spatie/php-structure-discoverer/tree/2.4.2"
},
"funding": [
{
"url": "https://github.com/LaravelAutoDiscoverer",
"type": "github"
}
],
"time": "2026-04-28T06:26:02+00:00"
},
{ {
"name": "spomky-labs/cbor-php", "name": "spomky-labs/cbor-php",
"version": "3.2.3", "version": "3.2.3",
@ -4490,16 +5066,16 @@
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
"version": "v3.6.0", "version": "v3.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git", "url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4512,7 +5088,7 @@
"name": "symfony/contracts" "name": "symfony/contracts"
}, },
"branch-alias": { "branch-alias": {
"dev-main": "3.6-dev" "dev-main": "3.7-dev"
} }
}, },
"autoload": { "autoload": {
@ -4537,7 +5113,7 @@
"description": "A generic function and convention to trigger deprecation notices", "description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0"
}, },
"funding": [ "funding": [
{ {
@ -4548,12 +5124,16 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-25T14:21:43+00:00" "time": "2026-04-13T15:52:40+00:00"
}, },
{ {
"name": "symfony/error-handler", "name": "symfony/error-handler",
@ -4723,16 +5303,16 @@
}, },
{ {
"name": "symfony/event-dispatcher-contracts", "name": "symfony/event-dispatcher-contracts",
"version": "v3.6.0", "version": "v3.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git", "url": "https://github.com/symfony/event-dispatcher-contracts.git",
"reference": "59eb412e93815df44f05f342958efa9f46b1e586" "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32",
"reference": "59eb412e93815df44f05f342958efa9f46b1e586", "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4746,7 +5326,7 @@
"name": "symfony/contracts" "name": "symfony/contracts"
}, },
"branch-alias": { "branch-alias": {
"dev-main": "3.6-dev" "dev-main": "3.7-dev"
} }
}, },
"autoload": { "autoload": {
@ -4779,7 +5359,7 @@
"standards" "standards"
], ],
"support": { "support": {
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0"
}, },
"funding": [ "funding": [
{ {
@ -4790,12 +5370,16 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-25T14:21:43+00:00" "time": "2026-01-05T13:30:16+00:00"
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
@ -4947,16 +5531,16 @@
}, },
{ {
"name": "symfony/http-kernel", "name": "symfony/http-kernel",
"version": "v8.0.8", "version": "v8.0.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-kernel.git", "url": "https://github.com/symfony/http-kernel.git",
"reference": "1770f6818d83b2fddc12185025b93f39a90cb628" "reference": "fb3f65b3d4ca2dad31c80d323819a762ca31d6ac"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/1770f6818d83b2fddc12185025b93f39a90cb628", "url": "https://api.github.com/repos/symfony/http-kernel/zipball/fb3f65b3d4ca2dad31c80d323819a762ca31d6ac",
"reference": "1770f6818d83b2fddc12185025b93f39a90cb628", "reference": "fb3f65b3d4ca2dad31c80d323819a762ca31d6ac",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5027,7 +5611,7 @@
"description": "Provides a structured process for converting a Request into a Response", "description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/http-kernel/tree/v8.0.8" "source": "https://github.com/symfony/http-kernel/tree/v8.0.10"
}, },
"funding": [ "funding": [
{ {
@ -5047,7 +5631,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-03-31T21:14:05+00:00" "time": "2026-05-06T12:27:31+00:00"
}, },
{ {
"name": "symfony/mailer", "name": "symfony/mailer",
@ -6546,16 +7130,16 @@
}, },
{ {
"name": "symfony/service-contracts", "name": "symfony/service-contracts",
"version": "v3.6.1", "version": "v3.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/service-contracts.git", "url": "https://github.com/symfony/service-contracts.git",
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a",
"reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -6573,7 +7157,7 @@
"name": "symfony/contracts" "name": "symfony/contracts"
}, },
"branch-alias": { "branch-alias": {
"dev-main": "3.6-dev" "dev-main": "3.7-dev"
} }
}, },
"autoload": { "autoload": {
@ -6609,7 +7193,7 @@
"standards" "standards"
], ],
"support": { "support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.6.1" "source": "https://github.com/symfony/service-contracts/tree/v3.7.0"
}, },
"funding": [ "funding": [
{ {
@ -6629,7 +7213,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-07-15T11:30:57+00:00" "time": "2026-03-28T09:44:51+00:00"
}, },
{ {
"name": "symfony/string", "name": "symfony/string",
@ -6723,16 +7307,16 @@
}, },
{ {
"name": "symfony/translation", "name": "symfony/translation",
"version": "v8.0.8", "version": "v8.0.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/translation.git", "url": "https://github.com/symfony/translation.git",
"reference": "27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f" "reference": "f63e9342e12646a57c91ef8a366a4f9d8e557b67"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f", "url": "https://api.github.com/repos/symfony/translation/zipball/f63e9342e12646a57c91ef8a366a4f9d8e557b67",
"reference": "27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f", "reference": "f63e9342e12646a57c91ef8a366a4f9d8e557b67",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -6792,7 +7376,7 @@
"description": "Provides tools to internationalize your application", "description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/translation/tree/v8.0.8" "source": "https://github.com/symfony/translation/tree/v8.0.10"
}, },
"funding": [ "funding": [
{ {
@ -6812,20 +7396,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-03-30T15:14:47+00:00" "time": "2026-05-06T11:30:54+00:00"
}, },
{ {
"name": "symfony/translation-contracts", "name": "symfony/translation-contracts",
"version": "v3.6.1", "version": "v3.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/translation-contracts.git", "url": "https://github.com/symfony/translation-contracts.git",
"reference": "65a8bc82080447fae78373aa10f8d13b38338977" "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d",
"reference": "65a8bc82080447fae78373aa10f8d13b38338977", "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -6838,7 +7422,7 @@
"name": "symfony/contracts" "name": "symfony/contracts"
}, },
"branch-alias": { "branch-alias": {
"dev-main": "3.6-dev" "dev-main": "3.7-dev"
} }
}, },
"autoload": { "autoload": {
@ -6874,7 +7458,7 @@
"standards" "standards"
], ],
"support": { "support": {
"source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0"
}, },
"funding": [ "funding": [
{ {
@ -6894,7 +7478,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-07-15T13:41:35+00:00" "time": "2026-01-05T13:30:16+00:00"
}, },
{ {
"name": "symfony/type-info", "name": "symfony/type-info",
@ -7515,23 +8099,23 @@
}, },
{ {
"name": "webmozart/assert", "name": "webmozart/assert",
"version": "2.3.0", "version": "1.12.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/webmozarts/assert.git", "url": "https://github.com/webmozarts/assert.git",
"reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4" "reference": "9be6926d8b485f55b9229203f962b51ed377ba68"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4", "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68",
"reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4", "reference": "9be6926d8b485f55b9229203f962b51ed377ba68",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-ctype": "*", "ext-ctype": "*",
"ext-date": "*", "ext-date": "*",
"ext-filter": "*", "ext-filter": "*",
"php": "^8.2" "php": "^7.2 || ^8.0"
}, },
"suggest": { "suggest": {
"ext-intl": "", "ext-intl": "",
@ -7541,7 +8125,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-feature/2-0": "2.0-dev" "dev-master": "1.10-dev"
} }
}, },
"autoload": { "autoload": {
@ -7557,10 +8141,6 @@
{ {
"name": "Bernhard Schussek", "name": "Bernhard Schussek",
"email": "bschussek@gmail.com" "email": "bschussek@gmail.com"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com"
} }
], ],
"description": "Assertions to validate method input/output with nice error messages.", "description": "Assertions to validate method input/output with nice error messages.",
@ -7571,9 +8151,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/webmozarts/assert/issues", "issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/2.3.0" "source": "https://github.com/webmozarts/assert/tree/1.12.1"
}, },
"time": "2026-04-11T10:33:05+00:00" "time": "2025-10-29T15:56:20+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [

217
config/data.php Normal file
View File

@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
use Illuminate\Contracts\Support\Arrayable;
use Spatie\LaravelData\Casts\DateTimeInterfaceCast;
use Spatie\LaravelData\Casts\EnumCast;
use Spatie\LaravelData\Normalizers\ArrayableNormalizer;
use Spatie\LaravelData\Normalizers\ArrayNormalizer;
use Spatie\LaravelData\Normalizers\JsonNormalizer;
use Spatie\LaravelData\Normalizers\ModelNormalizer;
use Spatie\LaravelData\Normalizers\ObjectNormalizer;
use Spatie\LaravelData\RuleInferrers\AttributesRuleInferrer;
use Spatie\LaravelData\RuleInferrers\BuiltInTypesRuleInferrer;
use Spatie\LaravelData\RuleInferrers\NullableRuleInferrer;
use Spatie\LaravelData\RuleInferrers\RequiredRuleInferrer;
use Spatie\LaravelData\RuleInferrers\SometimesRuleInferrer;
use Spatie\LaravelData\Support\Creation\ValidationStrategy;
use Spatie\LaravelData\Transformers\ArrayableTransformer;
use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer;
use Spatie\LaravelData\Transformers\EnumTransformer;
return [
/*
* The package will use this format when working with dates. If this option
* is an array, it will try to convert from the first format that works,
* and will serialize dates using the first format from the array.
*/
'date_format' => DATE_ATOM,
/*
* When transforming or casting dates, the following timezone will be used to
* convert the date to the correct timezone. If set to null no timezone will
* be passed.
*/
'date_timezone' => null,
/*
* It is possible to enable certain features of the package, these would otherwise
* be breaking changes, and thus they are disabled by default. In the next major
* version of the package, these features will be enabled by default.
*/
'features' => [
'cast_and_transform_iterables' => false,
/*
* When trying to set a computed property value, the package will throw an exception.
* You can disable this behaviour by setting this option to true, which will then just
* ignore the value being passed into the computed property and recalculate it.
*/
'ignore_exception_when_trying_to_set_computed_property_value' => false,
],
/*
* Global transformers will take complex types and transform them into simple
* types.
*/
'transformers' => [
DateTimeInterface::class => DateTimeInterfaceTransformer::class,
Arrayable::class => ArrayableTransformer::class,
BackedEnum::class => EnumTransformer::class,
],
/*
* Global casts will cast values into complex types when creating a data
* object from simple types.
*/
'casts' => [
DateTimeInterface::class => DateTimeInterfaceCast::class,
BackedEnum::class => EnumCast::class,
// Enumerable::class => Spatie\LaravelData\Casts\EnumerableCast::class,
],
/*
* Rule inferrers can be configured here. They will automatically add
* validation rules to properties of a data object based upon
* the type of the property.
*/
'rule_inferrers' => [
SometimesRuleInferrer::class,
NullableRuleInferrer::class,
RequiredRuleInferrer::class,
BuiltInTypesRuleInferrer::class,
AttributesRuleInferrer::class,
],
/*
* Normalizers return an array representation of the payload, or null if
* it cannot normalize the payload. The normalizers below are used for
* every data object, unless overridden in a specific data object class.
*/
'normalizers' => [
ModelNormalizer::class,
// Spatie\LaravelData\Normalizers\FormRequestNormalizer::class,
ArrayableNormalizer::class,
ObjectNormalizer::class,
ArrayNormalizer::class,
JsonNormalizer::class,
],
/*
* Data objects can be wrapped into a key like 'data' when used as a resource,
* this key can be set globally here for all data objects. You can pass in
* `null` if you want to disable wrapping.
*/
'wrap' => null,
/*
* Adds a specific caster to the Symphony VarDumper component which hides
* some properties from data objects and collections when being dumped
* by `dump` or `dd`. Can be 'enabled', 'disabled' or 'development'
* which will only enable the caster locally.
*/
'var_dumper_caster_mode' => 'development',
/*
* It is possible to skip the PHP reflection analysis of data objects
* when running in production. This will speed up the package. You
* can configure where data objects are stored and which cache
* store should be used.
*
* Structures are cached forever as they'll become stale when your
* application is deployed with changes. You can set a duration
* in seconds if you want the cache to clear after a certain
* timeframe.
*/
'structure_caching' => [
'enabled' => true,
'directories' => [app_path('Data')],
'cache' => [
'store' => env('CACHE_STORE', env('CACHE_DRIVER', 'file')),
'prefix' => 'laravel-data',
'duration' => null,
],
'reflection_discovery' => [
'enabled' => true,
'base_path' => base_path(),
'root_namespace' => null,
],
],
/*
* A data object can be validated when created using a factory or when calling the from
* method. By default, only when a request is passed the data is being validated. This
* behaviour can be changed to always validate or to completely disable validation.
*/
'validation_strategy' => ValidationStrategy::OnlyRequests->value,
/*
* A data object can map the names of its properties when transforming (output) or when
* creating (input). By default, the package will not map any names. You can set a
* global strategy here, or override it on a specific data object.
*/
'name_mapping_strategy' => [
'input' => null,
'output' => null,
],
/*
* When using an invalid include, exclude, only or except partial, the package will
* throw an exception. You can disable this behaviour by setting this option to true.
*/
'ignore_invalid_partials' => false,
/*
* When transforming a nested chain of data objects, the package can end up in an infinite
* loop when including a recursive relationship. The max transformation depth can be
* set as a safety measure to prevent this from happening. When set to null, the
* package will not enforce a maximum depth.
*/
'max_transformation_depth' => null,
/*
* When the maximum transformation depth is reached, the package will throw an exception.
* You can disable this behaviour by setting this option to true which will return an
* empty array.
*/
'throw_when_max_transformation_depth_reached' => true,
/*
* When using the `make:data` command, the package will use these settings to generate
* the data classes. You can override these settings by passing options to the command.
*/
'commands' => [
/*
* Provides default configuration for the `make:data` command. These settings can be overridden with options
* passed directly to the `make:data` command for generating single Data classes, or if not set they will
* automatically fall back to these defaults. See `php artisan make:data --help` for more information
*/
'make' => [
/*
* The default namespace for generated Data classes. This exists under the application's root namespace,
* so the default 'Data` will end up as '\App\Data', and generated Data classes will be placed in the
* app/Data/ folder. Data classes can live anywhere, but this is where `make:data` will put them.
*/
'namespace' => 'Data',
/*
* This suffix will be appended to all data classes generated by make:data, so that they are less likely
* to conflict with other related classes, controllers or models with a similar name without resorting
* to adding an alias for the Data object. Set to a blank string (not null) to disable.
*/
'suffix' => 'Data',
],
],
/*
* When using Livewire, the package allows you to enable or disable the synths
* these synths will automatically handle the data objects and their
* properties when used in a Livewire component.
*/
'livewire' => [
'enable_synths' => false,
],
];

View File

@ -0,0 +1,86 @@
<?php
use App\Data\Ui\ConnectedServices\ConnectedServiceCollectionData;
use App\Data\Ui\ConnectedServices\ConnectedServiceData;
use App\Data\Ui\ConnectedServices\ConnectedServicesData;
use App\Enums\ConnectionStatus;
use App\Enums\ConnectionTechTypes;
use Illuminate\Support\Collection;
use Livewire\Component;
use Spatie\LaravelData\DataCollection;
new class extends Component {
public ConnectedServicesData $connectedServices;
public function mount(): void
{
$this->connectedServices = new ConnectedServicesData(
services: new ConnectedServiceCollectionData([
new ConnectedServiceData(
name: 'Microsoft Outlook',
icon: 'home',
type: ConnectionTechTypes::OIDC,
connectionService: 'Azure AD',
status: ConnectionStatus::Connected
),
new ConnectedServiceData(
name: 'Microsoft Teams',
icon: 'home',
type: ConnectionTechTypes::OIDC,
connectionService: 'Azure AD',
status: ConnectionStatus::Pending
),
new ConnectedServiceData(
name: 'Keka HR',
icon: 'home',
type: ConnectionTechTypes::SAML,
connectionService: 'KeKA',
status: ConnectionStatus::Disconnected
),
new ConnectedServiceData(
name: 'Trutimer',
icon: 'home',
type: ConnectionTechTypes::SAML,
connectionService: 'Redmine',
status: ConnectionStatus::Error
),
])
);
}
};
?>
<div class="flex overflow-x-auto gap-x-4">
@foreach ($connectedServices->services as $service)
<x-shared.card class="min-w-60">
<div class="flex space-x-4">
<div class="bg-blue-200 p-4 rounded-lg flex items-center justify-center max-h-10">
<svg role="img" class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>
Outline</title>
<path
d="M 15.081 21.857 L 15.081 22.459 C 15.081 23.636 13.812 24.378 12.785 23.8 L 3.543 18.602 C 3.058 18.329 2.758 17.816 2.758 17.26 L 2.758 6.742 C 2.758 6.185 3.058 5.672 3.543 5.399 L 12.785 0.201 C 13.812 -0.378 15.082 0.365 15.081 1.544 L 15.081 2.145 L 16.178 1.814 C 17.167 1.517 18.163 2.258 18.162 3.29 L 18.162 3.915 L 19.511 3.746 C 20.431 3.632 21.243 4.348 21.242 5.275 L 21.242 18.726 C 21.243 19.652 20.431 20.37 19.511 20.254 L 18.162 20.085 L 18.162 20.71 C 18.163 21.743 17.167 22.484 16.178 22.186 L 15.081 21.857 Z M 15.081 20.249 L 16.621 20.71 L 16.621 3.29 L 15.081 3.753 L 15.081 20.249 Z M 18.162 5.467 L 18.162 18.534 L 19.702 18.726 L 19.702 5.275 L 18.162 5.467 Z M 2.758 16.801 L 2.758 7.2 L 2.758 16.801 Z M 4.298 6.742 L 4.298 17.26 L 13.54 22.459 L 13.54 1.544 L 4.298 6.742 Z M 5.838 7.765 L 7.379 6.995 L 7.379 17.005 L 5.838 16.235 L 5.838 7.765 Z"/>
</svg>
</div>
<div class="flex flex-col">
<p class="font-medium">{{$service->name}}</p>
<p class="text-sm text-gray-600">{{$service->type->name}} - {{$service->connectionService}}</p>
</div>
</div>
<div class="mt-4 flex space-x-2.5 justify-between items-center">
<div class="flex items-center space-x-1">
<div
@class([
"w-2 h-2 rounded-full",
'bg-green-400' => $service->status === ConnectionStatus::Connected,
'bg-gray-400' => $service->status === ConnectionStatus::Disconnected,
'bg-yellow-400' => $service->status === ConnectionStatus::Pending,
'bg-red-400' => $service->status === ConnectionStatus::Error,
])
></div>
<p class="text-sm">{{ucfirst($service->status->name)}}</p>
</div>
<x-shared.button>Revoke</x-shared.button>
</div>
</x-shared.card>
@endforeach
</div>

View File

@ -0,0 +1,37 @@
<?php
use Livewire\Component;
new class extends Component
{
public string $selectedTab = 'all-tab';
};
?>
<div>
<x-shared.card class="p-0">
<div class="p-4 flex justify-between border-b border-gray-200">
<h2 class="font-medium flex items-center gap-x-2">
<x-lucide-grid-2x2-plus class="w-5 h-5"/>
{{ __('Connected Services') }}
</h2>
<x-shared.button>
<div class="flex items-center gap-x-2">
<x-lucide-plus class="w-5 h-5"/>
{{ __('Add service') }}
</div>
</x-shared.button>
</div>
<x-shared.tabs.tabs wire:model="selectedTab" class="px-4">
<x-shared.tabs.tab name="all-tab" label="All" class="">
<livewire:connected-services.all-connected-services />
</x-shared.tabs.tab>
<x-shared.tabs.tab name="oidc-tab" label="OIDC">
<div>Tricks</div>
</x-shared.tabs.tab>
<x-shared.tabs.tab name="saml-tab" label="SAML">
<div>Musics</div>
</x-shared.tabs.tab>
</x-shared.tabs.tabs>
</x-shared.card>
</div>

View File

@ -0,0 +1,192 @@
<?php
use Livewire\Component;
use Livewire\Attributes\On;
new class extends Component {
/** Collapsed = icons-only mode */
public bool $collapsed = false;
/** Active nav key — passed as a prop from the parent layout */
public string $active = 'dashboard';
public function mount(string $active = 'dashboard'): void
{
$this->active = $active;
$this->collapsed = session('sidebar_collapsed', false);
}
public function toggle(): void
{
$this->collapsed = !$this->collapsed;
session(['sidebar_collapsed' => $this->collapsed]);
}
public function navItems(): array
{
return [
[
'key' => 'dashboard',
'label' => 'Dashboard',
'icon' => 'lucide-house',
'route' => 'home',
],
[
'key' => 'users',
'label' => 'Users',
'icon' => 'lucide-users',
'route' => 'home',
],
];
}
};
?>
<aside
x-data="{ collapsed: @entangle('collapsed') }"
:class="collapsed ? 'w-12' : 'w-64'"
class="relative flex flex-col h-screen bg-white border-r border-gray-100
transition-[width] duration-300 ease-in-out shrink-0 overflow-hidden"
:aria-expanded="String(!collapsed)"
>
<div class="flex items-center h-20 px-2 border-b border-gray-100 shrink-0">
<a
href="{{ route('dashboard') }}"
wire:navigate
class="shrink-0 flex items-center justify-center w-8 h-8
rounded-lg bg-blue-600 text-gray-50
shadow-sm focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-cyan-400"
aria-label="Go to dashboard"
>
<span class="w-4 h-4">
@svg('lucide-shield-cog')
</span>
</a>
<span
x-cloak
x-show="!collapsed"
x-transition:enter="transition-all duration-200 delay-150 ease-out"
x-transition:enter-start="opacity-0 -translate-x-2"
x-transition:enter-end="opacity-100 translate-x-0"
x-transition:leave="transition-opacity duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="ml-3 text-sm font-semibold text-gray-900 whitespace-nowrap tracking-tight"
>
<h1 class="text-lg">Company SSO</h1>
<h2 class="text-sm font-light">Admin Panel</h2>
</span>
</div>
<nav
class="flex-1 py-3 px-2 space-y-0.5 overflow-y-auto overflow-x-hidden"
aria-label="Main navigation"
>
@foreach ($this->navItems() as $item)
@php $isActive = $active === $item['key']; @endphp
<a
href=""
wire:navigate
@class([
'group relative flex items-center gap-3 px-2 py-2.5 rounded-lg',
'text-sm font-medium transition-colors duration-150',
'bg-gray-100 text-gray-900' => $isActive,
'text-gray-500 hover:bg-gray-50 hover:text-gray-800' => ! $isActive,
])
aria-current="{{ $isActive ? 'page' : 'false' }}"
>
{{-- Active pill indicator --}}
@if ($isActive)
<span
class="absolute left-0 inset-y-2 w-0.5 rounded-r-full bg-blue-600"
aria-hidden="true"
></span>
@endif
<span class="w-4 h-4">
@svg($item['icon'])
</span>
{{-- Label hidden when collapsed --}}
<span
x-cloak
x-show="!collapsed"
x-transition:enter="transition-opacity duration-200 delay-100"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity duration-75"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="whitespace-nowrap overflow-hidden leading-none"
>
{{ $item['label'] }}
</span>
{{-- Tooltip only rendered & visible in collapsed mode --}}
<span
x-cloak
x-show="collapsed"
role="tooltip"
class="pointer-events-none absolute left-full ml-3 z-50
whitespace-nowrap rounded-md bg-gray-900 px-2.5 py-1.5
text-xs font-medium text-white shadow-xl
opacity-0 group-hover:opacity-100
transition-opacity duration-150 delay-75"
>
{{ $item['label'] }}
{{-- Arrow --}}
<span
class="absolute right-full top-1/2 -translate-y-1/2
border-[5px] border-transparent border-r-gray-900"
aria-hidden="true"
></span>
</span>
</a>
@endforeach
</nav>
<div class="border-t border-gray-100 px-2 py-3 shrink-0">
<button
wire:click="toggle"
type="button"
class="group w-full flex items-center gap-3 px-2 py-2.5 rounded-lg
text-sm font-medium text-gray-400
hover:bg-gray-50 hover:text-gray-700
transition-colors duration-150
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-400"
:aria-label="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
>
<span class="flex-shrink-0 flex items-center justify-center w-5 h-5" aria-hidden="true">
<svg
:class="collapsed ? 'rotate-180' : 'rotate-0'"
class="w-4 h-4 transition-transform duration-300 ease-in-out"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/>
</svg>
</span>
<span
x-cloak
x-show="!collapsed"
x-transition:enter="transition-opacity duration-200 delay-100"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition-opacity duration-75"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="whitespace-nowrap"
>
Collapse
</span>
</button>
</div>
</aside>

View File

@ -0,0 +1,18 @@
<grid class="grid grid-cols-4 gap-x-4">
<x-shared.card >
<p class="text-sm text-gray-400">Total Users</p>
<p class="text-2xl">284</p>
</x-shared.card>
<x-shared.card >
<p class="text-sm text-gray-400">Active Roles</p>
<p class="text-2xl">4</p>
</x-shared.card>
<x-shared.card >
<p class="text-sm text-gray-400">Services</p>
<p class="text-2xl">8</p>
</x-shared.card>
<x-shared.card >
<p class="text-sm text-gray-400">Revoked Today</p>
<p class="text-2xl">7</p>
</x-shared.card>
</grid>

View File

@ -0,0 +1,12 @@
<div class="w-full py-4 px-10 border-b border-gray-200 flex justify-center">
<div class="flex-1 py-2">
<p class="text-xl font-medium">Admin Panel</p>
</div>
<div class="flex space-x-4">
<x-shared.button>
<x-lucide-bell class="w-5 h-5" />
</x-shared.button>
<div class="rounded-full w-12 h-12 bg-blue-200">
</div>
</div>
</div>

View File

@ -1,12 +0,0 @@
@props([
'id' => uniqid(),
])
<svg {{ $attributes }} fill="none">
<defs>
<pattern id="pattern-{{ $id }}" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
<path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
</pattern>
</defs>
<rect stroke="none" fill="url(#pattern-{{ $id }})" width="100%" height="100%"></rect>
</svg>

View File

@ -0,0 +1,3 @@
<button class="rounded-lg border border-gray-300 px-6 py-2 text-gray-700">
{{$slot}}
</button>

View File

@ -0,0 +1,3 @@
<div {{$attributes->twMerge('rounded-xl border border-gray-200 p-4 shadow-xs bg-white')}}>
{{$slot}}
</div>

View File

@ -0,0 +1,45 @@
@php use Illuminate\Support\Facades\Blade; @endphp
@props([
'name',
'label' => null,
'disabled' => false,
'hidden' => false,
])
@php
$fromLabel = $label ?? '';
$labelHtml = Blade::render("
<div @class([
'whitespace-nowrap',
'text-base-content/30 cursor-not-allowed' => '{$disabled}'
])>
{$fromLabel}
</div>
");
@endphp
<div
data-name="{{ $name }}"
x-init="
const newItem = {
name: '{{ $name }}',
label: {{ json_encode($labelHtml) }},
disabled: {{ $disabled ? 'true' : 'false' }},
hidden: {{ $hidden ? 'true' : 'false' }}
};
const index = tabs.findIndex(item => item.name === '{{ $name }}');
index !== -1 ? tabs[index] = newItem : tabs.push(newItem);
Livewire.hook('morph.removed', ({el}) => {
if (el.getAttribute('data-name') == '{{ $name }}'){
tabs = tabs.filter(i => i.name !== '{{ $name }}')
}
})
"
x-show="selected === '{{ $name }}'"
role="tabpanel"
{{ $attributes->class("tab-content py-5 px-1") }}
>
{{ $slot }}
</div>

View File

@ -0,0 +1,37 @@
@props([
'selected' => null,
'labelClass' => 'px-4 py-3 cursor-pointer',
'activeClass' => 'font-medium border-b border-b-gray-400',
'labelDivClass' => 'border-b border-b-base-content/10 flex overflow-x-auto',
'tabsClass' => 'relative w-full',
])
<div
x-data="{
tabs: [],
selected:
@if($selected)
'{{ $selected }}'
@else
@entangle($attributes->wire('model'))
@endif
}"
class="{{ $tabsClass }}"
x-class="font-semibold pb-1 border-b-[length:var(--border)] border-b-base-content/50 border-b-base-content/10 flex overflow-x-auto scrollbar-hide relative w-full"
>
<div class="{{ $labelDivClass }}">
<template x-for="tab in tabs" :key="tab.name">
<a
role="tab"
x-init="if (typeof tab == 'undefined') $el.remove()"
x-html="tab.label"
@click="tab.disabled ? null: selected = tab.name"
:class="{ '{{ $activeClass }} tab-active': selected === tab.name, 'hidden': tab.hidden }"
class="{{ $labelClass }}"></a>
</template>
</div>
<div role="tablist" {{ $attributes->except(['wire:model', 'wire:model.live'])->class(['block']) }}>
{{ $slot }}
</div>
</div>

View File

@ -1,18 +0,0 @@
<x-layouts::app :title="__('Dashboard')">
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
</div>
<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
</div>
</x-layouts::app>

View File

@ -1,101 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head> <head>
@include('partials.head') @include('partials.head')
</head> </head>
<body class="min-h-screen bg-white dark:bg-zinc-800"> <body class="min-h-screen bg-gray-50 antialiased">
<flux:sidebar sticky collapsible="mobile" class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900"> <div class="flex h-full">
<flux:sidebar.header> <livewire:dashboard-sidebar :active="$activeNav ?? 'dashboard'"/>
<x-app-logo :sidebar="true" href="{{ route('dashboard') }}" wire:navigate /> <div class="flex flex-col flex-1 min-w-0 overflow-hidden">
<flux:sidebar.collapse class="lg:hidden" /> <x-dashbord-topbar/>
</flux:sidebar.header> <main class="">
{{ $slot }}
<flux:sidebar.nav> </main>
<flux:sidebar.group :heading="__('Platform')" class="grid"> </div>
<flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate> </div>
{{ __('Dashboard') }} @livewireScripts
</flux:sidebar.item> </body>
</flux:sidebar.group>
</flux:sidebar.nav>
<flux:spacer />
<flux:sidebar.nav>
<flux:sidebar.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:sidebar.item>
<flux:sidebar.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
{{ __('Documentation') }}
</flux:sidebar.item>
</flux:sidebar.nav>
<x-desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" />
</flux:sidebar>
<!-- Mobile User Menu -->
<flux:header class="lg:hidden">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<flux:spacer />
<flux:dropdown position="top" align="end">
<flux:profile
:initials="auth()->user()->initials()"
icon-trailing="chevron-down"
/>
<flux:menu>
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<flux:avatar
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
/>
<div class="grid flex-1 text-start text-sm leading-tight">
<flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading>
<flux:text class="truncate">{{ auth()->user()->email }}</flux:text>
</div>
</div>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>
{{ __('Settings') }}
</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item
as="button"
type="submit"
icon="arrow-right-start-on-rectangle"
class="w-full cursor-pointer"
data-test="logout-button"
>
{{ __('Log out') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:header>
{{ $slot }}
@persist('toast')
<flux:toast.group>
<flux:toast />
</flux:toast.group>
@endpersist
@fluxScripts
</body>
</html> </html>

View File

@ -0,0 +1,20 @@
<?php
use Illuminate\Routing\Attributes\Controllers\Middleware;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
new
#[Layout('layouts.app')]
#[Title('Dashboard')]
#[Middleware('auth,verified')]
class extends Component {
};
?>
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
<x-dashboard-stats />
<livewire:connected-services />
</div>

View File

@ -12,4 +12,4 @@
@fonts @fonts
@vite(['resources/css/app.css', 'resources/js/app.js']) @vite(['resources/css/app.css', 'resources/js/app.js'])
@fluxAppearance @livewireStyles

View File

@ -1,11 +1,11 @@
<?php <?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::view('/', 'welcome')->name('home'); Route::view('/', 'welcome')->name('home');
Route::middleware(['auth', 'verified'])->group(function () { Route::livewire('dashboard', 'pages::dashboard')->name('dashboard');
Route::view('dashboard', 'dashboard')->name('dashboard');
});
require __DIR__.'/settings.php'; require __DIR__ . '/settings.php';

View File

@ -1,21 +1,23 @@
<?php <?php
declare(strict_types=1);
use App\Models\User; use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
use Laravel\Fortify\Features; use Laravel\Fortify\Features;
beforeEach(function () { beforeEach(function (): void {
$this->skipUnlessFortifyHas(Features::resetPasswords()); $this->skipUnlessFortifyHas(Features::resetPasswords());
}); });
test('reset password link screen can be rendered', function () { test('reset password link screen can be rendered', function (): void {
$response = $this->get(route('password.request')); $response = $this->get(route('password.request'));
$response->assertOk(); $response->assertOk();
}); });
test('reset password link can be requested', function () { test('reset password link can be requested', function (): void {
Notification::fake(); Notification::fake();
$user = User::factory()->create(); $user = User::factory()->create();
@ -25,7 +27,7 @@
Notification::assertSentTo($user, ResetPassword::class); Notification::assertSentTo($user, ResetPassword::class);
}); });
test('reset password screen can be rendered', function () { test('reset password screen can be rendered', function (): void {
Notification::fake(); Notification::fake();
$user = User::factory()->create(); $user = User::factory()->create();
@ -41,7 +43,7 @@
}); });
}); });
test('password can be reset with valid token', function () { test('password can be reset with valid token', function (): void {
Notification::fake(); Notification::fake();
$user = User::factory()->create(); $user = User::factory()->create();