Skip to content

Commit 3789378

Browse files
committed
feat: make name unique & email optional for users who don't want to log in + release: v1.3.0
1 parent 4897c2d commit 3789378

File tree

7 files changed

+129
-14
lines changed

7 files changed

+129
-14
lines changed

DEVELOPER-DOCS.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The past development is documented in the [4_IMPLEMENTATION.md](docs/4_IMPLEMENT
1616
- [Prerequisites](#prerequisites)
1717
- [Commands for Everyday Development](#commands-for-everyday-development)
1818
- [Setting Up the Development Environment](#setting-up-the-development-environment)
19+
- [Seeding Production](#seeding-production)
1920
- [Usage](#usage)
2021
- [Accessing the Workspace Container](#accessing-the-workspace-container)
2122
- [Run Artisan Commands:](#run-artisan-commands)
@@ -123,6 +124,90 @@ npm run dev
123124

124125
Open your browser and navigate to [http://localhost](http://localhost).
125126

127+
## Seeding Production
128+
129+
Seeding production isn't as easy as running `docker compose exec php-fpm php artisan db:seed --force` on the server [as in the Laravel docs](https://laravel.com/docs/12.x/seeding#forcing-seeding-production).
130+
131+
That's because neither `fakerphp/faker` (the dev dependency most crucial to seeding) nor `composer` itself is included in the `production` Docker image (see the [docker/deployment/php-fpm/Dockerfile](docker/deployment/php-fpm/Dockerfile) -> only the production dependencies get copied from a previous builder stage to the production stage).
132+
133+
Thus, The easiest way to seed production like development here, is exporting the data from the development Postgres database, getting the exported data to the server and importing it there.
134+
135+
> **Warning:** Do this production seeding just for demonstration purposes and delete all that data before serious usage.<br>
136+
> The seeded admin user's credentials are publicly visible in this project's [DatabaseSeeder.php](database/seeders/DatabaseSeeder.php) and all other users have the password `password`.
137+
138+
1. Run the waffle-dashboard as described in the [Commands for Everyday Development](#commands-for-everyday-development) section.
139+
2. Export the data from the two main database tables
140+
```shell
141+
docker compose -f compose.dev.yaml exec postgres bash
142+
\copy users TO '/tmp/users.csv' CSV HEADER;
143+
\copy waffle_eatings TO '/tmp/waffle_eatings.csv' CSV HEADER;
144+
exit
145+
exit
146+
```
147+
3. Copy the CSV files from the Postgres container to your local host machine
148+
```shell
149+
docker cp waffle-dashboard-postgres-1:/tmp/users.csv ./users.csv
150+
docker cp waffle-dashboard-postgres-1:/tmp/waffle_eatings.csv ./waffle_eatings.csv
151+
```
152+
4. Transfer it from your local host machine to the server
153+
```shell
154+
scp users.csv waffle_eatings.csv root@lchristmann-1:/opt/waffle-dashboard
155+
```
156+
> Given I have below SSH config in my `~/.ssh/config` file
157+
>
158+
> ```
159+
> Host lchristmann-1
160+
> HostName yourServersIPAddressHere
161+
> User root
162+
> IdentityFile ~/.ssh/lchristmann-1
163+
> ```
164+
5. SSH into the server and `cd` into the waffle-dashboard's directory
165+
```shell
166+
ssh lchristmann-1
167+
cd /opt/waffle-dashboard
168+
```
169+
6. Copy the CSV files from the server to the Postgres container
170+
```shell
171+
docker cp users.csv waffle-dashboard-postgres-1:/tmp/users.csv
172+
docker cp waffle_eatings.csv waffle-dashboard-postgres-1:/tmp/waffle_eatings.csv
173+
```
174+
7. Backup the data from the users table
175+
```shell
176+
docker compose exec -T postgres psql -U laravel -d app -c "\
177+
COPY (SELECT name, email, email_verified_at, password, is_admin, remember_token, created_at, updated_at FROM users) \
178+
TO STDOUT WITH CSV HEADER" > users_data.csv
179+
```
180+
8. Delete all data from the `users` and `waffle_eatings` tables (else we'd run into conflicts trying to insert records with already existing `id` values)
181+
```shell
182+
docker compose exec postgres psql -d app -U laravel
183+
TRUNCATE TABLE waffle_eatings, users RESTART IDENTITY;
184+
```
185+
9. Import the seeding data to the server's Postgres database
186+
```postgresql
187+
\copy users FROM '/tmp/users.csv' DELIMITER ',' CSV HEADER;
188+
\copy waffle_eatings FROM '/tmp/waffle_eatings.csv' DELIMITER ',' CSV HEADER;
189+
SELECT setval(pg_get_serial_sequence('users','id'), (SELECT MAX(id) FROM users));
190+
SELECT setval(pg_get_serial_sequence('waffle_eatings','id'), (SELECT MAX(id) FROM waffle_eatings));
191+
```
192+
```shell
193+
exit
194+
```
195+
10. Restore the previously present users data
196+
```shell
197+
docker compose exec -T postgres psql -U laravel -d app -c "\
198+
COPY users(name, email, email_verified_at, password, is_admin, remember_token, created_at, updated_at) \
199+
FROM STDIN WITH CSV HEADER" < users_data.csv
200+
```
201+
202+
Done - now you can log in for example with the admin user: `[email protected]` and `admin` or any other user with their email and password `password`.
203+
204+
To clean up the temporary files, run below commands:
205+
206+
```shell
207+
rm users.csv waffle_eatings.csv # in this project
208+
rm /opt/waffle-dashboard/users_data.csv /opt/waffle-dashboard/waffle_eatings.csv # on the server
209+
```
210+
126211
## Usage
127212

128213
Here are some common commands and tips for using the development environment:

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ The Waffle Dashboard is a lightweight, self-hostable application for tracking an
99
![screenshot of the waffle dashboard](docs/screenshots/dashboard.png)
1010
_Check out the [docs/screenshots](docs/screenshots) folder._
1111

12+
> Please star the repository if you find this project useful - it helps me understand how many people rely on it when making changes.
13+
1214
## Table of Contents <!-- omit in toc -->
1315

1416
- [Get the Waffle Dashboard](#get-the-waffle-dashboard)

app/Filament/Actions/UserBulkCreateAction.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Filament\Forms\Components\TextInput;
99
use Filament\Forms\Components\Toggle;
1010
use Filament\Schemas\Components\Grid;
11+
use Filament\Schemas\Components\Utilities\Get;
1112
use Illuminate\Support\Facades\Hash;
1213

1314
class UserBulkCreateAction extends Action
@@ -30,11 +31,17 @@ protected function setUp(): void
3031
->schema([
3132
Grid::make()
3233
->schema([
33-
TextInput::make('name')->required()->maxLength(255),
34-
TextInput::make('email')->required()->email()->maxLength(255),
34+
TextInput::make('name')->unique()->required()->maxLength(255),
35+
TextInput::make('email')->unique()->email()->maxLength(255)
36+
->required(fn (Get $get): bool => filled($get('password')))
37+
->live(onBlur: true),
3538

36-
TextInput::make('password')->password()->required()
37-
->dehydrateStateUsing(fn ($state) => $state ? Hash::make($state) : null),
39+
TextInput::make('password')
40+
->password()
41+
->required(fn (Get $get): bool => filled($get('email')))
42+
->dehydrateStateUsing(fn ($state) => $state ? Hash::make($state) : null)
43+
->dehydrated()
44+
->live(onBlur: true),
3845

3946
Toggle::make('is_admin')->label('Admin')->inline(false),
4047
])
@@ -46,8 +53,8 @@ protected function setUp(): void
4653
foreach ($data['users'] as $userData) {
4754
User::create([
4855
'name' => $userData['name'],
49-
'email' => $userData['email'],
50-
'password' => $userData['password'],
56+
'email' => $userData['email'] ?? null,
57+
'password' => $userData['password'] ?? null,
5158
'is_admin' => $userData['is_admin'] ?? false,
5259
]);
5360
}

app/Filament/Resources/Users/Schemas/UserForm.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Models\User;
66
use Filament\Forms\Components\TextInput;
77
use Filament\Forms\Components\Toggle;
8+
use Filament\Schemas\Components\Utilities\Get;
89
use Filament\Schemas\Schema;
910
use Illuminate\Support\Facades\Hash;
1011

@@ -14,14 +15,17 @@ public static function configure(Schema $schema): Schema
1415
{
1516
return $schema
1617
->components([
17-
TextInput::make('name')->required(fn (string $context) => $context === 'create')->maxLength(255),
18-
TextInput::make('email')->required(fn (string $context) => $context === 'create')->email()->maxLength(255),
18+
TextInput::make('name')->unique()->required()->maxLength(255),
19+
TextInput::make('email')->unique()->email()->maxLength(255)
20+
->required(fn (Get $get, string $context) => $context === 'create' && filled($get('password')))
21+
->live(onBlur: true),
1922

2023
TextInput::make('password')
2124
->password()
22-
->required(fn (string $context) => $context === 'create')
25+
->required(fn (Get $get, string $context) => $context === 'create' && filled($get('email')))
2326
->dehydrateStateUsing(fn ($state) => $state ? Hash::make($state) : null)
24-
->dehydrated(fn ($state) => filled($state)),
27+
->dehydrated(fn ($state) => filled($state))
28+
->live(onBlur: true),
2529

2630
Toggle::make('is_admin')->label('Admin')->inline(false)
2731
->disabled(fn (?User $record) => $record && $record->id === auth()->id())

database/migrations/0001_01_01_000000_create_users_table.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ public function up(): void
1313
{
1414
Schema::create('users', function (Blueprint $table) {
1515
$table->id();
16-
$table->string('name');
17-
$table->string('email')->unique();
16+
$table->string('name')->unique();
17+
$table->string('email')->nullable();
1818
$table->timestamp('email_verified_at')->nullable();
19-
$table->string('password');
19+
$table->string('password')->nullable();
2020
$table->boolean('is_admin')->default(false);
2121
$table->rememberToken();
2222
$table->timestamps();
2323
});
2424

25+
DB::statement('CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE email IS NOT NULL;');
26+
2527
Schema::create('password_reset_tokens', function (Blueprint $table) {
2628
$table->string('email')->primary();
2729
$table->string('token');

docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ services:
3030

3131
php-fpm:
3232
# For the php-fpm service, we will create a custom image to install the necessary PHP extensions and setup proper permissions.
33-
image: leanderchristmann/waffle-dashboard:1.2.0
33+
image: leanderchristmann/waffle-dashboard:1.3.0
3434
restart: unless-stopped
3535
volumes:
3636
# Mount persistent storage

docs/4_IMPLEMENTATION.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,18 @@ On this occasion, I also optimized the Leaderboard page (now running one query f
341341
> Commits:
342342
>
343343
> - `bb786c22`: `docs: upload new screenshots`
344+
> - `4897c2db`: `release: v1.2.0`
345+
346+
## 19. Further Improve the User Experience (UX) (`v1.3.0`)
347+
348+
In practice, there'll be users, who don't manage their own waffle eatings entries, but let others enter them.<br>
349+
Hence the users bulk create would be too cumbersome, if it didn't allow creation just by name.
350+
351+
Therefor we need the name to be unique. Email and password can be set (together, forming a pair of credentials), but don't have to.
352+
353+
Extra: for showcasing purposes I added instructions on how to transfer seeding data into a production environment.
354+
355+
Some helpful documentation pages:
356+
357+
- https://filamentphp.com/docs/4.x/forms/overview#conditionally-making-a-field-required
358+
- https://stackoverflow.com/questions/8289100/create-unique-constraint-with-null-columns

0 commit comments

Comments
 (0)