🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,32 @@
{
"name": "filament/widgets",
"description": "Easily add beautiful dashboard widgets to any Livewire component.",
"license": "MIT",
"homepage": "https://github.com/filamentphp/filament",
"support": {
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"require": {
"php": "^8.2",
"filament/schemas": "self.version",
"filament/support": "self.version"
},
"autoload": {
"psr-4": {
"Filament\\Widgets\\": "src"
}
},
"extra": {
"laravel": {
"providers": [
"Filament\\Widgets\\WidgetsServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,339 @@
---
title: Overview
---
## Introduction
Filament allows you to build dynamic dashboards, comprised of "widgets". Each widget is an element on the dashboard that displays data in a specific way. For example, you can display [stats](stats-overview), [chart](charts), or a [table](#table-widgets).
## Creating a widget
To create a widget, you can use the `make:filament-widget` command:
```bash
php artisan make:filament-widget MyWidget
```
This command will ask you which type of widget you want to create. You can choose from the following options:
- **Custom**: A custom widget that you can build from scratch.
- **Chart**: A widget that displays a [chart](charts).
- **Stats overview**: A widget that displays [statistics](stats-overview).
- **Table**: A widget that displays a [table](#table-widgets).
## Sorting widgets
Each widget class contains a `$sort` property that may be used to change its order on the page, relative to other widgets:
```php
protected static ?int $sort = 2;
```
## Customizing the dashboard page
If you want to customize the dashboard class, for example, to [change the number of widget columns](#customizing-the-widgets-grid), create a new file at `app/Filament/Pages/Dashboard.php`:
```php
<?php
namespace App\Filament\Pages;
use Filament\Pages\Dashboard as BaseDashboard;
class Dashboard extends BaseDashboard
{
// ...
}
```
Finally, remove the original `Dashboard` class from [configuration file](../panel-configuration):
```php
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([]);
}
```
If you don't discover pages with `discoverPages()` in the directory you created the new dashboard class, you should manually register the class in the `pages()` method:
```php
use App\Filament\Pages\Dashboard;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->pages([
Dashboard::class,
]);
}
```
### Creating multiple dashboards
If you want to create multiple dashboards, you can do so by repeating [the process described above](#customizing-the-dashboard-page). Creating new pages that extend the `Dashboard` class will allow you to create as many dashboards as you need.
You will also need to define the URL path to the extra dashboard, otherwise it will be at `/`:
```php
protected static string $routePath = 'finance';
```
You may also customize the title of the dashboard by overriding the `$title` property:
```php
protected static ?string $title = 'Finance dashboard';
```
The primary dashboard shown to a user is the first one they have access to (controlled by [`canAccess()` method](../navigation/custom-pages#authorization)), according to the defined navigation sort order.
The default sort order for dashboards is `-2`. You can control the sort order of custom dashboards with `$navigationSort`:
```php
protected static ?int $navigationSort = 15;
```
### Customizing the widgets' grid
You may change how many grid columns are used to display widgets.
Firstly, you must [replace the original Dashboard page](#customizing-the-dashboard-page).
Now, in your new `app/Filament/Pages/Dashboard.php` file, you may override the `getColumns()` method to return a number of grid columns to use:
```php
public function getColumns(): int | array
{
return 2;
}
```
#### Responsive widgets grid
You may wish to change the number of widget grid columns based on the responsive [breakpoint](https://tailwindcss.com/docs/responsive-design#overview) of the browser. You can do this using an array that contains the number of columns that should be used at each breakpoint:
```php
public function getColumns(): int | array
{
return [
'md' => 4,
'xl' => 5,
];
}
```
This pairs well with [responsive widget widths](#responsive-widget-widths).
#### Customizing widget width
You may customize the width of a widget using the `$columnSpan` property. You may use a number between 1 and 12 to indicate how many columns the widget should span, or `full` to make it occupy the full width of the page:
```php
protected int | string | array $columnSpan = 'full';
```
##### Responsive widget widths
You may wish to change the widget width based on the responsive [breakpoint](https://tailwindcss.com/docs/responsive-design#overview) of the browser. You can do this using an array that contains the number of columns that the widget should occupy at each breakpoint:
```php
protected int | string | array $columnSpan = [
'md' => 2,
'xl' => 3,
];
```
This is especially useful when using a [responsive widgets grid](#responsive-widgets-grid).
## Conditionally hiding widgets
You may override the static `canView()` method on widgets to conditionally hide them:
```php
public static function canView(): bool
{
return auth()->user()->isAdmin();
}
```
## Table widgets
You may easily add tables to your dashboard. Start by creating a widget with the command:
```bash
php artisan make:filament-widget LatestOrders --table
```
You may now [customize the table](../tables) by editing the widget file.
## Custom widgets
To get started building a `BlogPostsOverview` widget:
```bash
php artisan make:filament-widget BlogPostsOverview
```
This command will create two files - a widget class in the `/Widgets` directory of the Filament directory, and a view in the `/widgets` directory of the Filament views directory.
The class is a [Livewire component](https://livewire.laravel.com/docs/components), so any Livewire features are available to you. The Blade view can contain any HTML you like, and you can access any public Livewire properties in the view. You can also access the Livewire component instance in the view using `$this`.
## Filtering widget data
You may add a form to the dashboard that allows the user to filter the data displayed across all widgets. When the filters are updated, the widgets will be reloaded with the new data.
Firstly, you must [replace the original Dashboard page](#customizing-the-dashboard-page).
Now, in your new `app/Filament/Pages/Dashboard.php` file, you may add the `HasFiltersForm` trait, and add the `filtersForm()` method to return form components:
```php
use Filament\Forms\Components\DatePicker;
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Concerns\HasFiltersForm;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class Dashboard extends BaseDashboard
{
use HasFiltersForm;
public function filtersForm(Schema $schema): Schema
{
return $schema
->components([
Section::make()
->schema([
DatePicker::make('startDate'),
DatePicker::make('endDate'),
// ...
])
->columns(3),
]);
}
}
```
In widget classes that require data from the filters, you need to add the `InteractsWithPageFilters` trait, which will allow you to use the `$this->pageFilters` property to access the raw data from the filters form:
```php
use App\Models\BlogPost;
use Carbon\CarbonImmutable;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\Concerns\InteractsWithPageFilters;
use Illuminate\Database\Eloquent\Builder;
class BlogPostsOverview extends StatsOverviewWidget
{
use InteractsWithPageFilters;
public function getStats(): array
{
$startDate = $this->pageFilters['startDate'] ?? null;
$endDate = $this->pageFilters['endDate'] ?? null;
return [
StatsOverviewWidget\Stat::make(
label: 'Total posts',
value: BlogPost::query()
->when($startDate, fn (Builder $query) => $query->whereDate('created_at', '>=', $startDate))
->when($endDate, fn (Builder $query) => $query->whereDate('created_at', '<=', $endDate))
->count(),
),
// ...
];
}
}
```
The `$this->pageFilters` array will always reflect the current form data. Please note that this data is not validated, as it is available live and not intended to be used for anything other than querying the database. You must ensure that the data is valid before using it. In this example, we check if the start date is set before using it in the query.
### Filtering widget data using an action modal
Alternatively, you can swap out the filters form for an action modal, that can be opened by clicking a button in the header of the page. There are many benefits to using this approach:
- The filters form is not always visible, which allows you to use the full height of the page for widgets.
- The filters do not update the widgets until the user clicks the "Apply" button, which means that the widgets are not reloaded until the user is ready. This can improve performance if the widgets are expensive to load.
- Validation can be performed on the filters form, which means that the widgets can rely on the fact that the data is valid - the user cannot submit the form until it is. Canceling the modal will discard the user's changes.
To use an action modal instead of a filters form, you can use the `HasFiltersAction` trait instead of `HasFiltersForm`. Then, register the `FilterAction` class as an action in `getHeaderActions()`:
```php
use Filament\Forms\Components\DatePicker;
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Actions\FilterAction;
use Filament\Pages\Dashboard\Concerns\HasFiltersAction;
class Dashboard extends BaseDashboard
{
use HasFiltersAction;
protected function getHeaderActions(): array
{
return [
FilterAction::make()
->schema([
DatePicker::make('startDate'),
DatePicker::make('endDate'),
// ...
]),
];
}
}
```
Handling data from the filter action is the same as handling data from the filters header form, except that the data is validated before being passed to the widget. The `InteractsWithPageFilters` trait still applies.
### Persisting widget filters in the user's session
By default, the dashboard filters applied will persist in the user's session between page loads. To disable this, override the `$persistsFiltersInSession` property in the dashboard page class:
```php
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Concerns\HasFiltersForm;
class Dashboard extends BaseDashboard
{
use HasFiltersForm;
protected bool $persistsFiltersInSession = false;
}
```
Alternatively, override the `persistsFiltersInSession()` method in the dashboard page class:
```php
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Concerns\HasFiltersForm;
class Dashboard extends BaseDashboard
{
use HasFiltersForm;
public function persistsFiltersInSession(): bool
{
return false;
}
}
```
## Disabling the default widgets
By default, two widgets are displayed on the dashboard. These widgets can be disabled by updating the `widgets()` array of the [configuration](../panel-configuration):
```php
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->widgets([]);
}
```
@@ -0,0 +1,191 @@
---
title: Stats overview widgets
---
## Introduction
Filament comes with a "stats overview" widget template, which you can use to display a number of different stats in a single widget, without needing to write a custom view.
Start by creating a widget with the command:
```bash
php artisan make:filament-widget StatsOverview --stats-overview
```
This command will create a new `StatsOverview.php` file. Open it, and return `Stat` instances from the `getStats()` method:
```php
<?php
namespace App\Filament\Widgets;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class StatsOverview extends BaseWidget
{
protected function getStats(): array
{
return [
Stat::make('Unique views', '192.1k'),
Stat::make('Bounce rate', '21%'),
Stat::make('Average time on page', '3:12'),
];
}
}
```
Now, check out your widget in the dashboard.
## Adding a description and icon to a stat
You may add a `description()` to provide additional information, along with a `descriptionIcon()`:
```php
use Filament\Widgets\StatsOverviewWidget\Stat;
protected function getStats(): array
{
return [
Stat::make('Unique views', '192.1k')
->description('32k increase')
->descriptionIcon('heroicon-m-arrow-trending-up'),
Stat::make('Bounce rate', '21%')
->description('7% decrease')
->descriptionIcon('heroicon-m-arrow-trending-down'),
Stat::make('Average time on page', '3:12')
->description('3% increase')
->descriptionIcon('heroicon-m-arrow-trending-up'),
];
}
```
The `descriptionIcon()` method also accepts a second parameter to put the icon before the description instead of after it:
```php
use Filament\Support\Enums\IconPosition;
use Filament\Widgets\StatsOverviewWidget\Stat;
Stat::make('Unique views', '192.1k')
->description('32k increase')
->descriptionIcon('heroicon-m-arrow-trending-up', IconPosition::Before)
```
## Changing the color of the stat
You may also give stats a [color](../styling/colors):
```php
use Filament\Widgets\StatsOverviewWidget\Stat;
protected function getStats(): array
{
return [
Stat::make('Unique views', '192.1k')
->description('32k increase')
->descriptionIcon('heroicon-m-arrow-trending-up')
->color('success'),
Stat::make('Bounce rate', '21%')
->description('7% increase')
->descriptionIcon('heroicon-m-arrow-trending-down')
->color('danger'),
Stat::make('Average time on page', '3:12')
->description('3% increase')
->descriptionIcon('heroicon-m-arrow-trending-up')
->color('success'),
];
}
```
## Adding extra HTML attributes to a stat
You may also pass extra HTML attributes to stats using `extraAttributes()`:
```php
use Filament\Widgets\StatsOverviewWidget\Stat;
protected function getStats(): array
{
return [
Stat::make('Processed', '192.1k')
->color('success')
->extraAttributes([
'class' => 'cursor-pointer',
'wire:click' => "\$dispatch('setStatusFilter', { filter: 'processed' })",
]),
// ...
];
}
```
In this example, we are deliberately escaping the `$` in `$dispatch()` since this needs to be passed directly to the HTML, it is not a PHP variable.
## Adding a chart to a stat
You may also add or chain a `chart()` to each stat to provide historical data. The `chart()` method accepts an array of data points to plot:
```php
use Filament\Widgets\StatsOverviewWidget\Stat;
protected function getStats(): array
{
return [
Stat::make('Unique views', '192.1k')
->description('32k increase')
->descriptionIcon('heroicon-m-arrow-trending-up')
->chart([7, 2, 10, 3, 15, 4, 17])
->color('success'),
// ...
];
}
```
## Live updating stats (polling)
By default, stats overview widgets refresh their data every 5 seconds.
To customize this, you may override the `$pollingInterval` property on the class to a new interval:
```php
protected ?string $pollingInterval = '10s';
```
Alternatively, you may disable polling altogether:
```php
protected ?string $pollingInterval = null;
```
## Disabling lazy loading
By default, widgets are lazy-loaded. This means that they will only be loaded when they are visible on the page.
To disable this behavior, you may override the `$isLazy` property on the widget class:
```php
protected static bool $isLazy = false;
```
## Adding a heading and description
You may also add heading and description text above the widget by overriding the `$heading` and `$description` properties:
```php
protected ?string $heading = 'Analytics';
protected ?string $description = 'An overview of some analytics.';
```
If you need to dynamically generate the heading or description text, you can instead override the `getHeading()` and `getDescription()` methods:
```php
protected function getHeading(): ?string
{
return 'Analytics';
}
protected function getDescription(): ?string
{
return 'An overview of some analytics.';
}
```
@@ -0,0 +1,392 @@
---
title: Chart widgets
---
import Aside from "@components/Aside.astro"
## Introduction
Filament comes with many "chart" widget templates, which you can use to display real-time, interactive charts.
Start by creating a widget with the command:
```bash
php artisan make:filament-widget BlogPostsChart --chart
```
There is a single `ChartWidget` class that is used for all charts. The type of chart is set by the `getType()` method. In this example, that method returns the string `'line'`.
The `protected ?string $heading` variable is used to set the heading that describes the chart. If you need to set the heading dynamically, you can override the `getHeading()` method.
The `getData()` method is used to return an array of datasets and labels. Each dataset is a labeled array of points to plot on the chart, and each label is a string. This structure is identical to the [Chart.js](https://www.chartjs.org/docs) library, which Filament uses to render charts. You may use the [Chart.js documentation](https://www.chartjs.org/docs) to fully understand the possibilities to return from `getData()`, based on the chart type.
```php
<?php
namespace App\Filament\Widgets;
use Filament\Widgets\ChartWidget;
class BlogPostsChart extends ChartWidget
{
protected ?string $heading = 'Blog Posts';
protected function getData(): array
{
return [
'datasets' => [
[
'label' => 'Blog posts created',
'data' => [0, 10, 5, 2, 21, 32, 45, 74, 65, 45, 77, 89],
],
],
'labels' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
];
}
protected function getType(): string
{
return 'line';
}
}
```
Now, check out your widget in the dashboard.
## Available chart types
Below is a list of available chart widget classes which you may extend, and their corresponding [Chart.js](https://www.chartjs.org/docs) documentation page, for inspiration on what to return from `getData()`:
- Bar chart - [Chart.js documentation](https://www.chartjs.org/docs/latest/charts/bar)
- Bubble chart - [Chart.js documentation](https://www.chartjs.org/docs/latest/charts/bubble)
- Doughnut chart - [Chart.js documentation](https://www.chartjs.org/docs/latest/charts/doughnut)
- Line chart - [Chart.js documentation](https://www.chartjs.org/docs/latest/charts/line)
- Pie chart - [Chart.js documentation](https://www.chartjs.org/docs/latest/charts/doughnut.html#pie)
- Polar area chart - [Chart.js documentation](https://www.chartjs.org/docs/latest/charts/polar)
- Radar chart - [Chart.js documentation](https://www.chartjs.org/docs/latest/charts/radar)
- Scatter chart - [Chart.js documentation](https://www.chartjs.org/docs/latest/charts/scatter)
## Customizing the chart color
You can customize the [color](../styling/colors) of the chart data by setting the `$color` property:
```php
protected string $color = 'info';
```
If you're looking to customize the color further, or use multiple colors across multiple datasets, you can still make use of Chart.js's [color options](https://www.chartjs.org/docs/latest/general/colors.html) in the data:
```php
protected function getData(): array
{
return [
'datasets' => [
[
'label' => 'Blog posts created',
'data' => [0, 10, 5, 2, 21, 32, 45, 74, 65, 45, 77, 89],
'backgroundColor' => '#36A2EB',
'borderColor' => '#9BD0F5',
],
],
'labels' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
];
}
```
## Generating chart data from an Eloquent model
To generate chart data from an Eloquent model, Filament recommends that you install the `flowframe/laravel-trend` package. You can view the [documentation](https://github.com/Flowframe/laravel-trend).
Here is an example of generating chart data from a model using the `laravel-trend` package:
```php
use Flowframe\Trend\Trend;
use Flowframe\Trend\TrendValue;
protected function getData(): array
{
$data = Trend::model(BlogPost::class)
->between(
start: now()->startOfYear(),
end: now()->endOfYear(),
)
->perMonth()
->count();
return [
'datasets' => [
[
'label' => 'Blog posts',
'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
],
],
'labels' => $data->map(fn (TrendValue $value) => $value->date),
];
}
```
## Filtering chart data
### Basic Select filter
You can set up chart filters to change the data that is presented. Commonly, this is used to change the time period that chart data is rendered for.
To set a default filter value, set the `$filter` property:
```php
public ?string $filter = 'today';
```
Then, define the `getFilters()` method to return an array of values and labels for your filter:
```php
protected function getFilters(): ?array
{
return [
'today' => 'Today',
'week' => 'Last week',
'month' => 'Last month',
'year' => 'This year',
];
}
```
You can use the active filter value within your `getData()` method:
```php
protected function getData(): array
{
$activeFilter = $this->filter;
// ...
}
```
### Custom filters
You can use [schema components](../schemas) to build custom filters for your chart widget. This approach offers a more flexible way to define filters.
To get started, use the `HasFiltersSchema` trait and implement the `filtersSchema()` method:
```php
use Filament\Forms\Components\DatePicker;
use Filament\Schemas\Schema;
use Filament\Widgets\ChartWidget\Concerns\HasFiltersSchema;
class BlogPostsChart extends ChartWidget
{
use HasFiltersSchema;
// ...
public function filtersSchema(Schema $schema): Schema
{
return $schema->components([
DatePicker::make('startDate')
->default(now()->subDays(30)),
DatePicker::make('endDate')
->default(now()),
]);
}
}
```
The filter values are accessible via the `$this->filters` array. You can use these values inside your `getData()` method:
```php
protected function getData(): array
{
$startDate = $this->filters['startDate'] ?? null;
$endDate = $this->filters['endDate'] ?? null;
return [
// ...
];
}
```
The `$this->filters` array will always reflect the current form data. Please note that this data is not validated, as it is available live and not intended to be used for anything other than querying the database. You must ensure that the data is valid before using it.
<Aside variant="info">
If you want to add filters that apply to multiple widgets at once, see [filtering widget data](overview#filtering-widget-data) in the dashboard.
</Aside>
## Live updating chart data (polling)
By default, chart widgets refresh their data every 5 seconds.
To customize this, you may override the `$pollingInterval` property on the class to a new interval:
```php
protected ?string $pollingInterval = '10s';
```
Alternatively, you may disable polling altogether:
```php
protected ?string $pollingInterval = null;
```
## Setting a maximum chart height
You may place a maximum height on the chart to ensure that it doesn't get too big, using the `$maxHeight` property:
```php
protected ?string $maxHeight = '300px';
```
## Setting chart configuration options
You may specify an `$options` variable on the chart class to control the many configuration options that the Chart.js library provides. For instance, you could turn off the [legend](https://www.chartjs.org/docs/latest/configuration/legend.html) for a line chart:
```php
protected ?array $options = [
'plugins' => [
'legend' => [
'display' => false,
],
],
];
```
Alternatively, you can override the `getOptions()` method to return a dynamic array of options:
```php
protected function getOptions(): array
{
return [
'plugins' => [
'legend' => [
'display' => false,
],
],
];
}
```
These PHP arrays will get transformed into JSON objects when the chart is rendered. If you want to return raw JavaScript from this method instead, you can return a `RawJs` object. This is useful if you want to use a JavaScript callback function, for example:
```php
use Filament\Support\RawJs;
protected function getOptions(): RawJs
{
return RawJs::make(<<<JS
{
scales: {
y: {
ticks: {
callback: (value) => '€' + value,
},
},
},
}
JS);
}
```
## Adding a description
You may add a description, below the heading of the chart, using the `getDescription()` method:
```php
public function getDescription(): ?string
{
return 'The number of blog posts published per month.';
}
```
## Disabling lazy loading
By default, widgets are lazy-loaded. This means that they will only be loaded when they are visible on the page.
To disable this behavior, you may override the `$isLazy` property on the widget class:
```php
protected static bool $isLazy = false;
```
## Making the chart collapsible
You may allow the chart to be collapsible by setting the `$isCollapsible` property on the widget class to be `true`:
```php
protected bool $isCollapsible = true;
```
## Using custom Chart.js plugins
Chart.js offers a powerful plugin system that allows you to extend its functionality and create custom chart behaviors. This guide details how to use them in a chart widget.
### Step 1: Install the plugin with NPM
To start with, install the plugin using NPM into your project. In this guide, we will install [`chartjs-plugin-datalabels`](https://chartjs-plugin-datalabels.netlify.app/guide/getting-started.html#installation):
```bash
npm install chartjs-plugin-datalabels --save-dev
```
### Step 2: Create a JavaScript file importing the plugin
Create a new JavaScript file where you will define your custom plugin. In this guide, we'll call it `filament-chart-js-plugins.js`. Import the plugin, and add it to the `window.filamentChartJsPlugins` array:
```javascript
import ChartDataLabels from 'chartjs-plugin-datalabels'
window.filamentChartJsPlugins ??= []
window.filamentChartJsPlugins.push(ChartDataLabels)
```
This is equivalent to including the plugins "inline" via `new Chart(..., { plugins: [...] })` when instantiating a Chart.js chart.
It's important to initialise the array if it has not been already, before pushing onto it. This ensures that mutliple JavaScript files (especially those from Filament plugins) that register Chart.js plugins do not overwrite each other, regardless of the order they are booted in.
You can push as many plugins to the array as you would like to install, you do not need a separate file to import each plugin.
Additionally, you can also register any "global plugins" which will use `Chart.register([...])` in the `window.filamentChartJsGlobalPlugins` array:
```javascript
import ChartDataLabels from 'chartjs-plugin-datalabels'
window.filamentChartJsGlobalPlugins ??= []
window.filamentChartJsGlobalPlugins.push(ChartDataLabels)
```
### Step 3: Compile the JavaScript file with Vite
Now, you need to build the JavaScript file with Vite, or your bundler of choice. Include the file in your Vite configuration (usually `vite.config.js`). For example:
```javascript
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
'resources/css/filament/admin/theme.css',
'resources/js/filament-chart-js-plugins.js', // Include the new file in the `input` array so it is built
],
}),
],
});
```
Build the file with `npm run build`.
### Step 4: Register the JavaScript file in Filament
Filament needs to know to include this JavaScript file when rendering chart widgets. You can do this in the `boot()` method of a service provider like `AppServiceProvider`:
```php
use Filament\Support\Assets\Js;
use Filament\Support\Facades\FilamentAsset;
use Illuminate\Support\Facades\Vite;
FilamentAsset::register([
Js::make('chart-js-plugins', Vite::asset('resources/js/filament-chart-js-plugins.js'))->module(),
]);
```
You can find out more about [asset registration](../advanced/assets), and even [register assets for a specific panel](../panel-configuration#registering-assets-for-a-panel).
@@ -0,0 +1,63 @@
.fi-wi-chart {
& .fi-wi-chart-canvas-ctn {
@apply mx-auto;
&:not(.fi-wi-chart-canvas-ctn-no-aspect-ratio) {
@apply aspect-square;
}
}
@supports (container-type: inline-size) {
& .fi-section-content {
@apply @container;
}
& .fi-wi-chart-canvas-ctn:not(.fi-wi-chart-canvas-ctn-no-aspect-ratio) {
@apply @sm:aspect-[1.5];
}
}
@supports not (container-type: inline-size) {
& .fi-wi-chart-canvas-ctn:not(.fi-wi-chart-canvas-ctn-no-aspect-ratio) {
@apply sm:aspect-[1.5];
}
}
& .fi-wi-chart-filter {
&.fi-input-wrp {
@apply w-max sm:-my-2;
}
&.fi-dropdown {
& .fi-wi-chart-filter-content {
@apply p-6;
}
}
}
& .fi-color {
& .fi-wi-chart-bg-color {
@apply text-color-50 dark:text-color-400/10;
}
& .fi-wi-chart-border-color {
@apply text-color-500 dark:text-color-400;
}
}
& .fi-wi-chart-bg-color {
@apply text-gray-100 dark:text-gray-800;
}
& .fi-wi-chart-border-color {
@apply text-gray-400;
}
& .fi-wi-chart-grid-color {
@apply text-gray-200 dark:text-gray-800;
}
& .fi-wi-chart-text-color {
@apply text-gray-500 dark:text-gray-400;
}
}
@@ -0,0 +1,3 @@
@import './chart-widget.css' layer(components);
@import './stats-overview-widget.css' layer(components);
@import './widget.css' layer(components);
@@ -0,0 +1,61 @@
.fi-wi-stats-overview-stat {
@apply relative block h-full rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10;
& .fi-icon {
@apply shrink-0 text-gray-400 dark:text-gray-500;
}
& .fi-wi-stats-overview-stat-content {
@apply grid gap-y-2;
}
& .fi-wi-stats-overview-stat-label-ctn {
@apply flex items-center gap-x-2;
}
& .fi-wi-stats-overview-stat-label {
@apply text-sm font-medium text-gray-500 dark:text-gray-400;
}
& .fi-wi-stats-overview-stat-value {
@apply text-3xl font-semibold tracking-tight text-gray-950 dark:text-white;
}
& .fi-wi-stats-overview-stat-description {
@apply flex items-center gap-x-1 text-sm text-gray-500 dark:text-gray-400;
&.fi-color {
@apply text-(--text) dark:text-(--dark-text);
& .fi-icon {
@apply text-color-500;
}
}
}
& .fi-wi-stats-overview-stat-chart {
@apply absolute inset-x-0 bottom-0 overflow-hidden rounded-b-xl;
& > canvas {
@apply h-6;
}
& .fi-wi-stats-overview-stat-chart-bg-color {
@apply text-gray-100 dark:text-gray-800;
}
& .fi-wi-stats-overview-stat-chart-border-color {
@apply text-gray-400;
}
&.fi-color {
& .fi-wi-stats-overview-stat-chart-bg-color {
@apply text-color-50 dark:text-color-400/10;
}
& .fi-wi-stats-overview-stat-chart-border-color {
@apply text-color-500 dark:text-color-400;
}
}
}
}
@@ -0,0 +1,3 @@
.fi-wi {
@apply gap-6;
}
@@ -0,0 +1,169 @@
import Chart from 'chart.js/auto'
import 'chartjs-adapter-luxon'
if (
window.filamentChartJsGlobalPlugins &&
Array.isArray(window.filamentChartJsGlobalPlugins) &&
window.filamentChartJsGlobalPlugins.length > 0
) {
Chart.register(...window.filamentChartJsGlobalPlugins)
}
export default function chart({ cachedData, options, type }) {
return {
userPointBackgroundColor: options?.pointBackgroundColor,
userXGridColor: options?.scales?.x?.grid?.color,
userYGridColor: options?.scales?.y?.grid?.color,
userRadialGridColor: options?.scales?.r?.grid?.color,
userRadialTicksColor: options?.scales?.r?.ticks?.color,
init() {
this.initChart()
this.$wire.$on('updateChartData', ({ data }) => {
chart = this.getChart()
chart.data = data
chart.update('resize')
})
Alpine.effect(() => {
Alpine.store('theme')
this.$nextTick(() => {
if (!this.getChart()) {
return
}
this.getChart().destroy()
this.initChart()
})
})
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
if (Alpine.store('theme') !== 'system') {
return
}
this.$nextTick(() => {
this.getChart().destroy()
this.initChart()
})
})
this.resizeHandler = Alpine.debounce(() => {
this.getChart().destroy()
this.initChart()
}, 250)
window.addEventListener('resize', this.resizeHandler)
this.resizeObserver = new ResizeObserver(() => this.resizeHandler())
this.resizeObserver.observe(this.$el)
},
initChart(data = null) {
if (
!this.$refs.canvas ||
!this.$refs.backgroundColorElement ||
!this.$refs.borderColorElement ||
!this.$refs.textColorElement ||
!this.$refs.gridColorElement
) {
return
}
Chart.defaults.animation.duration = 0
Chart.defaults.backgroundColor = getComputedStyle(
this.$refs.backgroundColorElement,
).color
const borderColor = getComputedStyle(
this.$refs.borderColorElement,
).color
Chart.defaults.borderColor = borderColor
Chart.defaults.color = getComputedStyle(
this.$refs.textColorElement,
).color
Chart.defaults.font.family = getComputedStyle(this.$el).fontFamily
Chart.defaults.plugins.legend.labels.boxWidth = 12
Chart.defaults.plugins.legend.position = 'bottom'
const gridColor = getComputedStyle(
this.$refs.gridColorElement,
).color
options ??= {}
options.borderWidth ??= 2
options.maintainAspectRatio ??= false
options.pointBackgroundColor =
this.userPointBackgroundColor ?? borderColor
options.pointHitRadius ??= 4
options.pointRadius ??= 2
options.scales ??= {}
options.scales.x ??= {}
options.scales.x.border ??= {}
options.scales.x.border.display ??= false
options.scales.x.grid ??= {}
options.scales.x.grid.color = this.userXGridColor ?? gridColor
options.scales.x.grid.display ??= false
options.scales.y ??= {}
options.scales.y.border ??= {}
options.scales.y.border.display ??= false
options.scales.y.grid ??= {}
options.scales.y.grid.color = this.userYGridColor ?? gridColor
if (['doughnut', 'pie', 'polarArea'].includes(type)) {
options.scales.x.display ??= false
options.scales.y.display ??= false
options.scales.y.grid.display ??= false
}
if (type === 'polarArea') {
const textColor = getComputedStyle(
this.$refs.textColorElement,
).color
options.scales.r ??= {}
options.scales.r.grid ??= {}
options.scales.r.grid.color =
this.userRadialGridColor ?? gridColor
options.scales.r.ticks ??= {}
options.scales.r.ticks.color =
this.userRadialTicksColor ?? textColor
options.scales.r.ticks.backdropColor ??= 'transparent'
}
return new Chart(this.$refs.canvas, {
type,
data: data ?? cachedData,
options,
plugins: window.filamentChartJsPlugins ?? [],
})
},
getChart() {
if (!this.$refs.canvas) {
return null
}
return Chart.getChart(this.$refs.canvas)
},
destroy() {
window.removeEventListener('resize', this.resizeHandler)
if (this.resizeObserver) {
this.resizeObserver.disconnect()
}
this.getChart()?.destroy()
},
}
}
@@ -0,0 +1,109 @@
import Chart from 'chart.js/auto'
export default function statsOverviewStatChart({
dataChecksum,
labels,
values,
}) {
return {
dataChecksum,
init() {
Alpine.effect(() => {
Alpine.store('theme')
const chart = this.getChart()
if (chart) {
chart.destroy()
}
this.initChart()
})
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
if (Alpine.store('theme') !== 'system') {
return
}
this.$nextTick(() => {
const chart = this.getChart()
if (chart) {
chart.destroy()
}
this.initChart()
})
})
},
initChart() {
if (
!this.$refs.canvas ||
!this.$refs.backgroundColorElement ||
!this.$refs.borderColorElement
) {
return
}
return new Chart(this.$refs.canvas, {
type: 'line',
data: {
labels: labels,
datasets: [
{
data: values,
borderWidth: 2,
fill: 'start',
tension: 0.5,
backgroundColor: getComputedStyle(
this.$refs.backgroundColorElement,
).color,
borderColor: getComputedStyle(
this.$refs.borderColorElement,
).color,
},
],
},
options: {
animation: {
duration: 0,
},
elements: {
point: {
radius: 0,
},
},
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
display: false,
},
y: {
display: false,
},
},
tooltips: {
enabled: false,
},
},
})
},
getChart() {
if (!this.$refs.canvas) {
return null
}
return Chart.getChart(this.$refs.canvas)
},
}
}
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'ማጣርያ',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'تصفية',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'ফিল্টার করুন',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'پاڵاوتن',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filtr',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filtern',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filter',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filtrar',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filtrer',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Szűrés',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filter',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filtra',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'フィルター',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => '필터',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filter',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filter',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filter',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filtruj',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filtrar',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filtro',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Фильтр',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filter',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Филтер',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filter',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filter',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Filtre',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Фільтр',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'فلٹر کریں',
],
],
];
@@ -0,0 +1,13 @@
<?php
return [
'actions' => [
'filter' => [
'label' => 'Bộ lọc',
],
],
];
@@ -0,0 +1,110 @@
@php
use Filament\Widgets\View\Components\ChartWidgetComponent;
use Illuminate\View\ComponentAttributeBag;
$color = $this->getColor();
$heading = $this->getHeading();
$description = $this->getDescription();
$filters = $this->getFilters();
$isCollapsible = $this->isCollapsible();
$type = $this->getType();
@endphp
<x-filament-widgets::widget class="fi-wi-chart">
<x-filament::section
:description="$description"
:heading="$heading"
:collapsible="$isCollapsible"
>
@if ($filters || method_exists($this, 'getFiltersSchema'))
<x-slot name="afterHeader">
@if ($filters)
<x-filament::input.wrapper
inline-prefix
wire:target="filter"
class="fi-wi-chart-filter"
>
<x-filament::input.select
inline-prefix
wire:model.live="filter"
>
@foreach ($filters as $value => $label)
<option value="{{ $value }}">
{{ $label }}
</option>
@endforeach
</x-filament::input.select>
</x-filament::input.wrapper>
@endif
@if (method_exists($this, 'getFiltersSchema'))
<x-filament::dropdown
placement="bottom-end"
shift
width="xs"
class="fi-wi-chart-filter"
>
<x-slot name="trigger">
{{ $this->getFiltersTriggerAction() }}
</x-slot>
<div class="fi-wi-chart-filter-content">
{{ $this->getFiltersSchema() }}
</div>
</x-filament::dropdown>
@endif
</x-slot>
@endif
<div
@if ($pollingInterval = $this->getPollingInterval())
wire:poll.{{ $pollingInterval }}="updateChartData"
@endif
>
<div
x-load
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('chart', 'filament/widgets') }}"
wire:ignore
data-chart-type="{{ $type }}"
x-data="chart({
cachedData: @js($this->getCachedData()),
options: @js($this->getOptions()),
type: @js($type),
})"
{{
(new ComponentAttributeBag)
->color(ChartWidgetComponent::class, $color)
->class([
'fi-wi-chart-canvas-ctn',
'fi-wi-chart-canvas-ctn-no-aspect-ratio' => filled($maxHeight = $this->getMaxHeight()),
])
->style([
'max-height: ' . $maxHeight => filled($maxHeight),
])
}}
>
<canvas x-ref="canvas"></canvas>
<span
x-ref="backgroundColorElement"
class="fi-wi-chart-bg-color"
></span>
<span
x-ref="borderColorElement"
class="fi-wi-chart-border-color"
></span>
<span
x-ref="gridColorElement"
class="fi-wi-chart-grid-color"
></span>
<span
x-ref="textColorElement"
class="fi-wi-chart-text-color"
></span>
</div>
</div>
</x-filament::section>
</x-filament-widgets::widget>
@@ -0,0 +1,5 @@
<div
{{ $attributes->gridColumn($this->getColumnSpan(), $this->getColumnStart())->class(['fi-wi-widget']) }}
>
{{ $slot }}
</div>
@@ -0,0 +1,39 @@
{{-- @deprecated Use a schema to render widgets. --}}
@props([
'columns' => [
'lg' => 2,
],
'data' => [],
'widgets' => [],
])
@php
if (is_array($columns)) {
$columns['lg'] ??= ($columns ? (is_array($columns) ? null : $columns) : 2);
}
@endphp
<div {{ $attributes->grid($columns)->class(['fi-wi']) }}>
@php
$normalizeWidgetClass = function (string | Filament\Widgets\WidgetConfiguration $widget): string {
if ($widget instanceof \Filament\Widgets\WidgetConfiguration) {
return $widget->widget;
}
return $widget;
};
@endphp
@foreach ($widgets as $widgetKey => $widget)
@php
$widgetClass = $normalizeWidgetClass($widget);
@endphp
@livewire(
$widgetClass,
[...(($widget instanceof \Filament\Widgets\WidgetConfiguration) ? [...$widget->widget::getDefaultProperties(), ...$widget->getProperties()] : $widget::getDefaultProperties()), ...$data],
key("{$widgetClass}-{$widgetKey}"),
)
@endforeach
</div>
@@ -0,0 +1,23 @@
@php
$columns = $this->getColumns();
$pollingInterval = $this->getPollingInterval();
$heading = $this->getHeading();
$description = $this->getDescription();
$hasHeading = filled($heading);
$hasDescription = filled($description);
@endphp
<x-filament-widgets::widget
:attributes="
(new \Illuminate\View\ComponentAttributeBag)
->merge([
'wire:poll.' . $pollingInterval => $pollingInterval ? true : null,
], escape: false)
->class([
'fi-wi-stats-overview',
])
"
>
{{ $this->content }}
</x-filament-widgets::widget>
@@ -0,0 +1,86 @@
@php
use Filament\Support\Enums\IconPosition;
use Filament\Widgets\View\Components\StatsOverviewWidgetComponent\StatComponent\DescriptionComponent;
use Filament\Widgets\View\Components\StatsOverviewWidgetComponent\StatComponent\StatsOverviewWidgetStatChartComponent;
use Illuminate\View\ComponentAttributeBag;
$chartColor = $getChartColor() ?? 'gray';
$descriptionColor = $getDescriptionColor() ?? 'gray';
$descriptionIcon = $getDescriptionIcon();
$descriptionIconPosition = $getDescriptionIconPosition();
$url = $getUrl();
$tag = $url ? 'a' : 'div';
$chartDataChecksum = $generateChartDataChecksum();
@endphp
<{!! $tag !!}
@if ($url)
{{ \Filament\Support\generate_href_html($url, $shouldOpenUrlInNewTab()) }}
@endif
{{
$getExtraAttributeBag()
->class([
'fi-wi-stats-overview-stat',
])
}}
>
<div class="fi-wi-stats-overview-stat-content">
<div class="fi-wi-stats-overview-stat-label-ctn">
{{ \Filament\Support\generate_icon_html($getIcon()) }}
<span class="fi-wi-stats-overview-stat-label">
{{ $getLabel() }}
</span>
</div>
<div class="fi-wi-stats-overview-stat-value">
{{ $getValue() }}
</div>
@if ($description = $getDescription())
<div
{{ (new ComponentAttributeBag)->color(DescriptionComponent::class, $descriptionColor)->class(['fi-wi-stats-overview-stat-description']) }}
>
@if ($descriptionIcon && in_array($descriptionIconPosition, [IconPosition::Before, 'before']))
{{ \Filament\Support\generate_icon_html($descriptionIcon, attributes: (new \Illuminate\View\ComponentAttributeBag)) }}
@endif
<span>
{{ $description }}
</span>
@if ($descriptionIcon && in_array($descriptionIconPosition, [IconPosition::After, 'after']))
{{ \Filament\Support\generate_icon_html($descriptionIcon, attributes: (new \Illuminate\View\ComponentAttributeBag)) }}
@endif
</div>
@endif
</div>
@if ($chart = $getChart())
{{-- An empty function to initialize the Alpine component with until it's loaded with `x-load`. This removes the need for `x-ignore`, allowing the chart to be updated via Livewire polling. --}}
<div x-data="{ statsOverviewStatChart() {} }">
<div
x-load
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('stats-overview/stat/chart', 'filament/widgets') }}"
x-data="statsOverviewStatChart({
dataChecksum: @js($chartDataChecksum),
labels: @js(array_keys($chart)),
values: @js(array_values($chart)),
})"
{{ (new ComponentAttributeBag)->color(StatsOverviewWidgetStatChartComponent::class, $chartColor)->class(['fi-wi-stats-overview-stat-chart']) }}
>
<canvas x-ref="canvas"></canvas>
<span
x-ref="backgroundColorElement"
class="fi-wi-stats-overview-stat-chart-bg-color"
></span>
<span
x-ref="borderColorElement"
class="fi-wi-stats-overview-stat-chart-border-color"
></span>
</div>
</div>
@endif
</{!! $tag !!}>
@@ -0,0 +1,7 @@
<x-filament-widgets::widget class="fi-wi-table">
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\Widgets\View\WidgetsRenderHook::TABLE_WIDGET_START, scopes: static::class) }}
{{ $this->table }}
{{ \Filament\Support\Facades\FilamentView::renderHook(\Filament\Widgets\View\WidgetsRenderHook::TABLE_WIDGET_END, scopes: static::class) }}
</x-filament-widgets::widget>
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class BarChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'bar';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class BubbleChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'bubble';
}
}
@@ -0,0 +1,134 @@
<?php
namespace Filament\Widgets;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Support\RawJs;
use Illuminate\Contracts\Support\Htmlable;
use Livewire\Attributes\Locked;
abstract class ChartWidget extends Widget implements HasSchemas
{
use Concerns\CanPoll;
use InteractsWithSchemas;
/**
* @var array<string, mixed> | null
*/
protected ?array $cachedData = null;
#[Locked]
public ?string $dataChecksum = null;
public ?string $filter = null;
protected string $color = 'primary';
protected ?string $heading = null;
protected ?string $description = null;
protected ?string $maxHeight = null;
/**
* @var array<string, mixed> | null
*/
protected ?array $options = null;
protected bool $isCollapsible = false;
/**
* @var view-string
*/
protected string $view = 'filament-widgets::chart-widget';
public function mount(): void
{
if (method_exists($this, 'getFiltersSchema')) {
$this->getFiltersSchema()->fill();
}
$this->dataChecksum = $this->generateDataChecksum();
}
abstract protected function getType(): string;
protected function generateDataChecksum(): string
{
return md5(json_encode($this->getCachedData()));
}
/**
* @return array<string, mixed>
*/
protected function getCachedData(): array
{
return $this->cachedData ??= $this->getData();
}
/**
* @return array<string, mixed>
*/
protected function getData(): array
{
return [];
}
/**
* @return array<scalar, scalar> | null
*/
protected function getFilters(): ?array
{
return null;
}
public function getHeading(): string | Htmlable | null
{
return $this->heading;
}
public function getDescription(): string | Htmlable | null
{
return $this->description;
}
protected function getMaxHeight(): ?string
{
return $this->maxHeight;
}
/**
* @return array<string, mixed> | RawJs | null
*/
protected function getOptions(): array | RawJs | null
{
return $this->options;
}
public function updateChartData(): void
{
$newDataChecksum = $this->generateDataChecksum();
if ($newDataChecksum !== $this->dataChecksum) {
$this->dataChecksum = $newDataChecksum;
$this->dispatch('updateChartData', data: $this->getCachedData());
}
}
public function rendering(): void
{
$this->updateChartData();
}
public function getColor(): string
{
return $this->color;
}
public function isCollapsible(): bool
{
return $this->isCollapsible;
}
}
@@ -0,0 +1,40 @@
<?php
namespace Filament\Widgets\ChartWidget\Concerns;
use Filament\Actions\Action;
use Filament\Schemas\Schema;
use Filament\Support\Facades\FilamentIcon;
use Filament\Support\Icons\Heroicon;
use Filament\Widgets\View\WidgetsIconAlias;
trait HasFiltersSchema /** @phpstan-ignore trait.unused */
{
public ?array $filters = [];
public function filtersSchema(Schema $schema): Schema
{
return $schema;
}
public function getFiltersTriggerAction(): Action
{
return Action::make('filter')
->label(__('filament-widgets::chart.actions.filter.label'))
->iconButton()
->icon(FilamentIcon::resolve(WidgetsIconAlias::CHART_WIDGET_FILTER) ?? Heroicon::Funnel)
->color('gray')
->livewireClickHandlerEnabled(false);
}
public function getFiltersSchema(): Schema
{
if ((! $this->isCachingSchemas) && $this->hasCachedSchema('filtersSchema')) {
return $this->getSchema('filtersSchema');
}
return $this->filtersSchema($this->makeSchema()
->statePath('filters')
->live());
}
}
@@ -0,0 +1,114 @@
<?php
namespace Filament\Widgets\Commands\FileGenerators;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Filament\Widgets\ChartWidget;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Literal;
use Nette\PhpGenerator\Method;
use Nette\PhpGenerator\Property;
class ChartWidgetClassGenerator extends ClassGenerator
{
final public function __construct(
protected string $fqn,
protected string $type,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
$extends = $this->getExtends();
$extendsBasename = class_basename($extends);
return [
...(($extendsBasename === class_basename($this->getFqn())) ? [$extends => "Base{$extendsBasename}"] : [$extends]),
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return ChartWidget::class;
}
protected function addPropertiesToClass(ClassType $class): void
{
$this->addHeadingPropertyToClass($class);
}
protected function addMethodsToClass(ClassType $class): void
{
$this->addGetDataMethodToClass($class);
$this->addGetTypeMethodToClass($class);
}
protected function addHeadingPropertyToClass(ClassType $class): void
{
$property = $class->addProperty(
'heading',
(string) str($this->getBasename())
->classBasename()
->kebab()
->replace('-', ' ')
->ucwords(),
)
->setProtected()
->setType('?string');
$this->configureHeadingProperty($property);
}
protected function configureHeadingProperty(Property $property): void {}
protected function addGetDataMethodToClass(ClassType $class): void
{
$method = $class->addMethod('getData')
->setProtected()
->setReturnType('array')
->setBody(<<<'PHP'
return [
//
];
PHP);
$this->configureGetDataMethod($method);
}
protected function configureGetDataMethod(Method $method): void {}
protected function addGetTypeMethodToClass(ClassType $class): void
{
$method = $class->addMethod('getType')
->setProtected()
->setReturnType('string')
->setBody(new Literal(<<<'PHP'
return ?;
PHP, [$this->getType()]));
$this->configureGetTypeMethod($method);
}
protected function configureGetTypeMethod(Method $method): void {}
public function getFqn(): string
{
return $this->fqn;
}
public function getType(): string
{
return $this->type;
}
}
@@ -0,0 +1,61 @@
<?php
namespace Filament\Widgets\Commands\FileGenerators;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Filament\Support\Commands\FileGenerators\Concerns\CanGenerateViewProperty;
use Filament\Widgets\Widget;
use Nette\PhpGenerator\ClassType;
class CustomWidgetClassGenerator extends ClassGenerator
{
use CanGenerateViewProperty;
final public function __construct(
protected string $fqn,
protected string $view,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
$extends = $this->getExtends();
$extendsBasename = class_basename($extends);
return [
...(($extendsBasename === class_basename($this->getFqn())) ? [$extends => "Base{$extendsBasename}"] : [$extends]),
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return Widget::class;
}
protected function addPropertiesToClass(ClassType $class): void
{
$this->addViewPropertyToClass($class);
}
public function getFqn(): string
{
return $this->fqn;
}
public function getView(): string
{
return $this->view;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Filament\Widgets\Commands\FileGenerators;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Method;
class StatsOverviewWidgetClassGenerator extends ClassGenerator
{
final public function __construct(
protected string $fqn,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
$extends = $this->getExtends();
$extendsBasename = class_basename($extends);
return [
...(($extendsBasename === class_basename($this->getFqn())) ? [$extends => "Base{$extendsBasename}"] : [$extends]),
Stat::class,
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return StatsOverviewWidget::class;
}
protected function addMethodsToClass(ClassType $class): void
{
$this->addGetStatsMethodToClass($class);
}
protected function addGetStatsMethodToClass(ClassType $class): void
{
$method = $class->addMethod('getStats')
->setProtected()
->setReturnType('array')
->setBody(<<<'PHP'
return [
//
];
PHP);
$this->configureGetStatsMethod($method);
}
protected function configureGetStatsMethod(Method $method): void {}
public function getFqn(): string
{
return $this->fqn;
}
}
@@ -0,0 +1,97 @@
<?php
namespace Filament\Widgets\Commands\FileGenerators;
use Filament\Support\Commands\Concerns\CanReadModelSchemas;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Filament\Tables\Commands\FileGenerators\Concerns\CanGenerateModelTables;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Method;
class TableWidgetClassGenerator extends ClassGenerator
{
use CanGenerateModelTables;
use CanReadModelSchemas;
/**
* @param class-string<Model> $modelFqn
*/
final public function __construct(
protected string $fqn,
protected string $modelFqn,
protected bool $isGenerated,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
$extends = $this->getExtends();
$extendsBasename = class_basename($extends);
return [
...(($extendsBasename === class_basename($this->getFqn())) ? [$extends => "Base{$extendsBasename}"] : [$extends]),
Table::class,
...($this->hasPartialImports() ? ['Filament\Tables'] : []),
Builder::class,
$this->getModelFqn(),
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return TableWidget::class;
}
protected function addMethodsToClass(ClassType $class): void
{
$this->addTableMethodToClass($class);
}
protected function addTableMethodToClass(ClassType $class): void
{
$method = $class->addMethod('table')
->setPublic()
->setReturnType(Table::class)
->setBody($this->generateTableMethodBody($this->getModelFqn()));
$method->addParameter('table')
->setType(Table::class);
$this->configureTableMethod($method);
}
protected function configureTableMethod(Method $method): void {}
public function getFqn(): string
{
return $this->fqn;
}
/**
* @return class-string<Model>
*/
public function getModelFqn(): string
{
return $this->modelFqn;
}
public function isGenerated(): bool
{
return $this->isGenerated;
}
}
@@ -0,0 +1,524 @@
<?php
namespace Filament\Widgets\Commands;
use Filament\Support\Commands\Concerns\CanAskForLivewireComponentLocation;
use Filament\Support\Commands\Concerns\CanAskForResource;
use Filament\Support\Commands\Concerns\CanAskForViewLocation;
use Filament\Support\Commands\Concerns\CanManipulateFiles;
use Filament\Support\Commands\Concerns\HasCluster;
use Filament\Support\Commands\Concerns\HasPanel;
use Filament\Support\Commands\Concerns\HasResourcesLocation;
use Filament\Support\Commands\Exceptions\FailureCommandOutput;
use Filament\Support\Commands\FileGenerators\Concerns\CanCheckFileGenerationFlags;
use Filament\Support\Facades\FilamentCli;
use Filament\Widgets\ChartWidget;
use Filament\Widgets\Commands\FileGenerators\ChartWidgetClassGenerator;
use Filament\Widgets\Commands\FileGenerators\CustomWidgetClassGenerator;
use Filament\Widgets\Commands\FileGenerators\StatsOverviewWidgetClassGenerator;
use Filament\Widgets\Commands\FileGenerators\TableWidgetClassGenerator;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\TableWidget;
use Filament\Widgets\Widget;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use ReflectionClass;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function Filament\Support\discover_app_classes;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\search;
use function Laravel\Prompts\select;
use function Laravel\Prompts\suggest;
use function Laravel\Prompts\text;
#[AsCommand(name: 'make:filament-widget', aliases: [
'filament:make-widget',
'filament:widget',
])]
class MakeWidgetCommand extends Command
{
use CanAskForLivewireComponentLocation;
use CanAskForResource;
use CanAskForViewLocation;
use CanCheckFileGenerationFlags;
use CanManipulateFiles;
use HasCluster;
use HasPanel;
use HasResourcesLocation;
protected $description = 'Create a new Filament widget class';
protected $name = 'make:filament-widget';
/**
* @var array<string>
*/
protected $aliases = [
'filament:make-widget',
'filament:widget',
];
/**
* @var class-string
*/
protected string $fqn;
protected string $fqnEnd;
protected ?string $view = null;
protected ?string $viewPath = null;
protected bool $hasResource;
/**
* @var ?class-string
*/
protected ?string $resourceFqn = null;
/**
* @var class-string<Widget> | null
*/
protected ?string $type = null;
protected string $widgetsNamespace;
protected string $widgetsDirectory;
/**
* @return array<InputArgument>
*/
protected function getArguments(): array
{
return [
new InputArgument(
name: 'name',
mode: InputArgument::OPTIONAL,
description: 'The name of the widget to generate, optionally prefixed with directories',
),
];
}
/**
* @return array<InputOption>
*/
protected function getOptions(): array
{
return [
new InputOption(
name: 'chart',
shortcut: 'C',
mode: InputOption::VALUE_NONE,
description: 'Create a chart widget',
),
new InputOption(
name: 'cluster',
shortcut: null,
mode: InputOption::VALUE_OPTIONAL,
description: 'The cluster that the resource belongs to',
),
new InputOption(
name: 'panel',
shortcut: null,
mode: InputOption::VALUE_REQUIRED,
description: 'The panel to create the widget in',
),
new InputOption(
name: 'resource',
shortcut: 'R',
mode: InputOption::VALUE_OPTIONAL,
description: 'The resource to create the widget in',
),
new InputOption(
name: 'resource-namespace',
shortcut: null,
mode: InputOption::VALUE_OPTIONAL,
description: 'The namespace of the resource class, such as [' . app()->getNamespace() . 'Filament\\Resources]',
),
new InputOption(
name: 'stats-overview',
shortcut: 'S',
mode: InputOption::VALUE_NONE,
description: 'Create a stats overview widget',
),
new InputOption(
name: 'table',
shortcut: 'T',
mode: InputOption::VALUE_NONE,
description: 'Create a table widget',
),
new InputOption(
name: 'force',
shortcut: 'F',
mode: InputOption::VALUE_NONE,
description: 'Overwrite the contents of the files if they already exist',
),
];
}
public function handle(): int
{
try {
$this->configureFqnEnd();
$this->configureType();
$this->configurePanel(
question: 'Which panel would you like to create this widget in?',
initialQuestion: 'Would you like to create this widget in a panel?',
);
$this->configureHasResource();
$this->configureCluster();
$this->configureResource();
$this->configureWidgetsLocation();
$this->configureLocation();
$this->createCustomWidget();
$this->createChartWidget();
$this->createStatsOverviewWidget();
$this->createTableWidget();
$this->createView();
} catch (FailureCommandOutput) {
return static::FAILURE;
}
$this->components->info("Filament widget [{$this->fqn}] created successfully.");
if (filled($this->resourceFqn)) {
$this->components->info("Make sure to register the widget in [{$this->resourceFqn}::getWidgets()], and add it to a page in the resource.");
} elseif ($this->panel && empty($this->panel->getWidgetNamespaces())) {
$this->components->info('Make sure to register the widget with [widgets()] or discover it with [discoverWidgets()] in the panel service provider.');
}
return static::SUCCESS;
}
protected function configureFqnEnd(): void
{
$this->fqnEnd = (string) str($this->argument('name') ?? text(
label: 'What is the widget name?',
placeholder: 'BlogPostsChart',
required: true,
))
->trim('/')
->trim('\\')
->trim(' ')
->studly()
->replace('/', '\\');
}
protected function configureType(): void
{
$this->type = match (true) {
boolval($this->option('chart')) => ChartWidget::class,
boolval($this->option('stats-overview')) => StatsOverviewWidget::class,
boolval($this->option('table')) => TableWidget::class,
default => null,
} ?? select(
label: 'Which type of widget would you like to create?',
options: [
Widget::class => 'Custom',
ChartWidget::class => 'Chart',
StatsOverviewWidget::class => 'Stats overview',
TableWidget::class => 'Table',
],
);
}
protected function configureHasResource(): void
{
if (! $this->panel) {
$this->hasResource = false;
return;
}
$this->hasResource = $this->option('resource') || confirm(
label: 'Would you like to create this widget in a resource?',
default: false,
);
}
protected function configureCluster(): void
{
if (! $this->hasResource) {
return;
}
$this->configureClusterFqn(
initialQuestion: 'Is the resource in a cluster?',
question: 'Which cluster is the resource in?',
);
if (blank($this->clusterFqn)) {
return;
}
$this->configureClusterResourcesLocation();
}
protected function configureResource(): void
{
if (! $this->hasResource) {
return;
}
$this->configureResourcesLocation(question: 'Which namespace would you like to search for resources in?');
$this->resourceFqn = $this->askForResource(
question: 'Which resource would you like to create this widget in?',
initialResource: $this->option('resource'),
);
$pluralResourceBasenameBeforeResource = (string) str($this->resourceFqn)
->classBasename()
->beforeLast('Resource')
->plural();
$resourceNamespacePartBeforeBasename = (string) str($this->resourceFqn)
->beforeLast('\\')
->classBasename();
if ($pluralResourceBasenameBeforeResource === $resourceNamespacePartBeforeBasename) {
$this->widgetsNamespace = (string) str($this->resourceFqn)
->beforeLast('\\')
->append('\\Widgets');
$this->widgetsDirectory = (string) str((new ReflectionClass($this->resourceFqn))->getFileName())
->beforeLast(DIRECTORY_SEPARATOR)
->append('/Widgets');
return;
}
$this->widgetsNamespace = "{$this->resourceFqn}\\Widgets";
$this->widgetsDirectory = (string) str((new ReflectionClass($this->resourceFqn))->getFileName())
->beforeLast('.')
->append('/Widgets');
}
protected function configureWidgetsLocation(): void
{
if (filled($this->resourceFqn)) {
return;
}
if (! $this->panel) {
[
$this->widgetsNamespace,
$this->widgetsDirectory,
] = $this->askForLivewireComponentLocation(
question: 'Where would you like to create the widget?',
);
return;
}
$directories = $this->panel->getWidgetDirectories();
$namespaces = $this->panel->getWidgetNamespaces();
foreach ($directories as $index => $directory) {
if (str($directory)->startsWith(base_path('vendor'))) {
unset($directories[$index]);
unset($namespaces[$index]);
}
}
if (count($namespaces) < 2) {
$this->widgetsNamespace = (Arr::first($namespaces) ?? app()->getNamespace() . 'Filament\\Widgets');
$this->widgetsDirectory = (Arr::first($directories) ?? app_path('Filament/Widgets/'));
return;
}
$keyedNamespaces = array_combine(
$namespaces,
$namespaces,
);
$this->widgetsNamespace = search(
label: 'Which namespace would you like to create this widget in?',
options: function (?string $search) use ($keyedNamespaces): array {
if (blank($search)) {
return $keyedNamespaces;
}
$search = str($search)->trim()->replace(['\\', '/'], '');
return array_filter($keyedNamespaces, fn (string $namespace): bool => str($namespace)->replace(['\\', '/'], '')->contains($search, ignoreCase: true));
},
);
$this->widgetsDirectory = $directories[array_search($this->widgetsNamespace, $namespaces)];
}
protected function configureLocation(): void
{
$this->fqn = $this->widgetsNamespace . '\\' . $this->fqnEnd;
if ($this->type === Widget::class) {
$componentLocations = FilamentCli::getLivewireComponentLocations();
$matchingComponentLocationNamespaces = collect($componentLocations)
->keys()
->filter(fn (string $namespace): bool => str($this->fqn)->startsWith($namespace));
[
$this->view,
$this->viewPath,
] = $this->askForViewLocation(
view: str($this->fqn)
->whenContains(
'Filament\\',
fn (Stringable $fqn) => $fqn->after('Filament\\')->prepend('Filament\\'),
fn (Stringable $fqn) => $fqn
->afterLast('\\Livewire\\')
->prepend('Livewire\\'),
)
->replace('\\', '/')
->explode('/')
->map(Str::kebab(...))
->implode('.'),
question: 'Where would you like to create the Blade view for the widget?',
defaultNamespace: (count($matchingComponentLocationNamespaces) === 1)
? $componentLocations[Arr::first($matchingComponentLocationNamespaces)]['viewNamespace'] ?? null
: null,
);
}
}
protected function createCustomWidget(): void
{
if ($this->type !== Widget::class) {
return;
}
$path = (string) str("{$this->widgetsDirectory}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
if (! $this->option('force') && $this->checkForCollision($path)) {
throw new FailureCommandOutput;
}
$this->writeFile($path, app(CustomWidgetClassGenerator::class, [
'fqn' => $this->fqn,
'view' => $this->view,
]));
}
protected function createChartWidget(): void
{
if ($this->type !== ChartWidget::class) {
return;
}
$type = select(
label: 'Which type of chart would you like to create?',
options: [
'bar' => 'Bar chart',
'bubble' => 'Bubble chart',
'doughnut' => 'Doughnut chart',
'line' => 'Line chart',
'pie' => 'Pie chart',
'polarArea' => 'Polar area chart',
'radar' => 'Radar chart',
'scatter' => 'Scatter chart',
],
default: 'line',
);
$path = (string) str("{$this->widgetsDirectory}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
if (! $this->option('force') && $this->checkForCollision($path)) {
throw new FailureCommandOutput;
}
$this->writeFile($path, app(ChartWidgetClassGenerator::class, [
'fqn' => $this->fqn,
'type' => $type,
]));
}
protected function createStatsOverviewWidget(): void
{
if ($this->type !== StatsOverviewWidget::class) {
return;
}
$path = (string) str("{$this->widgetsDirectory}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
if (! $this->option('force') && $this->checkForCollision($path)) {
throw new FailureCommandOutput;
}
$this->writeFile($path, app(StatsOverviewWidgetClassGenerator::class, [
'fqn' => $this->fqn,
]));
}
protected function createTableWidget(): void
{
if ($this->type !== TableWidget::class) {
return;
}
$modelFqns = discover_app_classes(parentClass: Model::class);
$modelFqn = suggest(
label: 'What is the model?',
options: function (string $search) use ($modelFqns): array {
$search = str($search)->trim()->replace(['\\', '/'], '');
if (blank($search)) {
return $modelFqns;
}
return array_filter(
$modelFqns,
fn (string $class): bool => str($class)->replace(['\\', '/'], '')->contains($search, ignoreCase: true),
);
},
placeholder: app()->getNamespace() . 'Models\\BlogPost',
);
$isGenerated = confirm(
label: 'Should the table columns be generated from the current database columns?',
default: false,
);
$path = (string) str("{$this->widgetsDirectory}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
if (! $this->option('force') && $this->checkForCollision($path)) {
throw new FailureCommandOutput;
}
$this->writeFile($path, app(TableWidgetClassGenerator::class, [
'fqn' => $this->fqn,
'modelFqn' => $modelFqn ?: Model::class,
'isGenerated' => $isGenerated,
]));
}
protected function createView(): void
{
if (blank($this->view)) {
return;
}
if (! $this->option('force') && $this->checkForCollision($this->viewPath)) {
throw new FailureCommandOutput;
}
$this->copyStubToApp('WidgetView', $this->viewPath);
}
}
@@ -0,0 +1,13 @@
<?php
namespace Filament\Widgets\Concerns;
trait CanPoll
{
protected ?string $pollingInterval = '5s';
protected function getPollingInterval(): ?string
{
return $this->pollingInterval;
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class DoughnutChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'doughnut';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class LineChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'line';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class PieChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'pie';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class PolarAreaChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'polarArea';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class RadarChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'radar';
}
}
@@ -0,0 +1,14 @@
<?php
namespace Filament\Widgets;
/**
* @deprecated Extend `ChartWidget` instead and define the `getType()` method.
*/
class ScatterChartWidget extends ChartWidget
{
protected function getType(): string
{
return 'scatter';
}
}
@@ -0,0 +1,114 @@
<?php
namespace Filament\Widgets;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Schemas\Schema;
use Filament\Widgets\StatsOverviewWidget\Stat;
class StatsOverviewWidget extends Widget implements HasSchemas
{
use Concerns\CanPoll;
use InteractsWithSchemas;
/**
* @var array<Stat> | null
*/
protected ?array $cachedStats = null;
protected int | string | array $columnSpan = 'full';
protected ?string $heading = null;
protected ?string $description = null;
/**
* @var int | array<string, ?int> | null
*/
protected int | array | null $columns = null;
/**
* @var view-string
*/
protected string $view = 'filament-widgets::stats-overview-widget';
public function content(Schema $schema): Schema
{
return $schema
->components([
$this->getSectionContentComponent(),
]);
}
public function getSectionContentComponent(): Component
{
return Section::make()
->heading($this->getHeading())
->description($this->getDescription())
->schema($this->getCachedStats())
->columns($this->getColumns())
->contained(false)
->gridContainer();
}
/**
* @return int | array<string, ?int> | null
*/
protected function getColumns(): int | array | null
{
if ($this->columns) {
return $this->columns;
}
$count = count($this->getCachedStats());
if ($count < 3) {
return ['@xl' => 3, '!@lg' => 3];
}
if (($count % 3) !== 1) {
return ['@xl' => 3, '!@lg' => 3];
}
return ['@xl' => 4, '!@lg' => 4];
}
protected function getDescription(): ?string
{
return $this->description;
}
protected function getHeading(): ?string
{
return $this->heading;
}
/**
* @return array<Stat>
*/
protected function getCachedStats(): array
{
return $this->cachedStats ??= $this->getStats();
}
/**
* @deprecated Use `getStats()` instead.
*
* @return array<Stat>
*/
protected function getCards(): array
{
return [];
}
/**
* @return array<Stat>
*/
protected function getStats(): array
{
return $this->getCards();
}
}
@@ -0,0 +1,182 @@
<?php
namespace Filament\Widgets\StatsOverviewWidget;
use BackedEnum;
use Closure;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Concerns\CanOpenUrl;
use Filament\Schemas\Components\Concerns\HasDescription;
use Filament\Schemas\Components\Concerns\HasLabel;
use Filament\Support\Concerns\HasColor;
use Filament\Support\Enums\IconPosition;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Htmlable;
class Stat extends Component
{
use CanOpenUrl;
use HasColor;
use HasDescription;
use HasLabel;
protected string $view = 'filament-widgets::stats-overview-widget.stat';
/**
* @var array<float> | null
*/
protected ?array $chart = null;
/**
* @var string | array<string> | null
*/
protected string | array | null $chartColor = null;
protected string | BackedEnum | null $icon = null;
protected string | BackedEnum | null $descriptionIcon = null;
protected IconPosition | string | null $descriptionIconPosition = null;
/**
* @var string | array<string> | null
*/
protected string | array | null $descriptionColor = null;
/**
* @var scalar | Htmlable | Closure
*/
protected $value;
/**
* @param scalar | Htmlable | Closure $value
*/
final public function __construct(string | Htmlable $label, $value)
{
$this->label($label);
$this->value($value);
}
/**
* @param scalar | Htmlable | Closure $value
*/
public static function make(string | Htmlable $label, $value): static
{
return app(static::class, ['label' => $label, 'value' => $value]);
}
/**
* @param string | array<string> | null $color
*/
public function chartColor(string | array | null $color): static
{
$this->chartColor = $color;
return $this;
}
public function icon(string | BackedEnum | null $icon): static
{
$this->icon = $icon;
return $this;
}
/**
* @param string | array<string> | null $color
*/
public function descriptionColor(string | array | null $color): static
{
$this->descriptionColor = $color;
return $this;
}
public function descriptionIcon(string | BackedEnum | null $icon, IconPosition | string | null $position = null): static
{
$this->descriptionIcon = $icon;
$this->descriptionIconPosition = $position;
return $this;
}
/**
* @param array<float> | Arrayable | null $chart
*/
public function chart(array | Arrayable | null $chart): static
{
if (is_null($chart)) {
return $this;
}
if ($chart instanceof Arrayable) {
$chart = $chart->toArray();
}
$this->chart = $chart;
return $this;
}
/**
* @param scalar | Htmlable | Closure $value
*/
public function value($value): static
{
$this->value = $value;
return $this;
}
/**
* @return array<float> | null
*/
public function getChart(): ?array
{
return $this->chart;
}
/**
* @return string | array<string> | null
*/
public function getChartColor(): string | array | null
{
return $this->chartColor ?? $this->getColor();
}
public function getIcon(): string | BackedEnum | Htmlable | null
{
return $this->icon;
}
/**
* @return string | array<string> | null
*/
public function getDescriptionColor(): string | array | null
{
return $this->descriptionColor ?? $this->getColor();
}
public function getDescriptionIcon(): string | BackedEnum | Htmlable | null
{
return $this->descriptionIcon;
}
public function getDescriptionIconPosition(): IconPosition | string
{
return $this->descriptionIconPosition ?? IconPosition::After;
}
/**
* @return scalar | Htmlable | Closure
*/
public function getValue(): mixed
{
return value($this->value);
}
public function generateChartDataChecksum(): string
{
return md5(json_encode($this->getChart()) . now());
}
}
@@ -0,0 +1,53 @@
<?php
namespace Filament\Widgets;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Enums\PaginationMode;
use Filament\Tables\Table;
use Illuminate\Contracts\Support\Htmlable;
class TableWidget extends Widget implements HasActions, HasSchemas, HasTable
{
use InteractsWithActions;
use InteractsWithSchemas;
use InteractsWithTable {
makeTable as makeBaseTable;
}
/**
* @var view-string
*/
protected string $view = 'filament-widgets::table-widget';
/**
* @deprecated Override the `table()` method to configure the table.
*/
protected static ?string $heading = null;
/**
* @deprecated Override the `table()` method to configure the table.
*/
protected function getTableHeading(): string | Htmlable | null
{
return static::$heading;
}
protected function makeTable(): Table
{
return $this->makeBaseTable()
->heading(
$this->getTableHeading() ?? (string) str(class_basename(static::class))
->beforeLast('Widget')
->kebab()
->replace('-', ' ')
->ucwords(),
)
->paginationMode(PaginationMode::Simple);
}
}
@@ -0,0 +1,18 @@
<?php
namespace Filament\Widgets\View\Components;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class ChartWidgetComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
return [];
}
}
@@ -0,0 +1,57 @@
<?php
namespace Filament\Widgets\View\Components\StatsOverviewWidgetComponent\StatComponent;
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class DescriptionComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
$gray = FilamentColor::getColor('gray');
ksort($color);
foreach (array_keys($color) as $shade) {
if ($shade < 600) {
continue;
}
if (Color::isTextContrastRatioAccessible('oklch(1 0 0)', $color[$shade])) {
$text = $shade;
break;
}
}
$text ??= 950;
krsort($color);
foreach (array_keys($color) as $shade) {
if ($shade > 400) {
continue;
}
if (Color::isTextContrastRatioAccessible($gray[900], $color[$shade])) {
$darkText = $shade;
break;
}
}
$darkText ??= 200;
return [
'text' => $text,
'dark:text' => $darkText,
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace Filament\Widgets\View\Components\StatsOverviewWidgetComponent\StatComponent;
use Filament\Support\View\Components\Contracts\HasColor;
use Filament\Support\View\Components\Contracts\HasDefaultGrayColor;
class StatsOverviewWidgetStatChartComponent implements HasColor, HasDefaultGrayColor
{
/**
* @param array<int, string> $color
* @return array<string, int>
*/
public function getColorMap(array $color): array
{
return [];
}
}
@@ -0,0 +1,8 @@
<?php
namespace Filament\Widgets\View;
class WidgetsIconAlias
{
const CHART_WIDGET_FILTER = 'widgets::chart-widget.filter';
}
@@ -0,0 +1,10 @@
<?php
namespace Filament\Widgets\View;
class WidgetsRenderHook
{
const TABLE_WIDGET_END = 'widgets::table-widget.end';
const TABLE_WIDGET_START = 'widgets::table-widget.start';
}
@@ -0,0 +1,108 @@
<?php
namespace Filament\Widgets;
use Filament\Support\Concerns\CanBeLazy;
use Illuminate\Contracts\View\View;
use Livewire\Component;
abstract class Widget extends Component
{
use CanBeLazy;
protected static bool $isDiscovered = true;
protected static ?int $sort = null;
/**
* @var view-string
*/
protected string $view;
/**
* @var int | string | array<string, int | null>
*/
protected int | string | array $columnSpan = 1;
/**
* @var int | string | array<string, int | null>
*/
protected int | string | array $columnStart = [];
public static function canView(): bool
{
return true;
}
public static function getSort(): int
{
return static::$sort ?? -1;
}
/**
* @return int | string | array<string, int | null>
*/
public function getColumnSpan(): int | string | array
{
return $this->columnSpan;
}
/**
* @return int | string | array<string, int | null>
*/
public function getColumnStart(): int | string | array
{
return $this->columnStart;
}
/**
* @return array<string, mixed>
*/
protected function getViewData(): array
{
return [];
}
public static function isDiscovered(): bool
{
return static::$isDiscovered;
}
public function render(): View
{
return view($this->view, $this->getViewData());
}
/**
* @param array<string, mixed> $properties
*/
public static function make(array $properties = []): WidgetConfiguration
{
return app(WidgetConfiguration::class, ['widget' => static::class, 'properties' => $properties]);
}
/**
* @return array<string, mixed>
*/
public function getPlaceholderData(): array
{
return [
'columnSpan' => $this->getColumnSpan(),
'columnStart' => $this->getColumnStart(),
];
}
/**
* @return array<string, mixed>
*/
public static function getDefaultProperties(): array
{
$properties = [];
if (static::isLazy()) {
$properties['lazy'] = true;
}
return $properties;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Filament\Widgets;
class WidgetConfiguration
{
/**
* @param class-string<Widget> $widget
* @param array<string, mixed> $properties
*/
public function __construct(
public readonly string $widget,
protected array $properties = [],
) {}
/**
* @return array<string, mixed>
*/
public function getProperties(): array
{
return $this->properties;
}
}
@@ -0,0 +1,39 @@
<?php
namespace Filament\Widgets;
use Filament\Support\Assets\AlpineComponent;
use Filament\Support\Facades\FilamentAsset;
use Illuminate\Filesystem\Filesystem;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
class WidgetsServiceProvider extends PackageServiceProvider
{
public function configurePackage(Package $package): void
{
$package
->name('filament-widgets')
->hasCommands([
Commands\MakeWidgetCommand::class,
])
->hasTranslations()
->hasViews();
}
public function packageBooted(): void
{
FilamentAsset::register([
AlpineComponent::make('chart', __DIR__ . '/../dist/components/chart.js'),
AlpineComponent::make('stats-overview/stat/chart', __DIR__ . '/../dist/components/stats-overview/stat/chart.js'),
], 'filament/widgets');
if ($this->app->runningInConsole()) {
foreach (app(Filesystem::class)->files(__DIR__ . '/../stubs/') as $file) {
$this->publishes([
$file->getRealPath() => base_path("stubs/filament/{$file->getFilename()}"),
], 'filament-stubs');
}
}
}
}
@@ -0,0 +1,5 @@
<x-filament-widgets::widget>
<x-filament::section>
{{-- Widget content --}}
</x-filament::section>
</x-filament-widgets::widget>