feat: display vaults (monicahq/chandler#2)

This commit is contained in:
Regis Freyd
2021-12-06 21:27:31 -05:00
committed by GitHub
parent 250cd1e2c7
commit 4ebac764ae
45 changed files with 1923 additions and 236146 deletions
@@ -3,12 +3,10 @@
namespace App\Features\Account\ManageAccount\Services;
use App\Models\User;
use App\Models\Vault;
use App\Models\Account;
use App\Jobs\SetupAccount;
use App\Jobs\CreateAuditLog;
use App\Services\BaseService;
use Illuminate\Support\Facades\DB;
use App\Interfaces\ServiceInterface;
use Illuminate\Support\Facades\Hash;
@@ -17,7 +15,6 @@ class CreateAccount extends BaseService implements ServiceInterface
private User $user;
private Account $account;
private array $data;
public Vault $vault;
/**
* Get the validation rules that apply to the service.
@@ -47,7 +44,6 @@ class CreateAccount extends BaseService implements ServiceInterface
$this->account = Account::create();
$this->addFirstUser();
$this->createFirstVault();
$this->addLogs();
SetupAccount::dispatch($this->user)->onQueue('low');
@@ -67,21 +63,6 @@ class CreateAccount extends BaseService implements ServiceInterface
]);
}
private function createFirstVault(): void
{
$this->vault = Vault::create([
'account_id' => $this->account->id,
'type' => Vault::TYPE_PERSONAL,
'name' => trans('account.default_vault_name'),
]);
DB::table('user_vault')->insert([
'vault_id' => $this->vault->id,
'user_id' => $this->user->id,
'permission' => Vault::PERMISSION_MANAGE,
]);
}
private function addLogs(): void
{
CreateAuditLog::dispatch([
@@ -91,16 +72,5 @@ class CreateAccount extends BaseService implements ServiceInterface
'action_name' => 'account_created',
'objects' => json_encode([]),
]);
CreateAuditLog::dispatch([
'account_id' => $this->account->id,
'author_id' => $this->user->id,
'author_name' => $this->user->name,
'action_name' => 'vault_created',
'objects' => json_encode([
'vault_id' => $this->vault->id,
'vault_name' => $this->vault->name,
]),
]);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Features\Vault\ManageVault\ViewHelpers;
class VaultCreateViewHelper
{
/**
* Get all the data needed for the page.
*
* @return array
*/
public static function data(): array
{
return [
'url' => [
'store' => route('vault.store'),
'back' => route('vault.index'),
],
];
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Features\Vault\ManageVault\ViewHelpers;
use App\Models\Vault;
use App\Models\Account;
use Illuminate\Support\Facades\Auth;
class VaultIndexViewHelper
{
/**
* Get all the data needed for the general layout page.
*
* @param Vault $vault
* @return array
*/
public static function layoutData(Vault $vault = null): array
{
return [
'user' => [
'name' => Auth::user()->name,
],
'vault' => $vault ? [
'id' => $vault->id,
'name' => $vault->name,
] : null,
'url' => [
'vaults' => route('vault.index'),
'logout' => route('logout'),
],
];
}
/**
* Get all the data needed for the general layout page.
*
* @param Account $account
* @return array
*/
public static function data(Account $account): array
{
$vaults = Vault::where('account_id', $account->id)
->orderBy('name', 'asc')
->get();
$vaultCollection = collect();
foreach ($vaults as $vault) {
$vaultCollection->push([
'id' => $vault->id,
'name' => $vault->name,
'description' => $vault->description,
'url' => [
'show' => route('vault.show', [
'vault' => $vault,
]),
],
]);
}
return [
'vaults' => $vaultCollection,
'url' => [
'vault' => [
'new' => route('vault.new'),
],
],
];
}
}
@@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\Vault;
use Inertia\Inertia;
use App\Models\Vault;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use App\Features\Vault\ManageVault\Services\CreateVault;
use App\Features\Vault\ManageVault\ViewHelpers\VaultIndexViewHelper;
use App\Features\Vault\ManageVault\ViewHelpers\VaultCreateViewHelper;
class VaultController extends Controller
{
/**
* Show all the vaults of the user.
*
* @return Response
*/
public function index()
{
return Inertia::render('Vault/Index', [
'layoutData' => VaultIndexViewHelper::layoutData(),
'data' => VaultIndexViewHelper::data(Auth::user()->account),
]);
}
/**
* Display the create vault page.
*
* @return Response
*/
public function new()
{
return Inertia::render('Vault/Create', [
'layoutData' => VaultIndexViewHelper::layoutData(),
'data' => VaultCreateViewHelper::data(),
]);
}
/**
* Store the vault.
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
$data = [
'account_id' => Auth::user()->account_id,
'author_id' => Auth::user()->id,
'type' => Vault::TYPE_PERSONAL,
'name' => $request->input('name'),
'description' => $request->input('description'),
];
(new CreateVault)->execute($data);
return response()->json([
'data' => route('vault.index'),
], 201);
}
/**
* Display the vault.
*
* @param Request $request
* @param int $vaultId
* @return Response
*/
public function show(Request $request, int $vaultId)
{
$vault = Vault::find($vaultId);
return Inertia::render('Vault/Show', [
'layoutData' => VaultIndexViewHelper::layoutData($vault),
'data' => VaultCreateViewHelper::data(),
]);
}
}
+2
View File
@@ -2,6 +2,7 @@
namespace App\Http;
use App\Http\Middleware\CheckVaultAccess;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
@@ -64,5 +65,6 @@ class Kernel extends HttpKernel
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'vault' => CheckVaultAccess::class,
];
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
class CheckVaultAccess
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$requestedVaultId = $request->route()->parameter('vault');
$exists = DB::table('user_vault')->where('user_id', Auth::user()->id)
->where('vault_id', $requestedVaultId)
->count() > 0;
if ($exists) {
return $next($request);
} else {
abort(401);
}
}
}
@@ -35,7 +35,6 @@ class HandleInertiaRequests extends Middleware
{
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user(),
],
]);
}
+3 -3
View File
@@ -3,17 +3,17 @@
namespace App\Models;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class User extends Authenticatable
class User extends Authenticatable implements MustVerifyEmail
{
use Notifiable, HasFactory, HasApiTokens, MustVerifyEmail;
use Notifiable, HasFactory, HasApiTokens;
/**
* The attributes that are mass assignable.
+1 -1
View File
@@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
*
* @var string
*/
public const HOME = '/dashboard';
public const HOME = '/vaults';
/**
* The controller namespace for the application.
+2
View File
@@ -9,12 +9,14 @@
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"inertiajs/inertia-laravel": "^0.4.3",
"itsgoingd/clockwork": "^5.1",
"laravel/framework": "^8.65",
"laravel/sanctum": "^2.6",
"laravel/tinker": "^2.5",
"tightenco/ziggy": "^1.0"
},
"require-dev": {
"brianium/paratest": "^6.4",
"facade/ignition": "^2.5",
"fakerphp/faker": "^1.9.1",
"laravel/breeze": "^1.4",
Generated
+165 -1
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": "0de4f12469e5a4cc90c2736af9f7cb17",
"content-hash": "c20f904fa356640d9b6acf9396b6bd4a",
"packages": [
{
"name": "asm89/stack-cors",
@@ -1027,6 +1027,75 @@
],
"time": "2021-10-27T09:37:59+00:00"
},
{
"name": "itsgoingd/clockwork",
"version": "v5.1.1",
"source": {
"type": "git",
"url": "https://github.com/itsgoingd/clockwork.git",
"reference": "2daf30fa6dfc5a1ccfdb2142df59243a72c473d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/2daf30fa6dfc5a1ccfdb2142df59243a72c473d8",
"reference": "2daf30fa6dfc5a1ccfdb2142df59243a72c473d8",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=5.6",
"psr/log": "1.* || ^2.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Clockwork\\Support\\Laravel\\ClockworkServiceProvider"
],
"aliases": {
"Clockwork": "Clockwork\\Support\\Laravel\\Facade"
}
}
},
"autoload": {
"psr-4": {
"Clockwork\\": "Clockwork/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "itsgoingd",
"email": "itsgoingd@luzer.sk",
"homepage": "https://twitter.com/itsgoingd"
}
],
"description": "php dev tools in your browser",
"homepage": "https://underground.works/clockwork",
"keywords": [
"Devtools",
"debugging",
"laravel",
"logging",
"lumen",
"profiling",
"slim"
],
"support": {
"issues": "https://github.com/itsgoingd/clockwork/issues",
"source": "https://github.com/itsgoingd/clockwork/tree/v5.1.1"
},
"funding": [
{
"url": "https://github.com/itsgoingd",
"type": "github"
}
],
"time": "2021-11-01T17:38:35+00:00"
},
{
"name": "laravel/framework",
"version": "v8.74.0",
@@ -5823,6 +5892,101 @@
},
"time": "2018-12-13T10:34:14+00:00"
},
{
"name": "brianium/paratest",
"version": "v6.4.1",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "c32a5c4fc2ff339202437d25d19a5f496f880d61"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/c32a5c4fc2ff339202437d25d19a5f496f880d61",
"reference": "c32a5c4fc2ff339202437d25d19a5f496f880d61",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-pcre": "*",
"ext-reflection": "*",
"ext-simplexml": "*",
"php": "^7.3 || ^8.0",
"phpunit/php-code-coverage": "^9.2.9",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-timer": "^5.0.3",
"phpunit/phpunit": "^9.5.10",
"sebastian/environment": "^5.1.3",
"symfony/console": "^5.4.0 || ^6.0.0",
"symfony/process": "^5.4.0 || ^6.0.0"
},
"require-dev": {
"doctrine/coding-standard": "^9.0.0",
"ekino/phpstan-banned-code": "^0.5.0",
"ergebnis/phpstan-rules": "^0.15.3",
"ext-posix": "*",
"infection/infection": "^0.25.3",
"malukenho/mcbumpface": "^1.1.5",
"phpstan/phpstan": "^0.12.99",
"phpstan/phpstan-deprecation-rules": "^0.12.6",
"phpstan/phpstan-phpunit": "^0.12.22",
"phpstan/phpstan-strict-rules": "^0.12.11",
"squizlabs/php_codesniffer": "^3.6.1",
"symfony/filesystem": "^v5.4.0",
"thecodingmachine/phpstan-strict-rules": "^v0.12.2",
"vimeo/psalm": "^4.13.1"
},
"bin": [
"bin/paratest"
],
"type": "library",
"autoload": {
"psr-4": {
"ParaTest\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Scaturro",
"email": "scaturrob@gmail.com",
"role": "Developer"
},
{
"name": "Filippo Tessarotto",
"email": "zoeslam@gmail.com",
"role": "Developer"
}
],
"description": "Parallel testing for PHP",
"homepage": "https://github.com/paratestphp/paratest",
"keywords": [
"concurrent",
"parallel",
"phpunit",
"testing"
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v6.4.1"
},
"funding": [
{
"url": "https://github.com/sponsors/Slamdunk",
"type": "github"
},
{
"url": "https://paypal.me/filippotessarotto",
"type": "paypal"
}
],
"time": "2021-12-02T09:12:23+00:00"
},
{
"name": "composer/ca-bundle",
"version": "1.3.1",
+416
View File
@@ -0,0 +1,416 @@
<?php
return [
/*
|------------------------------------------------------------------------------------------------------------------
| Enable Clockwork
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork is enabled by default only when your application is in debug mode. Here you can explicitly enable or
| disable Clockwork. When disabled, no data is collected and the api and web ui are inactive.
|
*/
'enable' => env('CLOCKWORK_ENABLE', null),
/*
|------------------------------------------------------------------------------------------------------------------
| Features
|------------------------------------------------------------------------------------------------------------------
|
| You can enable or disable various Clockwork features here. Some features have additional settings (eg. slow query
| threshold for database queries).
|
*/
'features' => [
// Cache usage stats and cache queries including results
'cache' => [
'enabled' => env('CLOCKWORK_CACHE_ENABLED', true),
// Collect cache queries
'collect_queries' => env('CLOCKWORK_CACHE_QUERIES', true),
// Collect values from cache queries (high performance impact with a very high number of queries)
'collect_values' => env('CLOCKWORK_CACHE_COLLECT_VALUES', false),
],
// Database usage stats and queries
'database' => [
'enabled' => env('CLOCKWORK_DATABASE_ENABLED', true),
// Collect database queries (high performance impact with a very high number of queries)
'collect_queries' => env('CLOCKWORK_DATABASE_COLLECT_QUERIES', true),
// Collect details of models updates (high performance impact with a lot of model updates)
'collect_models_actions' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_ACTIONS', true),
// Collect details of retrieved models (very high performance impact with a lot of models retrieved)
'collect_models_retrieved' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_RETRIEVED', false),
// Query execution time threshold in milliseconds after which the query will be marked as slow
'slow_threshold' => env('CLOCKWORK_DATABASE_SLOW_THRESHOLD'),
// Collect only slow database queries
'slow_only' => env('CLOCKWORK_DATABASE_SLOW_ONLY', false),
// Detect and report duplicate queries
'detect_duplicate_queries' => env('CLOCKWORK_DATABASE_DETECT_DUPLICATE_QUERIES', false),
],
// Dispatched events
'events' => [
'enabled' => env('CLOCKWORK_EVENTS_ENABLED', true),
// Ignored events (framework events are ignored by default)
'ignored_events' => [
// App\Events\UserRegistered::class,
// 'user.registered'
],
],
// Laravel log (you can still log directly to Clockwork with laravel log disabled)
'log' => [
'enabled' => env('CLOCKWORK_LOG_ENABLED', true),
],
// Sent notifications
'notifications' => [
'enabled' => env('CLOCKWORK_NOTIFICATIONS_ENABLED', true),
],
// Performance metrics
'performance' => [
// Allow collecting of client metrics. Requires separate clockwork-browser npm package.
'client_metrics' => env('CLOCKWORK_PERFORMANCE_CLIENT_METRICS', true),
],
// Dispatched queue jobs
'queue' => [
'enabled' => env('CLOCKWORK_QUEUE_ENABLED', true),
],
// Redis commands
'redis' => [
'enabled' => env('CLOCKWORK_REDIS_ENABLED', true),
],
// Routes list
'routes' => [
'enabled' => env('CLOCKWORK_ROUTES_ENABLED', false),
// Collect only routes from particular namespaces (only application routes by default)
'only_namespaces' => ['App'],
],
// Rendered views
'views' => [
'enabled' => env('CLOCKWORK_VIEWS_ENABLED', true),
// Collect views including view data (high performance impact with a high number of views)
'collect_data' => env('CLOCKWORK_VIEWS_COLLECT_DATA', false),
// Use Twig profiler instead of Laravel events for apps using laravel-twigbridge (more precise, but does
// not support collecting view data)
'use_twig_profiler' => env('CLOCKWORK_VIEWS_USE_TWIG_PROFILER', false),
],
],
/*
|------------------------------------------------------------------------------------------------------------------
| Enable web UI
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork comes with a web UI accessible via http://your.app/clockwork. Here you can enable or disable this
| feature. You can also set a custom path for the web UI.
|
*/
'web' => env('CLOCKWORK_WEB', true),
/*
|------------------------------------------------------------------------------------------------------------------
| Enable toolbar
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can show a toolbar with basic metrics on all responses. Here you can enable or disable this feature.
| Requires a separate clockwork-browser npm library.
| For installation instructions see https://underground.works/clockwork/#docs-viewing-data
|
*/
'toolbar' => env('CLOCKWORK_TOOLBAR', true),
/*
|------------------------------------------------------------------------------------------------------------------
| HTTP requests collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork collects data about HTTP requests to your app. Here you can choose which requests should be collected.
|
*/
'requests' => [
// With on-demand mode enabled, Clockwork will only profile requests when the browser extension is open or you
// manually pass a "clockwork-profile" cookie or get/post data key.
// Optionally you can specify a "secret" that has to be passed as the value to enable profiling.
'on_demand' => env('CLOCKWORK_REQUESTS_ON_DEMAND', false),
// Collect only errors (requests with HTTP 4xx and 5xx responses)
'errors_only' => env('CLOCKWORK_REQUESTS_ERRORS_ONLY', false),
// Response time threshold in milliseconds after which the request will be marked as slow
'slow_threshold' => env('CLOCKWORK_REQUESTS_SLOW_THRESHOLD'),
// Collect only slow requests
'slow_only' => env('CLOCKWORK_REQUESTS_SLOW_ONLY', false),
// Sample the collected requests (e.g. set to 100 to collect only 1 in 100 requests)
'sample' => env('CLOCKWORK_REQUESTS_SAMPLE', false),
// List of URIs that should not be collected
'except' => [
'/horizon/.*', // Laravel Horizon requests
'/telescope/.*', // Laravel Telescope requests
'/_debugbar/.*', // Laravel DebugBar requests
],
// List of URIs that should be collected, any other URI will not be collected if not empty
'only' => [
// '/api/.*'
],
// Don't collect OPTIONS requests, mostly used in the CSRF pre-flight requests and are rarely of interest
'except_preflight' => env('CLOCKWORK_REQUESTS_EXCEPT_PREFLIGHT', true),
],
/*
|------------------------------------------------------------------------------------------------------------------
| Artisan commands collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed artisan commands. Here you can enable and configure which commands
| should be collected.
|
*/
'artisan' => [
// Enable or disable collection of executed Artisan commands
'collect' => env('CLOCKWORK_ARTISAN_COLLECT', false),
// List of commands that should not be collected (built-in commands are not collected by default)
'except' => [
// 'inspire'
],
// List of commands that should be collected, any other command will not be collected if not empty
'only' => [
// 'inspire'
],
// Enable or disable collection of command output
'collect_output' => env('CLOCKWORK_ARTISAN_COLLECT_OUTPUT', false),
// Enable or disable collection of built-in Laravel commands
'except_laravel_commands' => env('CLOCKWORK_ARTISAN_EXCEPT_LARAVEL_COMMANDS', true),
],
/*
|------------------------------------------------------------------------------------------------------------------
| Queue jobs collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed queue jobs. Here you can enable and configure which queue jobs should
| be collected.
|
*/
'queue' => [
// Enable or disable collection of executed queue jobs
'collect' => env('CLOCKWORK_QUEUE_COLLECT', false),
// List of queue jobs that should not be collected
'except' => [
// App\Jobs\ExpensiveJob::class
],
// List of queue jobs that should be collected, any other queue job will not be collected if not empty
'only' => [
// App\Jobs\BuggyJob::class
],
],
/*
|------------------------------------------------------------------------------------------------------------------
| Tests collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect data about executed tests. Here you can enable and configure which tests should be
| collected.
|
*/
'tests' => [
// Enable or disable collection of ran tests
'collect' => env('CLOCKWORK_TESTS_COLLECT', false),
// List of tests that should not be collected
'except' => [
// Tests\Unit\ExampleTest::class
],
],
/*
|------------------------------------------------------------------------------------------------------------------
| Enable data collection when Clockwork is disabled
|------------------------------------------------------------------------------------------------------------------
|
| You can enable this setting to collect data even when Clockwork is disabled, e.g. for future analysis.
|
*/
'collect_data_always' => env('CLOCKWORK_COLLECT_DATA_ALWAYS', false),
/*
|------------------------------------------------------------------------------------------------------------------
| Metadata storage
|------------------------------------------------------------------------------------------------------------------
|
| Configure how is the metadata collected by Clockwork stored. Two options are available:
| - files - A simple fast storage implementation storing data in one-per-request files.
| - sql - Stores requests in a sql database. Supports MySQL, PostgreSQL and SQLite. Requires PDO.
|
*/
'storage' => env('CLOCKWORK_STORAGE', 'files'),
// Path where the Clockwork metadata is stored
'storage_files_path' => env('CLOCKWORK_STORAGE_FILES_PATH', storage_path('clockwork')),
// Compress the metadata files using gzip, trading a little bit of performance for lower disk usage
'storage_files_compress' => env('CLOCKWORK_STORAGE_FILES_COMPRESS', false),
// SQL database to use, can be a name of database configured in database.php or a path to a SQLite file
'storage_sql_database' => env('CLOCKWORK_STORAGE_SQL_DATABASE', storage_path('clockwork.sqlite')),
// SQL table name to use, the table is automatically created and udpated when needed
'storage_sql_table' => env('CLOCKWORK_STORAGE_SQL_TABLE', 'clockwork'),
// Maximum lifetime of collected metadata in minutes, older requests will automatically be deleted, false to disable
'storage_expiration' => env('CLOCKWORK_STORAGE_EXPIRATION', 60 * 24 * 7),
/*
|------------------------------------------------------------------------------------------------------------------
| Authentication
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can be configured to require authentication before allowing access to the collected data. This might be
| useful when the application is publicly accessible. Setting to true will enable a simple authentication with a
| pre-configured password. You can also pass a class name of a custom implementation.
|
*/
'authentication' => env('CLOCKWORK_AUTHENTICATION', false),
// Password for the simple authentication
'authentication_password' => env('CLOCKWORK_AUTHENTICATION_PASSWORD', 'VerySecretPassword'),
/*
|------------------------------------------------------------------------------------------------------------------
| Stack traces collection
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork can collect stack traces for log messages and certain data like database queries. Here you can set
| whether to collect stack traces, limit the number of collected frames and set further configuration. Collecting
| long stack traces considerably increases metadata size.
|
*/
'stack_traces' => [
// Enable or disable collecting of stack traces
'enabled' => env('CLOCKWORK_STACK_TRACES_ENABLED', true),
// Limit the number of frames to be collected
'limit' => env('CLOCKWORK_STACK_TRACES_LIMIT', 10),
// List of vendor names to skip when determining caller, common vendors are automatically added
'skip_vendors' => [
// 'phpunit'
],
// List of namespaces to skip when determining caller
'skip_namespaces' => [
// 'Laravel'
],
// List of class names to skip when determining caller
'skip_classes' => [
// App\CustomLog::class
],
],
/*
|------------------------------------------------------------------------------------------------------------------
| Serialization
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork serializes the collected data to json for storage and transfer. Here you can configure certain aspects
| of serialization. Serialization has a large effect on the cpu time and memory usage.
|
*/
// Maximum depth of serialized multi-level arrays and objects
'serialization_depth' => env('CLOCKWORK_SERIALIZATION_DEPTH', 10),
// A list of classes that will never be serialized (e.g. a common service container class)
'serialization_blackbox' => [
\Illuminate\Container\Container::class,
\Illuminate\Foundation\Application::class,
\Laravel\Lumen\Application::class,
],
/*
|------------------------------------------------------------------------------------------------------------------
| Register helpers
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork comes with a "clock" global helper function. You can use this helper to quickly log something and to
| access the Clockwork instance.
|
*/
'register_helpers' => env('CLOCKWORK_REGISTER_HELPERS', true),
/*
|------------------------------------------------------------------------------------------------------------------
| Send headers for AJAX request
|------------------------------------------------------------------------------------------------------------------
|
| When trying to collect data, the AJAX method can sometimes fail if it is missing required headers. For example, an
| API might require a version number using Accept headers to route the HTTP request to the correct codebase.
|
*/
'headers' => [
// 'Accept' => 'application/vnd.com.whatever.v1+json',
],
/*
|------------------------------------------------------------------------------------------------------------------
| Server timing
|------------------------------------------------------------------------------------------------------------------
|
| Clockwork supports the W3C Server Timing specification, which allows for collecting a simple performance metrics
| in a cross-browser way. E.g. in Chrome, your app, database and timeline event timings will be shown in the Dev
| Tools network tab. This setting specifies the max number of timeline events that will be sent. Setting to false
| will disable the feature.
|
*/
'server_timing' => env('CLOCKWORK_SERVER_TIMING', 10),
];
+3
View File
@@ -24,7 +24,10 @@
"lodash": "^4.17.19",
"postcss": "^8.2.13",
"postcss-import": "^14.0.1",
"sass": "^1.15.2",
"sass-loader": "^7.1.0",
"tailwindcss": "^2.1.2",
"tiny-emitter": "^2.1.0",
"vue": "^3.0.5",
"vue-loader": "^16.1.2"
}
-188579
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
-47332
View File
File diff suppressed because one or more lines are too long
+4
View File
@@ -1,3 +1,7 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
body {
color: #343a4b;
}
-124
View File
@@ -1,124 +0,0 @@
<template>
<div>
<div class="min-h-screen bg-gray-100">
<nav class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="flex-shrink-0 flex items-center">
<Link :href="route('dashboard')">
<BreezeApplicationLogo class="block h-9 w-auto" />
</Link>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<BreezeNavLink :href="route('dashboard')" :active="route().current('dashboard')">
Dashboard
</BreezeNavLink>
</div>
</div>
<div class="hidden sm:flex sm:items-center sm:ml-6">
<!-- Settings Dropdown -->
<div class="ml-3 relative">
<BreezeDropdown align="right" width="48">
<template #trigger>
<span class="inline-flex rounded-md">
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
{{ $page.props.auth.user.name }}
<svg class="ml-2 -mr-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</span>
</template>
<template #content>
<BreezeDropdownLink :href="route('logout')" method="post" as="button">
Log Out
</BreezeDropdownLink>
</template>
</BreezeDropdown>
</div>
</div>
<!-- Hamburger -->
<div class="-mr-2 flex items-center sm:hidden">
<button @click="showingNavigationDropdown = ! showingNavigationDropdown" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': showingNavigationDropdown, 'inline-flex': ! showingNavigationDropdown }" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! showingNavigationDropdown, 'inline-flex': showingNavigationDropdown }" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': showingNavigationDropdown, 'hidden': ! showingNavigationDropdown}" class="sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<BreezeResponsiveNavLink :href="route('dashboard')" :active="route().current('dashboard')">
Dashboard
</BreezeResponsiveNavLink>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ $page.props.auth.user.name }}</div>
<div class="font-medium text-sm text-gray-500">{{ $page.props.auth.user.email }}</div>
</div>
<div class="mt-3 space-y-1">
<BreezeResponsiveNavLink :href="route('logout')" method="post" as="button">
Log Out
</BreezeResponsiveNavLink>
</div>
</div>
</div>
</nav>
<!-- Page Heading -->
<header class="bg-white shadow" v-if="$slots.header">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<slot name="header" />
</div>
</header>
<!-- Page Content -->
<main>
<slot />
</main>
</div>
</div>
</template>
<script>
import BreezeApplicationLogo from '@/Components/ApplicationLogo.vue'
import BreezeDropdown from '@/Components/Dropdown.vue'
import BreezeDropdownLink from '@/Components/DropdownLink.vue'
import BreezeNavLink from '@/Components/NavLink.vue'
import BreezeResponsiveNavLink from '@/Components/ResponsiveNavLink.vue'
import { Link } from '@inertiajs/inertia-vue3';
export default {
components: {
BreezeApplicationLogo,
BreezeDropdown,
BreezeDropdownLink,
BreezeNavLink,
BreezeResponsiveNavLink,
Link,
},
data() {
return {
showingNavigationDropdown: false,
}
},
}
</script>
+1 -1
View File
@@ -23,7 +23,7 @@
<script>
import BreezeButton from '@/Components/Button.vue'
import BreezeGuestLayout from '@/Layouts/Guest.vue'
import BreezeGuestLayout from '@/Shared/Guest.vue'
import BreezeInput from '@/Components/Input.vue'
import BreezeLabel from '@/Components/Label.vue'
import BreezeValidationErrors from '@/Components/ValidationErrors.vue'
+1 -1
View File
@@ -27,7 +27,7 @@
<script>
import BreezeButton from '@/Components/Button.vue'
import BreezeGuestLayout from '@/Layouts/Guest.vue'
import BreezeGuestLayout from '@/Shared/Guest.vue'
import BreezeInput from '@/Components/Input.vue'
import BreezeLabel from '@/Components/Label.vue'
import BreezeValidationErrors from '@/Components/ValidationErrors.vue'
+1 -1
View File
@@ -40,7 +40,7 @@
<script>
import BreezeButton from '@/Components/Button.vue'
import BreezeCheckbox from '@/Components/Checkbox.vue'
import BreezeGuestLayout from '@/Layouts/Guest.vue'
import BreezeGuestLayout from '@/Shared/Guest.vue'
import BreezeInput from '@/Components/Input.vue'
import BreezeLabel from '@/Components/Label.vue'
import BreezeValidationErrors from '@/Components/ValidationErrors.vue'
+1 -1
View File
@@ -43,7 +43,7 @@
<script>
import BreezeButton from '@/Components/Button.vue'
import BreezeGuestLayout from '@/Layouts/Guest.vue'
import BreezeGuestLayout from '@/Shared/Guest.vue'
import BreezeInput from '@/Components/Input.vue'
import BreezeLabel from '@/Components/Label.vue'
import BreezeValidationErrors from '@/Components/ValidationErrors.vue'
+1 -1
View File
@@ -29,7 +29,7 @@
<script>
import BreezeButton from '@/Components/Button.vue'
import BreezeGuestLayout from '@/Layouts/Guest.vue'
import BreezeGuestLayout from '@/Shared/Guest.vue'
import BreezeInput from '@/Components/Input.vue'
import BreezeLabel from '@/Components/Label.vue'
import BreezeValidationErrors from '@/Components/ValidationErrors.vue'
+1 -1
View File
@@ -22,7 +22,7 @@
<script>
import BreezeButton from '@/Components/Button.vue'
import BreezeGuestLayout from '@/Layouts/Guest.vue'
import BreezeGuestLayout from '@/Shared/Guest.vue'
import { Head, Link } from '@inertiajs/inertia-vue3';
export default {
-33
View File
@@ -1,33 +0,0 @@
<template>
<Head title="Dashboard" />
<BreezeAuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Dashboard
</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
You're logged in!
</div>
</div>
</div>
</div>
</BreezeAuthenticatedLayout>
</template>
<script>
import BreezeAuthenticatedLayout from '@/Layouts/Authenticated.vue'
import { Head } from '@inertiajs/inertia-vue3';
export default {
components: {
BreezeAuthenticatedLayout,
Head,
},
}
</script>
+93
View File
@@ -0,0 +1,93 @@
<style lang="scss" scoped>
.section-head {
border-top-left-radius: 7px;
border-top-right-radius: 7px;
}
</style>
<template>
<Layout :layoutData="layoutData">
<main class="sm:mt-24 relative">
<div class="max-w-lg mx-auto px-2 py-2 sm:py-6 sm:px-6 lg:px-8">
<form @submit.prevent="submit()" class="bg-white border border-gray-200 rounded-lg mb-6">
<div class="p-5 border-b border-gray-200 bg-blue-50 section-head">
<h1 class="text-center text-2xl mb-1 font-medium">Create a new vault</h1>
<p class="text-center">Vaults contain all your contacts data.</p>
</div>
<div class="p-5 border-b border-gray-200">
<text-input v-model="form.name" :autofocus="true" :div-outer-class="'mb-5'" :input-class="'block w-full'" :required="true" :maxlength="255" :label="'Vault name'" />
<text-area v-model="form.description" :label="'Description'" :maxlength="255" :textarea-class="'block w-full'" />
</div>
<div class="p-5 flex justify-between">
<pretty-link :href="data.url.back" :text="'Cancel'" :classes="'mr-3'" />
<pretty-button :href="'data.url.vault.new'" :text="'Create a vault'" :state="loadingState" :icon="'check'" :classes="'save'" />
</div>
</form>
</div>
</main>
</Layout>
</template>
<script>
import Layout from '@/Shared/Layout';
import PrettyLink from '@/Shared/PrettyLink';
import PrettyButton from '@/Shared/PrettyButton';
import TextInput from '@/Shared/TextInput';
import TextArea from '@/Shared/TextArea';
export default {
components: {
Layout,
PrettyLink,
PrettyButton,
TextInput,
TextArea,
},
props: {
layoutData: {
type: Object,
default: null,
},
data: {
type: Object,
default: null,
},
},
data() {
return {
addMode: false,
loadingState: '',
form: {
name: '',
description: '',
},
};
},
methods: {
showAddModal(type) {
if (type == 'lifeEvent') {
this.addMode = true;
}
},
submit() {
this.loadingState = 'loading';
axios.post(this.data.url.store, this.form)
.then(response => {
localStorage.success = 'The vault has been created';
this.$inertia.visit(response.data.data);
})
.catch(error => {
this.loadingState = null;
//this.form.errors = error.response.data;
});
},
},
};
</script>
+120
View File
@@ -0,0 +1,120 @@
<style lang="scss" scoped>
.vault-list {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.vault-detail {
height: 250px;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
}
@media (max-width: 480px) {
.vault-list {
grid-template-columns: 1fr;
}
}
.icon-sidebar {
color: #737e8d;
top: -2px;
}
input[type=checkbox] {
top: 3px;
width: 12px;
}
</style>
<template>
<Layout title="Dashboard" :layoutData="layoutData">
<main class="sm:mt-24 relative">
<!-- blank state -->
<div v-if="data.vaults.length == 0" class="max-w-md mx-auto px-2 py-2 sm:py-6 sm:px-6 lg:px-8">
<div class="bg-white border border-gray-200 rounded-lg mb-6 p-5">
<h2 class="text-lg text-center mb-6">Thanks for trying out Monica 👋</h2>
<p class="mb-3">Monica is there to help you build better relationships.</p>
<p class="mb-3">Contacts in Monica are stored in vaults. You can have as many vaults as you want: one vault for your personal life, one for your professional life, and/or one vault shared with your spouse.</p>
<div class="text-center">
<pretty-link :href="data.url.vault.new" :text="'Create a vault'" :icon="'plus'" />
</div>
</div>
</div>
<div v-if="data.vaults.length > 0" class="max-w-4xl mx-auto px-2 py-2 sm:py-6 sm:px-6 lg:px-8">
<div class="flex items-center justify-between mb-6">
<h3>All the vaults in the account</h3>
<pretty-link :href="data.url.vault.new" :text="'Create a vault'" :icon="'plus'" />
</div>
<div class="vault-list grid grid-cols-1 sm:grid-cols-3 gap-6">
<div v-for="vault in data.vaults" :key="vault.id" class="bg-white border border-gray-200 rounded-lg">
<div class="grid vault-detail">
<Link :href="vault.url.show" class="border-b border-gray-200 text-lg px-3 py-1 font-medium">
{{ vault.name }}
</Link>
<!-- description -->
<p v-if="vault.description" class="border-b border-gray-200 p-3">
{{ vault.description }}
</p>
<p v-else class="border-b border-gray-200 p-3">
No description yet.
</p>
<!-- actions -->
<div class="flex items-center justify-between px-3 py-2">
<Link :href="'href'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 hover:text-gray-900 pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</Link>
<Link :href="vault.url.show">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 hover:text-gray-900 pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</Link>
</div>
</div>
</div>
</div>
</div>
</main>
</Layout>
</template>
<script>
import Layout from '@/Shared/Layout';
import { Link } from '@inertiajs/inertia-vue3';
import PrettyLink from '@/Shared/PrettyLink';
export default {
components: {
Layout,
Link,
PrettyLink,
},
props: {
layoutData: {
type: Object,
default: null,
},
data: {
type: Array,
default: [],
},
},
data() {
return {
addMode: false,
};
},
methods: {
},
};
</script>
+45
View File
@@ -0,0 +1,45 @@
<style lang="scss" scoped>
</style>
<template>
<Layout title="Dashboard" :inside-vault="true" :layoutData="layoutData">
<main class="sm:mt-24 relative">
d
</main>
</Layout>
</template>
<script>
import Layout from '@/Shared/Layout';
import { Link } from '@inertiajs/inertia-vue3';
import PrettyLink from '@/Shared/PrettyLink';
export default {
components: {
Layout,
Link,
PrettyLink,
},
props: {
layoutData: {
type: Object,
default: null,
},
data: {
type: Array,
default: [],
},
},
data() {
return {
addMode: false,
};
},
methods: {
},
};
</script>
+134
View File
@@ -0,0 +1,134 @@
<style lang="scss" scoped>
main {
color: #343a4b;
}
</style>
<template>
<main>
<div class="min-h-full">
<div class="fixed w-full z-10 top-0">
<!-- main nav -->
<nav class="bg-gray-50 border-b max-w-8xl mx-auto px-3 sm:px-6 flex items-center justify-between h-10">
<div class="border border-gray-200 rounded-lg bg-white items-center sm:flex px-3">
<Link :href="layoutData.url.vaults" class="flex-shrink-0">
{{ layoutData.user.name }}
</Link>
<div v-if="layoutData.vault"><span class="mr-1 ml-1">></span> {{ layoutData.vault.name }}</div>
</div>
<!-- search box -->
<div v-if="insideVault" class="flew-grow">
<input type="text" class="focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md border border-gray-300 w-64 px-2 py-1" placeholder="Search a contact" />
</div>
<!-- icons -->
<div class="flew-grow">
<ul>
<li class="inline mr-4">
<svg xmlns="http://www.w3.org/2000/svg" class="inline text-gray-600 h-4 w-4 sm:h-5 sm:w-5 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</li>
<li class="inline">
<svg @click="logout()" xmlns="http://www.w3.org/2000/svg" class="inline text-gray-600 h-4 w-4 sm:h-5 sm:w-5 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
</li>
</ul>
</div>
</nav>
<!-- sub menu -->
<nav v-if="insideVault" class="sm:border-b bg-white">
<div class="max-w-8xl mx-auto px-4 sm:px-6">
<div class="py-2">
<div class="">
<div class="hidden md:block">
<div class="flex items-baseline justify-between space-x-6">
<div>
<Link href="" class="bg-blue-700 text-white px-2 py-1 mr-2 rounded-md text-sm font-medium">
Dashboard
</Link>
<Link href="" class="hover:bg-gray-700 hover:text-white px-2 py-1 mr-2 rounded-md text-sm font-medium">
Reports
</Link>
<Link href="contacts'" class="hover:bg-gray-700 hover:text-white px-2 py-1 mr-2 rounded-md text-sm font-medium">
Contacts
</Link>
<Link href="" class="hover:bg-gray-700 hover:text-white px-2 py-1 mr-2 rounded-md text-sm font-medium">
Gift center
</Link>
<Link href="" class="hover:bg-gray-700 hover:text-white px-2 py-1 mr-2 rounded-md text-sm font-medium">
Loans & debts center
</Link>
</div>
<Link href="" class="hover:bg-gray-700 hover:text-white px-2 py-1 rounded-md text-sm font-medium">
Settings
</Link>
</div>
</div>
</div>
</div>
</div>
</nav>
</div>
<main class="mt-10 sm:mt-20 relative">
<slot></slot>
</main>
</div>
<toaster />
</main>
</template>
<script>
import { Link } from '@inertiajs/inertia-vue3';
import Toaster from '@/Shared/Toaster';
export default {
components: {
Link,
Toaster,
},
props: {
title: {
type: String,
default: null,
},
insideVault: {
type: Boolean,
default: false,
},
layoutData: {
type: Object,
default: null,
},
},
created() {
if (localStorage.success) {
this.flash(localStorage.success, 'success');
localStorage.removeItem('success');
}
},
methods: {
logout() {
window.open(this.user.url.logout);
},
},
};
</script>
+82
View File
@@ -0,0 +1,82 @@
<style lang="scss" scoped>
.icon {
top: -1px;
}
.save {
background-color: #fcf27e;
}
button {
border-radius: .25rem !important;
border-color: #191a1b;
border-width: 1px !important;
box-shadow: 2px 2px 0 #191a1b;
display: inline-block !important;
position: relative !important;
text-decoration: none !important;
transition-duration: .15s !important;
transition-property: background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter !important;
transition-timing-function: cubic-bezier(.4,0,.2,1) !important;
font-size: .875rem !important;
line-height: 1.25rem !important;
padding-left: 9px;
padding-right: 9px;
&:hover {
box-shadow: none !important;
transform: translate(2px,2px);
}
&:disabled {
box-shadow: none;
transform: translate(0,0);
}
}
</style>
<template>
<button :class="classes" :disabled="state == 'loading'" class="relative text-sm" name="save" type="submit">
<span v-if="state == 'loading'">Loading</span>
<!-- + icon -->
<svg v-if="icon === 'plus' && state != 'loading'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline relative icon" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<!-- check icon -->
<svg v-if="icon === 'check' && state != 'loading'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline relative icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<span v-if="state != 'loading'">
{{ text }}
</span>
</button>
</template>
<script>
export default {
props: {
text: {
type: String,
default: '',
},
icon: {
type: String,
default: '',
},
state: {
type: String,
default: '',
},
classes: {
type: String,
default: '',
},
}
};
</script>
+77
View File
@@ -0,0 +1,77 @@
<style lang="scss" scoped>
.icon {
top: -1px;
}
a {
--tw-shadow: 2px 2px 0 #191a1b !important;
border-radius: .25rem !important;
border-color: #191a1b;
border-width: 1px !important;
box-shadow: var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow) !important;
display: inline-block !important;
position: relative !important;
text-decoration: none !important;
transition-duration: .15s !important;
transition-property: background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter !important;
transition-timing-function: cubic-bezier(.4,0,.2,1) !important;
font-size: .875rem !important;
line-height: 1.25rem !important;
padding-left: 9px;
padding-right: 9px;
&:hover {
box-shadow: none !important;
transform: translate(2px,2px);
}
}
</style>
<template>
<Link :class="classes" class="relative text-sm" :href="href">
<!-- + icon -->
<svg v-if="icon === 'plus' && state != 'loading'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline relative icon" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<!-- check icon -->
<svg v-if="icon === 'check' && state != 'loading'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline relative icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<span>
{{ text }}
</span>
</Link>
</template>
<script>
import { Link } from '@inertiajs/inertia-vue3';
export default {
components: {
Link,
},
props: {
text: {
type: String,
default: '',
},
icon: {
type: String,
default: '',
},
href: {
type: String,
default: '',
},
classes: {
type: String,
default: '',
},
}
};
</script>
+137
View File
@@ -0,0 +1,137 @@
<style lang="scss" scoped>
.optional-badge {
border-radius: 4px;
color: #283e59;
background-color: #edf2f9;
padding: 1px 3px;
}
.length {
top: 10px;
right: 10px;
background-color: #e5eeff;
padding: 3px 4px;
}
.counter {
padding-right: 64px;
}
</style>
<template>
<div class="mb3">
<label v-if="label" class="block text-sm mb-2" :for="id">
{{ label }}
<span v-if="!required" class="optional-badge text-xs">
optional
</span>
</label>
<div class="relative">
<textarea @input="$emit('update:modelValue', $event.target.value)"
v-model="modelValue"
:id="id"
:class="localTextAreaClasses"
:required="required"
:type="type"
:rows="rows"
:maxlength="maxlength"
@keydown.esc="sendEscKey"
@focus="showMaxLength"
@blur="displayMaxLength = false"
></textarea>
<span v-if="maxlength && displayMaxLength" class="length absolute text-xs rounded">
{{ charactersLeft }}
</span>
</div>
<p v-if="help" class="f7 mb3 lh-title">
{{ help }}
</p>
</div>
</template>
<script>
export default {
inheritAttrs: false,
model: {
prop: 'modelValue',
event: 'update:modelValue'
},
props: {
id: {
type: String,
default: 'text-area-',
},
type: {
type: String,
default: 'text',
},
textareaClass: {
type: String,
default: '',
},
modelValue: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
help: {
type: String,
default: '',
},
required: {
type: Boolean,
default: false,
},
rows: {
type: Number,
default: 3,
},
maxlength: {
type: Number,
default: null,
},
},
emits: [
'esc-key-pressed', 'update:modelValue'
],
data() {
return {
displayMaxLength: false,
};
},
created() {
this.localTextAreaClasses = 'border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm ' + this.textareaClass;
},
computed: {
charactersLeft() {
var char = 0;
if (this.modelValue) {
char = this.modelValue.length;
}
return `${this.maxlength - char} / ${this.maxlength}`;
},
},
methods: {
sendEscKey() {
this.$emit('esc-key-pressed');
},
showMaxLength() {
this.displayMaxLength = true;
}
},
};
</script>
+142
View File
@@ -0,0 +1,142 @@
<style lang="scss" scoped>
.optional-badge {
border-radius: 4px;
color: #283e59;
background-color: #edf2f9;
padding: 1px 3px;
}
.length {
top: 10px;
right: 10px;
background-color: #e5eeff;
padding: 3px 4px;
}
.counter {
padding-right: 64px;
}
</style>
<template>
<div :class="divOuterClass">
<label v-if="label" class="block text-sm mb-2" :for="id">
{{ label }}
<span v-if="!required" class="optional-badge text-xs">
optional
</span>
</label>
<div class="relative component">
<input
:class="localInputClasses"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
ref="input"
:type="type"
:maxlength="maxlength"
:id="id"
:required="required"
:autofocus="autofocus"
@focus="showMaxLength"
@blur="displayMaxLength = false"
>
<span v-if="maxlength && displayMaxLength" class="length absolute text-xs rounded">
{{ charactersLeft }}
</span>
</div>
<p v-if="help" class="text-sm mb-3">
{{ help }}
</p>
</div>
</template>
<script>
export default {
emits: ['update:modelValue'],
props: {
id: {
type: String,
default: 'text-input-',
},
inputClass: {
type: String,
default: '',
},
divOuterClass: {
type: String,
default: '',
},
modelValue: {
type: [String, Number],
default: '',
},
type: {
type: String,
default: 'text',
},
name: {
type: String,
default: 'input',
},
placeholder: {
type: String,
default: '',
},
help: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
required: {
type: Boolean,
default: false,
},
autofocus: {
type: Boolean,
default: false,
},
maxlength: {
type: Number,
default: null,
},
},
data() {
return {
localInputClasses: '',
displayMaxLength: false,
};
},
computed: {
charactersLeft() {
var char = 0;
if (this.modelValue) {
char = this.modelValue.length;
}
return `${this.maxlength - char} / ${this.maxlength}`;
},
},
created() {
this.localInputClasses = 'border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm ' + this.inputClass;
},
methods: {
focus() {
this.$refs.input.focus()
},
showMaxLength() {
this.displayMaxLength = true;
}
}
}
</script>
+94
View File
@@ -0,0 +1,94 @@
<style lang="scss" scoped>
.flash.notification {
z-index: 99999999999;
bottom: 30px;
right: 0px;
transform: translate(100%);
transition: all 0.8s ease-in-out;
background-color: #fff;
border: 1px solid #e7e7e7;
border-radius: 6px;
box-shadow: 1px 1px 2px rgba(122, 122, 122, 0.17);
padding: 10px 20px;
&.is-visible {
transform: translate(0);
opacity: 1;
right: 30px;
}
}
</style>
<template>
<div class="flash notification fixed" :class="[
levelClass, isOpen ? isVisibleClass : ''
]"
>
<span class="mr-1">👋</span> {{ messageText }}
</div>
</template>
<script>
export default {
props: {
level: {
type: String,
default: '',
},
message: {
type: String,
default: '',
},
},
data() {
return {
isOpen: false,
isVisibleClass: 'is-visible',
closeAfter: 5000, // 10 seconds, you can change that
levelClass: null,
messageText: null
};
},
created() {
if (this.level) {
this.levelClass = 'is-' + this.level;
}
if (this.message) {
this.messageText = this.message;
this.show();
}
const self = this;
this.$on('flash', data => self.show(data));
},
methods: {
show(data) {
if (data) {
this.messageText = data.message;
this.levelClass = 'is-' + data.level;
}
this.act(true, 100);
this.hide();
},
hide() {
this.act(false, this.closeAfter);
},
act(action, timeout) {
const self = this;
setTimeout(() => {
self.isOpen = action;
}, timeout);
}
},
};
</script>
+1 -1
View File
@@ -12,7 +12,7 @@ createInertiaApp({
setup({ el, app, props, plugin }) {
return createApp({ render: () => h(app, props) })
.use(plugin)
.mixin({ methods: { route } })
.mixin({ methods: _.assign({ route }, require('./methods').default) })
.mount(el);
},
});
+19
View File
@@ -0,0 +1,19 @@
import Emitter from 'tiny-emitter';
const emitter = new Emitter();
export default {
/**
* Flash a message.
*
* @param {string} message
* @param {string} level
*/
flash(message, level = 'success') {
this.$emitt('flash', { message, level });
},
$on: (...args) => emitter.on(...args),
$once: (...args) => emitter.once(...args),
$off: (...args) => emitter.off(...args),
$emitt: (...args) => emitter.emit(...args),
};
+20 -19
View File
@@ -1,27 +1,28 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title inertia>{{ config('app.name', 'Laravel') }}</title>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">
<title inertia>{{ config('app.name', 'Laravel') }}</title>
<!-- Styles -->
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<!-- Styles -->
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<!-- Scripts -->
@routes
<script src="{{ mix('js/app.js') }}" defer></script>
</head>
<body class="font-sans antialiased">
@inertia
<!-- Scripts -->
@routes
<script src="{{ mix('js/app.js') }}" defer></script>
</head>
<body class="font-sans antialiased">
@inertia
@env ('local')
<script src="http://localhost:8080/js/bundle.js"></script>
<script src="https://cdn.jsdelivr.net/gh/underground-works/clockwork-browser@1/dist/toolbar.js"></script>
@endenv
</body>
@env ('local')
<script src="http://localhost:8080/js/bundle.js"></script>
@endenv
</body>
</html>
+1 -1
View File
@@ -59,6 +59,6 @@ Route::get('/confirm-password', [ConfirmablePasswordController::class, 'show'])
Route::post('/confirm-password', [ConfirmablePasswordController::class, 'store'])
->middleware('auth');
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
Route::get('/logout', [AuthenticatedSessionController::class, 'destroy'])
->middleware('auth')
->name('logout');
+10 -4
View File
@@ -3,6 +3,7 @@
use Inertia\Inertia;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Application;
use App\Http\Controllers\Vault\VaultController;
/*
|--------------------------------------------------------------------------
@@ -29,15 +30,20 @@ require __DIR__.'/auth.php';
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', 'Dashboard\\DashboardController@index')->name('dashboard');
Route::get('home', 'HomeController@index')->name('home');
// vaults
Route::get('vaults', [VaultController::class, 'index'])->name('vault.index');
Route::get('vaults/new', [VaultController::class, 'new'])->name('vault.new');
Route::post('vaults', [VaultController::class, 'store'])->name('vault.store');
Route::middleware(['vault'])->prefix('vaults/{vault}')->group(function () {
Route::get('', [VaultController::class, 'show'])->name('vault.show');
});
Route::get('contacts', 'ContactController@index');
Route::get('settings', 'Settings\\SettingsController@index')->name('settings.index');
Route::resource('settings/information', 'Settings\\InformationController');
// vaults
Route::get('vaults/{vault}', 'HomeController@index')->name('vault.show');
// contacts
Route::get('vaults/{vault}/contacts/{contact}', 'HomeController@index')->name('contact.show');
});
+3
View File
@@ -0,0 +1,3 @@
*.json
*.json.gz
index
@@ -58,10 +58,6 @@ class CreateAccountTest extends TestCase
'is_account_administrator' => true,
]);
$this->assertDatabaseHas('user_vault', [
'user_id' => $user->id,
]);
$this->assertInstanceOf(
User::class,
$user
@@ -74,9 +70,5 @@ class CreateAccountTest extends TestCase
Queue::assertPushed(CreateAuditLog::class, function ($job) {
return $job->auditLog['action_name'] === 'account_created';
});
Queue::assertPushed(CreateAuditLog::class, function ($job) {
return $job->auditLog['action_name'] === 'vault_created';
});
}
}
@@ -0,0 +1,27 @@
<?php
namespace Tests\Features\Vault\ManageVault\ViewHelpers;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Features\Vault\ManageVault\ViewHelpers\VaultCreateViewHelper;
class VaultCreateViewHelperTest extends TestCase
{
use DatabaseTransactions;
/** @test */
public function it_gets_the_data_needed_for_the_view(): void
{
$array = VaultCreateViewHelper::data();
$this->assertEquals(
[
'url' => [
'store' => env('APP_URL').'/vaults',
'back' => env('APP_URL').'/vaults',
],
],
$array
);
}
}
@@ -0,0 +1,74 @@
<?php
namespace Tests\Features\Vault\ManageVault\ViewHelpers;
use Tests\TestCase;
use App\Models\User;
use App\Models\Vault;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Features\Vault\ManageVault\ViewHelpers\VaultIndexViewHelper;
class VaultIndexViewHelperTest extends TestCase
{
use DatabaseTransactions;
/** @test */
public function it_gets_general_layout_information(): void
{
$user = User::factory()->create();
$vault = Vault::factory()->create();
$this->be($user);
$array = VaultIndexViewHelper::layoutData($vault);
$this->assertEquals(
[
'user' => [
'name' => $user->name,
],
'vault' => [
'id' => $vault->id,
'name' => $vault->name,
],
'url' => [
'vaults' => env('APP_URL').'/vaults',
'logout' => env('APP_URL').'/logout',
],
],
$array
);
}
/** @test */
public function it_gets_the_data_needed_for_the_view(): void
{
$vault = Vault::factory()->create();
$array = VaultIndexViewHelper::data($vault->account);
$this->assertEquals(2, count($array));
$this->assertEquals(
[
0 => [
'id' => $vault->id,
'name' => $vault->name,
'description' => $vault->description,
'url' => [
'show' => env('APP_URL').'/vaults/'.$vault->id,
],
],
],
$array['vaults']->toArray()
);
$this->assertEquals(
[
'vault' => [
'new' => env('APP_URL').'/vaults/new',
],
],
$array['url']
);
}
}
+37 -3
View File
@@ -1829,7 +1829,7 @@ charenc@0.0.2:
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
chokidar@^3.5.2:
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
@@ -3178,6 +3178,11 @@ img-loader@^4.0.0:
dependencies:
loader-utils "^1.1.0"
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
import-cwd@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92"
@@ -3581,7 +3586,7 @@ loader-runner@^4.2.0:
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.4.0:
loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
@@ -3853,7 +3858,7 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
neo-async@^2.6.2:
neo-async@^2.5.0, neo-async@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
@@ -4214,6 +4219,11 @@ pify@^2.3.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
pify@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
pkg-dir@^4.1.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@@ -4872,6 +4882,25 @@ safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1,
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass-loader@^7.1.0:
version "7.3.1"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f"
integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==
dependencies:
clone-deep "^4.0.1"
loader-utils "^1.0.1"
neo-async "^2.5.0"
pify "^4.0.1"
semver "^6.3.0"
sass@^1.15.2:
version "1.44.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.44.0.tgz#619aa0a2275c097f9af5e6b8fe8a95e3056430fb"
integrity sha512-0hLREbHFXGQqls/K8X+koeP+ogFRPF4ZqetVB19b7Cst9Er8cOR0rc6RU7MaI4W1JmUShd1BPgPoeqmmgMMYFw==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
schema-utils@^2.6.5:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
@@ -5336,6 +5365,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-emitter@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tmp@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"