add nova
This commit is contained in:
66
nova/src/Metrics/HasHelpText.php
Normal file
66
nova/src/Metrics/HasHelpText.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
trait HasHelpText
|
||||
{
|
||||
/**
|
||||
* The help text for the metric.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $helpText;
|
||||
|
||||
/**
|
||||
* The width of the help text tooltip.
|
||||
*
|
||||
* @var string|int
|
||||
*/
|
||||
public $helpWidth = 250;
|
||||
|
||||
/**
|
||||
* Add help text to the metric.
|
||||
*
|
||||
* @param string $text
|
||||
* @return $this
|
||||
*/
|
||||
public function help($text)
|
||||
{
|
||||
$this->helpText = $text;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the help text for the metric.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHelpText()
|
||||
{
|
||||
return $this->helpText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the width for the help text tooltip.
|
||||
*
|
||||
* @param string $helpWidth
|
||||
* @return $this
|
||||
*/
|
||||
public function helpWidth($helpWidth)
|
||||
{
|
||||
$this->helpWidth = $helpWidth;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the width of the help text tooltip.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHelpWidth()
|
||||
{
|
||||
return $this->helpWidth;
|
||||
}
|
||||
}
|
||||
230
nova/src/Metrics/Metric.php
Normal file
230
nova/src/Metrics/Metric.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use DateInterval;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Nova\Card;
|
||||
use Laravel\Nova\Exceptions\HelperNotSupported;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
abstract class Metric extends Card
|
||||
{
|
||||
use HasHelpText;
|
||||
use ResolvesFilters;
|
||||
|
||||
/**
|
||||
* The displayable name of the metric.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* Indicates whether the metric should be refreshed when actions run.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $refreshWhenActionRuns = false;
|
||||
|
||||
/**
|
||||
* Indicates whether the metric should be refreshed when a filter is changed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $refreshWhenFiltersChange = false;
|
||||
|
||||
/**
|
||||
* Calculate the metric's value.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function resolve(NovaRequest $request)
|
||||
{
|
||||
$resolver = $this->getResolver($request);
|
||||
|
||||
if ($cacheFor = $this->cacheFor()) {
|
||||
$cacheFor = is_numeric($cacheFor) ? new DateInterval(sprintf('PT%dS', $cacheFor * 60)) : $cacheFor;
|
||||
|
||||
return Cache::remember(
|
||||
$this->getCacheKey($request),
|
||||
$cacheFor,
|
||||
$resolver
|
||||
);
|
||||
}
|
||||
|
||||
return $resolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a resolver function for the metric.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return \Closure(): mixed
|
||||
*/
|
||||
public function getResolver(NovaRequest $request)
|
||||
{
|
||||
return function () use ($request) {
|
||||
return $this->onlyOnDetail
|
||||
? $this->calculate($request, $request->findModelOrFail())
|
||||
: $this->calculate($request);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate cache key for the metric.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheKey(NovaRequest $request)
|
||||
{
|
||||
return sprintf(
|
||||
'nova.metric.%s.%s.%s.%s.%s.%s',
|
||||
$this->uriKey(),
|
||||
$request->input('range', 'no-range'),
|
||||
$request->input('timezone', 'no-timezone'),
|
||||
$request->input('twelveHourTime', 'no-12-hour-time'),
|
||||
$this->onlyOnDetail ? $request->findModelOrFail()->getKey() : 'no-resource-id',
|
||||
md5($request->input('filter', 'no-filter'))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the displayable name of the metric.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name()
|
||||
{
|
||||
return $this->name ?: Nova::humanize($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine for how many minutes the metric should be cached.
|
||||
*
|
||||
* @return \DateTimeInterface|\DateInterval|float|int|null
|
||||
*/
|
||||
public function cacheFor()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URI key for the metric.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function uriKey()
|
||||
{
|
||||
return Str::slug($this->name(), '-', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the metric should refresh when actions are run.
|
||||
*
|
||||
* @param bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function refreshWhenActionsRun($value = true)
|
||||
{
|
||||
$this->refreshWhenActionRuns = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the metric should refresh when actions are run.
|
||||
*
|
||||
* @param bool $value
|
||||
* @return $this
|
||||
*
|
||||
* @deprecated Use "refreshWhenActionsRun"
|
||||
*/
|
||||
public function refreshWhenActionRuns($value = true)
|
||||
{
|
||||
return $this->refreshWhenActionsRun($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the metric should refresh when filter changed.
|
||||
*
|
||||
* @param bool $value
|
||||
* @return $this
|
||||
*/
|
||||
public function refreshWhenFiltersChange($value = true)
|
||||
{
|
||||
if ($this->onlyOnDetail === true && $value === true) {
|
||||
throw new HelperNotSupported(sprintf('The %s helper method is not compatible with onlyOnDetail helper.', __METHOD__));
|
||||
}
|
||||
|
||||
$this->refreshWhenFiltersChange = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that the element should only be shown on the detail view.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onlyOnDetail()
|
||||
{
|
||||
if ($this->refreshWhenFiltersChange === true) {
|
||||
throw new HelperNotSupported(sprintf('The %s helper method is not compatible with refreshWhenFiltersChange helper.', __METHOD__));
|
||||
}
|
||||
|
||||
$this->onlyOnDetail = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the metric for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'class' => get_class($this),
|
||||
'name' => $this->name(),
|
||||
'uriKey' => $this->uriKey(),
|
||||
'helpWidth' => $this->getHelpWidth(),
|
||||
'helpText' => $this->getHelpText(),
|
||||
'refreshWhenActionRuns' => $this->refreshWhenActionRuns,
|
||||
'refreshWhenFiltersChange' => $this->refreshWhenFiltersChange,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert datetime to application timezone.
|
||||
*
|
||||
* @param \Carbon\CarbonInterface $datetime
|
||||
* @return \Carbon\CarbonInterface
|
||||
*/
|
||||
protected function asQueryDatetime($datetime)
|
||||
{
|
||||
if (! $datetime instanceof \DateTimeImmutable) {
|
||||
return $datetime->copy()->timezone(config('app.timezone'));
|
||||
}
|
||||
|
||||
return $datetime->timezone(config('app.timezone'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date between.
|
||||
*
|
||||
* @param array $ranges
|
||||
* @return array
|
||||
*/
|
||||
protected function formatQueryDateBetween(array $ranges)
|
||||
{
|
||||
return array_map(function ($datetime) {
|
||||
return $this->asQueryDatetime($datetime);
|
||||
}, $ranges);
|
||||
}
|
||||
}
|
||||
203
nova/src/Metrics/MetricTableRow.php
Normal file
203
nova/src/Metrics/MetricTableRow.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use JsonSerializable;
|
||||
use Laravel\Nova\Makeable;
|
||||
use Laravel\SerializableClosure\SerializableClosure;
|
||||
use Serializable;
|
||||
|
||||
class MetricTableRow implements JsonSerializable, Serializable
|
||||
{
|
||||
use Makeable;
|
||||
|
||||
/**
|
||||
* The icon of the metric row.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $icon;
|
||||
|
||||
/**
|
||||
* The icon class of the metric row.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $iconClass;
|
||||
|
||||
/**
|
||||
* The title of the metric row.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $title;
|
||||
|
||||
/**
|
||||
* The subtitle of the metric row.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $subtitle;
|
||||
|
||||
/**
|
||||
* The action callback used to generate the actions for the metric row.
|
||||
*
|
||||
* @var (\Closure():(array))|null
|
||||
*/
|
||||
public $actionCallback;
|
||||
|
||||
/**
|
||||
* Create a new Metric row.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->actionCallback = function () {
|
||||
return [];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the icon for the metric row.
|
||||
*
|
||||
* @param string $icon
|
||||
* @return $this
|
||||
*/
|
||||
public function icon($icon)
|
||||
{
|
||||
$this->icon = $icon;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the icon class for the metric row.
|
||||
*
|
||||
* @param string $class
|
||||
* @return $this
|
||||
*/
|
||||
public function iconClass($class)
|
||||
{
|
||||
$this->iconClass = $class;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title for the metric row.
|
||||
*
|
||||
* @param string $title
|
||||
* @return $this
|
||||
*/
|
||||
public function title($title)
|
||||
{
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the subtitle for the metric row.
|
||||
*
|
||||
* @param string $subtitle
|
||||
* @return $this
|
||||
*/
|
||||
public function subtitle($subtitle)
|
||||
{
|
||||
$this->subtitle = $subtitle;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the actions used for the metric row.
|
||||
*
|
||||
* @param \Closure():array $actionCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function actions($actionCallback)
|
||||
{
|
||||
$this->actionCallback = $actionCallback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the metric row for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'icon' => $this->icon,
|
||||
'iconClass' => $this->iconClass,
|
||||
'title' => $this->title,
|
||||
'subtitle' => $this->subtitle,
|
||||
'actions' => call_user_func($this->actionCallback),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize current object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
return serialize([
|
||||
'icon' => $this->icon,
|
||||
'iconClass' => $this->iconClass,
|
||||
'title' => $this->title,
|
||||
'subtitle' => $this->subtitle,
|
||||
'actions' => new SerializableClosure($this->actionCallback),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserialize current object.
|
||||
*
|
||||
* @param string $data
|
||||
* @return void
|
||||
*/
|
||||
public function unserialize($data)
|
||||
{
|
||||
$payload = unserialize($data);
|
||||
|
||||
$this->icon = $payload['icon'];
|
||||
$this->iconClass = $payload['iconClass'];
|
||||
$this->title = $payload['title'];
|
||||
$this->subtitle = $payload['subtitle'];
|
||||
$this->actionCallback = $payload['actions']->getClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize current object.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function __serialize()
|
||||
{
|
||||
return [
|
||||
'icon' => $this->icon,
|
||||
'iconClass' => $this->iconClass,
|
||||
'title' => $this->title,
|
||||
'subtitle' => $this->subtitle,
|
||||
'actions' => new SerializableClosure($this->actionCallback),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserialize current object.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return void
|
||||
*/
|
||||
public function __unserialize(array $data)
|
||||
{
|
||||
$this->icon = $data['icon'];
|
||||
$this->iconClass = $data['iconClass'];
|
||||
$this->title = $data['title'];
|
||||
$this->subtitle = $data['subtitle'];
|
||||
$this->actionCallback = $data['actions']->getClosure();
|
||||
}
|
||||
}
|
||||
37
nova/src/Metrics/MySqlTrendDateExpression.php
Normal file
37
nova/src/Metrics/MySqlTrendDateExpression.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
class MySqlTrendDateExpression extends TrendDateExpression
|
||||
{
|
||||
/**
|
||||
* Get the value of the expression.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
$offset = $this->offset();
|
||||
|
||||
if ($offset > 0) {
|
||||
$interval = '+ INTERVAL '.$offset.' HOUR';
|
||||
} elseif ($offset === 0) {
|
||||
$interval = '';
|
||||
} else {
|
||||
$interval = '- INTERVAL '.($offset * -1).' HOUR';
|
||||
}
|
||||
|
||||
switch ($this->unit) {
|
||||
case 'month':
|
||||
return "date_format({$this->wrap($this->column)} {$interval}, '%Y-%m')";
|
||||
case 'week':
|
||||
return "date_format({$this->wrap($this->column)} {$interval}, '%x-%v')";
|
||||
case 'day':
|
||||
return "date_format({$this->wrap($this->column)} {$interval}, '%Y-%m-%d')";
|
||||
case 'hour':
|
||||
return "date_format({$this->wrap($this->column)} {$interval}, '%Y-%m-%d %H:00')";
|
||||
case 'minute':
|
||||
return "date_format({$this->wrap($this->column)} {$interval}, '%Y-%m-%d %H:%i:00')";
|
||||
}
|
||||
}
|
||||
}
|
||||
154
nova/src/Metrics/Partition.php
Normal file
154
nova/src/Metrics/Partition.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Nova\Util;
|
||||
|
||||
abstract class Partition extends Metric
|
||||
{
|
||||
use RoundingPrecision;
|
||||
|
||||
/**
|
||||
* The element's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'partition-metric';
|
||||
|
||||
/**
|
||||
* Return a partition result showing the segments of a count aggregate.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $groupBy
|
||||
* @param \Illuminate\Database\Query\Expression|string|null $column
|
||||
* @return \Laravel\Nova\Metrics\PartitionResult
|
||||
*/
|
||||
public function count($request, $model, $groupBy, $column = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'count', $column, $groupBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a partition result showing the segments of an average aggregate.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string|null $column
|
||||
* @param \Illuminate\Database\Query\Expression|string $groupBy
|
||||
* @return \Laravel\Nova\Metrics\PartitionResult
|
||||
*/
|
||||
public function average($request, $model, $column, $groupBy)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'avg', $column, $groupBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a partition result showing the segments of a sum aggregate.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string|null $column
|
||||
* @param \Illuminate\Database\Query\Expression|string $groupBy
|
||||
* @return \Laravel\Nova\Metrics\PartitionResult
|
||||
*/
|
||||
public function sum($request, $model, $column, $groupBy)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'sum', $column, $groupBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a partition result showing the segments of a max aggregate.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string|null $column
|
||||
* @param \Illuminate\Database\Query\Expression|string $groupBy
|
||||
* @return \Laravel\Nova\Metrics\PartitionResult
|
||||
*/
|
||||
public function max($request, $model, $column, $groupBy)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'max', $column, $groupBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a partition result showing the segments of a min aggregate.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string|null $column
|
||||
* @param \Illuminate\Database\Query\Expression|string $groupBy
|
||||
* @return \Laravel\Nova\Metrics\PartitionResult
|
||||
*/
|
||||
public function min($request, $model, $column, $groupBy)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'min', $column, $groupBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a partition result showing the segments of a aggregate.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string $function
|
||||
* @param \Illuminate\Database\Query\Expression|string|null $column
|
||||
* @param \Illuminate\Database\Query\Expression|string $groupBy
|
||||
* @return \Laravel\Nova\Metrics\PartitionResult
|
||||
*/
|
||||
protected function aggregate($request, $model, $function, $column, $groupBy)
|
||||
{
|
||||
$query = $model instanceof Builder ? $model : (new $model)->newQuery();
|
||||
$grammar = $query->getQuery()->getGrammar();
|
||||
|
||||
$wrappedColumn = $grammar->wrap($column ?? $query->getModel()->getQualifiedKeyName());
|
||||
|
||||
$results = $query->select(
|
||||
$groupBy, DB::raw("{$function}({$wrappedColumn}) as aggregate")
|
||||
)->tap(function ($query) use ($request) {
|
||||
return $this->applyFilterQuery($request, $query);
|
||||
})->groupBy($groupBy)->get();
|
||||
|
||||
return $this->result($results->mapWithKeys(function ($result) use ($grammar, $groupBy) {
|
||||
return $this->formatAggregateResult(
|
||||
$result,
|
||||
$groupBy instanceof Expression ? $grammar->getValue($groupBy) : $groupBy
|
||||
);
|
||||
})->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the aggregate result for the partition.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $result
|
||||
* @param string $groupBy
|
||||
* @return array<string|int, int|float>
|
||||
*/
|
||||
protected function formatAggregateResult($result, $groupBy)
|
||||
{
|
||||
$key = with($result->{last(explode('.', $groupBy))}, function ($key) {
|
||||
return Util::value($key);
|
||||
});
|
||||
|
||||
if (! is_int($key)) {
|
||||
$key = (string) $key;
|
||||
}
|
||||
|
||||
return [$key => $result->aggregate ?? 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new partition metric result.
|
||||
*
|
||||
* @param array<string, int|float> $value
|
||||
* @return \Laravel\Nova\Metrics\PartitionResult
|
||||
*/
|
||||
public function result(array $value)
|
||||
{
|
||||
return new PartitionResult(collect($value)->map(function ($number) {
|
||||
return round($number, $this->roundingPrecision, $this->roundingMode);
|
||||
})->toArray());
|
||||
}
|
||||
}
|
||||
57
nova/src/Metrics/PartitionColors.php
Normal file
57
nova/src/Metrics/PartitionColors.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
class PartitionColors
|
||||
{
|
||||
/**
|
||||
* The color array to use for the chart.
|
||||
*
|
||||
* @var array<string|int, string>
|
||||
*/
|
||||
public $colors;
|
||||
|
||||
/**
|
||||
* The pointer to the current color in the chart array.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $pointer = 0;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param array<string|int, string> $colors
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($colors = [])
|
||||
{
|
||||
$this->colors = $colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color found at the given label key.
|
||||
*
|
||||
* @param string|int $label
|
||||
* @return string|null
|
||||
*/
|
||||
public function get($label)
|
||||
{
|
||||
return $this->colors[$label] ?? $this->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next color in the color list.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
protected function next()
|
||||
{
|
||||
return blank($this->colors) ? null :
|
||||
tap($this->colors[
|
||||
$this->pointer % count($this->colors)
|
||||
] ?? null, function () {
|
||||
$this->pointer++;
|
||||
});
|
||||
}
|
||||
}
|
||||
98
nova/src/Metrics/PartitionResult.php
Normal file
98
nova/src/Metrics/PartitionResult.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use Closure;
|
||||
use JsonSerializable;
|
||||
|
||||
class PartitionResult implements JsonSerializable
|
||||
{
|
||||
use RoundingPrecision;
|
||||
|
||||
/**
|
||||
* The value of the result.
|
||||
*
|
||||
* @var array<string, int|float>
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* The custom label name.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $labels = [];
|
||||
|
||||
/**
|
||||
* The custom label colors.
|
||||
*
|
||||
* @var \Laravel\Nova\Metrics\PartitionColors
|
||||
*/
|
||||
public $colors;
|
||||
|
||||
/**
|
||||
* Create a new partition result instance.
|
||||
*
|
||||
* @param array<string, int|float> $value
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array $value)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->colors = new PartitionColors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the labels for the partition result.
|
||||
*
|
||||
* @param \Closure(string):string $callback
|
||||
* @return $this
|
||||
*/
|
||||
public function label(Closure $callback)
|
||||
{
|
||||
$this->labels = collect($this->value)->mapWithKeys(function ($value, $label) use ($callback) {
|
||||
return [$label => $callback($label !== '' ? $label : null)];
|
||||
})->all();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom label colors.
|
||||
*
|
||||
* @param array<string, string> $colors
|
||||
* @return $this
|
||||
*/
|
||||
public function colors(array $colors)
|
||||
{
|
||||
$this->colors = new PartitionColors($colors);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the metric result for JSON serialization.
|
||||
*
|
||||
* @return array<string, array<array-key, array<string, mixed>>>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$values = collect($this->value);
|
||||
$total = $values->sum();
|
||||
|
||||
return [
|
||||
'value' => $values->map(function ($value, $label) use ($total) {
|
||||
$resolvedLabel = $this->labels[$label] ?? $label;
|
||||
|
||||
return array_filter([
|
||||
'color' => data_get($this->colors->colors, $label, $this->colors->get($resolvedLabel)),
|
||||
'label' => $resolvedLabel,
|
||||
'value' => $value,
|
||||
'percentage' => $total > 0 ? round(($value / $total) * 100, $this->roundingPrecision, $this->roundingMode) : 0,
|
||||
], function ($value) {
|
||||
return ! is_null($value);
|
||||
});
|
||||
})->values()->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
37
nova/src/Metrics/PostgresTrendDateExpression.php
Normal file
37
nova/src/Metrics/PostgresTrendDateExpression.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
class PostgresTrendDateExpression extends TrendDateExpression
|
||||
{
|
||||
/**
|
||||
* Get the value of the expression.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
$offset = $this->offset();
|
||||
|
||||
if ($offset > 0) {
|
||||
$interval = '+ interval \''.$offset.' hour\'';
|
||||
} elseif ($offset === 0) {
|
||||
$interval = '';
|
||||
} else {
|
||||
$interval = '- interval \''.($offset * -1).' HOUR\'';
|
||||
}
|
||||
|
||||
switch ($this->unit) {
|
||||
case 'month':
|
||||
return "to_char({$this->wrap($this->column)} {$interval}, 'YYYY-MM')";
|
||||
case 'week':
|
||||
return "to_char({$this->wrap($this->column)} {$interval}, 'IYYY-IW')";
|
||||
case 'day':
|
||||
return "to_char({$this->wrap($this->column)} {$interval}, 'YYYY-MM-DD')";
|
||||
case 'hour':
|
||||
return "to_char({$this->wrap($this->column)} {$interval}, 'YYYY-MM-DD HH24:00')";
|
||||
case 'minute':
|
||||
return "to_char({$this->wrap($this->column)} {$interval}, 'YYYY-MM-DD HH24:mi:00')";
|
||||
}
|
||||
}
|
||||
}
|
||||
96
nova/src/Metrics/Progress.php
Normal file
96
nova/src/Metrics/Progress.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
abstract class Progress extends Metric
|
||||
{
|
||||
use RoundingPrecision;
|
||||
|
||||
/**
|
||||
* The element's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'progress-metric';
|
||||
|
||||
/**
|
||||
* Return a progress result showing the growth of an count aggregate.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param callable(\Illuminate\Database\Eloquent\Builder):void $progress
|
||||
* @param \Illuminate\Database\Query\Expression|string|null $column
|
||||
* @param int|float|null $target
|
||||
* @return \Laravel\Nova\Metrics\ProgressResult
|
||||
*/
|
||||
public function count($request, $model, callable $progress, $column = null, $target = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'count', $column, $progress, $target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a progress result showing the growth of a sum aggregate.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param callable(\Illuminate\Database\Eloquent\Builder):void $progress
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param int|float|null $target
|
||||
* @return \Laravel\Nova\Metrics\ProgressResult
|
||||
*/
|
||||
public function sum($request, $model, callable $progress, $column, $target = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'sum', $column, $progress, $target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a progress result showing the segments of a aggregate.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string $function
|
||||
* @param \Illuminate\Database\Query\Expression|string|null $column
|
||||
* @param callable(\Illuminate\Database\Eloquent\Builder):void $progress
|
||||
* @param int|float|null $target
|
||||
* @return \Laravel\Nova\Metrics\ProgressResult
|
||||
*/
|
||||
protected function aggregate($request, $model, $function, $column, callable $progress, $target = null)
|
||||
{
|
||||
$query = $model instanceof Builder ? $model : (new $model)->newQuery();
|
||||
|
||||
$column = $column ?? $query->getModel()->getQualifiedKeyName();
|
||||
|
||||
$query->tap(function ($query) use ($request) {
|
||||
return $this->applyFilterQuery($request, $query);
|
||||
});
|
||||
|
||||
return $this->result(
|
||||
round(
|
||||
(clone $query)->tap(function ($query) use ($progress) {
|
||||
call_user_func($progress, $query);
|
||||
})->{$function}($column) ?? 0,
|
||||
$this->roundingPrecision,
|
||||
$this->roundingMode
|
||||
),
|
||||
$target ?? round(
|
||||
(clone $query)->{$function}($column) ?? 0,
|
||||
$this->roundingPrecision,
|
||||
$this->roundingMode
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new progress metric result.
|
||||
*
|
||||
* @param int|float $value
|
||||
* @param int|float $target
|
||||
* @return \Laravel\Nova\Metrics\ProgressResult
|
||||
*/
|
||||
public function result($value, $target)
|
||||
{
|
||||
return new ProgressResult($value, $target);
|
||||
}
|
||||
}
|
||||
181
nova/src/Metrics/ProgressResult.php
Normal file
181
nova/src/Metrics/ProgressResult.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class ProgressResult implements JsonSerializable
|
||||
{
|
||||
use RoundingPrecision;
|
||||
use TransformsResults;
|
||||
|
||||
/**
|
||||
* The current value of the result.
|
||||
*
|
||||
* @var int|float
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* The target value.
|
||||
*
|
||||
* @var int|float
|
||||
*/
|
||||
public $target;
|
||||
|
||||
/**
|
||||
* The metric value prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $prefix;
|
||||
|
||||
/**
|
||||
* The metric value suffix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $suffix;
|
||||
|
||||
/**
|
||||
* Whether to run inflection on the suffix.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $suffixInflection = true;
|
||||
|
||||
/**
|
||||
* The metric value formatting.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $format;
|
||||
|
||||
/**
|
||||
* Indicates if this metric is to be avoided.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $avoid = false;
|
||||
|
||||
/**
|
||||
* Create a new progress result instance.
|
||||
*
|
||||
* @param int|float $value
|
||||
* @param int|float $target
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($value, $target)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->target = $target;
|
||||
|
||||
$this->roundingPrecision = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the metric represents a dollar value.
|
||||
*
|
||||
* @param string $symbol
|
||||
* @return $this
|
||||
*/
|
||||
public function dollars($symbol = '$')
|
||||
{
|
||||
return $this->prefix($symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the metric represents a euro value.
|
||||
*
|
||||
* @param string $symbol
|
||||
* @return $this
|
||||
*/
|
||||
public function euros($symbol = '€')
|
||||
{
|
||||
return $this->prefix($symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metric value prefix.
|
||||
*
|
||||
* @param string $prefix
|
||||
* @return $this
|
||||
*/
|
||||
public function prefix($prefix)
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metric value suffix.
|
||||
*
|
||||
* @param string $suffix
|
||||
* @return $this
|
||||
*/
|
||||
public function suffix($suffix)
|
||||
{
|
||||
$this->suffix = $suffix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't apply suffix inflections.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutSuffixInflection()
|
||||
{
|
||||
$this->suffixInflection = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metric value formatting.
|
||||
*
|
||||
* @param string $format
|
||||
* @return $this
|
||||
*/
|
||||
public function format($format)
|
||||
{
|
||||
$this->format = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that this progress metric is tracking a "goal" that should be avoided.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function avoid()
|
||||
{
|
||||
$this->avoid = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the metric result for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$target = max($this->value, $this->target);
|
||||
|
||||
return [
|
||||
'value' => $this->resolveTransformedValue($this->value),
|
||||
'target' => $this->resolveTransformedValue($target),
|
||||
'percentage' => $target > 0 ? round(($this->value / $target) * 100, $this->roundingPrecision, $this->roundingMode) : 0,
|
||||
'prefix' => $this->prefix,
|
||||
'suffix' => $this->suffix,
|
||||
'suffixInflection' => $this->suffixInflection,
|
||||
'format' => $this->format,
|
||||
'avoid' => $this->avoid,
|
||||
];
|
||||
}
|
||||
}
|
||||
58
nova/src/Metrics/RangedMetric.php
Normal file
58
nova/src/Metrics/RangedMetric.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
abstract class RangedMetric extends Metric
|
||||
{
|
||||
/**
|
||||
* The ranges available for the metric.
|
||||
*
|
||||
* @var array<string|int, string>
|
||||
*/
|
||||
public $ranges = [];
|
||||
|
||||
/**
|
||||
* The selected range key.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $selectedRangeKey;
|
||||
|
||||
/**
|
||||
* Get the ranges available for the metric.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection|array
|
||||
*/
|
||||
public function ranges()
|
||||
{
|
||||
return $this->ranges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default range.
|
||||
*
|
||||
* @param string $key
|
||||
* @return $this
|
||||
*/
|
||||
public function defaultRange($key)
|
||||
{
|
||||
$this->selectedRangeKey = $key;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the metric for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'selectedRangeKey' => $this->selectedRangeKey,
|
||||
'ranges' => collect($this->ranges())->map(function ($range, $key) {
|
||||
return ['label' => $range, 'value' => $key];
|
||||
})->values()->all(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
nova/src/Metrics/ResolvesFilters.php
Normal file
49
nova/src/Metrics/ResolvesFilters.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Nova\Filters\FilterDecoder;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
|
||||
trait ResolvesFilters
|
||||
{
|
||||
/**
|
||||
* Filters for the metric.
|
||||
*
|
||||
* @var \Illuminate\Support\Collection|null
|
||||
*/
|
||||
protected $filters;
|
||||
|
||||
/**
|
||||
* Set filters for current metric.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $filters
|
||||
* @return $this
|
||||
*/
|
||||
public function setAvailableFilters(Collection $filters)
|
||||
{
|
||||
$this->filters = $filters;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filter query.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function applyFilterQuery(NovaRequest $request, $query)
|
||||
{
|
||||
if ($this->filters instanceof Collection) {
|
||||
(new FilterDecoder($request->filter, $this->filters))
|
||||
->filters()
|
||||
->each
|
||||
->__invoke($request, $query);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
38
nova/src/Metrics/RoundingPrecision.php
Normal file
38
nova/src/Metrics/RoundingPrecision.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
trait RoundingPrecision
|
||||
{
|
||||
/**
|
||||
* Rounding precision.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $roundingPrecision = 0;
|
||||
|
||||
/**
|
||||
* Rounding mode.
|
||||
*
|
||||
* @var 1|2|3|4
|
||||
*/
|
||||
public $roundingMode = PHP_ROUND_HALF_UP;
|
||||
|
||||
/**
|
||||
* Set the precision level used when rounding the value.
|
||||
*
|
||||
* @param int $precision
|
||||
* @param 1|2|3|4 $mode
|
||||
* @return $this
|
||||
*/
|
||||
public function precision($precision = 0, $mode = PHP_ROUND_HALF_UP)
|
||||
{
|
||||
$this->roundingPrecision = $precision;
|
||||
|
||||
if (in_array($mode, [PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD])) {
|
||||
$this->roundingMode = $mode;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
42
nova/src/Metrics/SqlSrvTrendDateExpression.php
Normal file
42
nova/src/Metrics/SqlSrvTrendDateExpression.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
class SqlSrvTrendDateExpression extends TrendDateExpression
|
||||
{
|
||||
/**
|
||||
* Get the value of the expression.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
$column = $this->wrap($this->column);
|
||||
$offset = $this->offset();
|
||||
|
||||
if ($offset >= 0) {
|
||||
$interval = $offset;
|
||||
} else {
|
||||
$interval = '-'.($offset * -1);
|
||||
}
|
||||
|
||||
$date = "DATEADD(hour, {$interval}, {$column})";
|
||||
|
||||
switch ($this->unit) {
|
||||
case 'month':
|
||||
return "FORMAT({$date}, 'yyyy-MM')";
|
||||
case 'week':
|
||||
return "concat(
|
||||
YEAR({$date}),
|
||||
'-',
|
||||
datepart(ISO_WEEK, {$date})
|
||||
)";
|
||||
case 'day':
|
||||
return "FORMAT({$date}, 'yyyy-MM-dd')";
|
||||
case 'hour':
|
||||
return "FORMAT({$date}, 'yyyy-MM-dd HH:00')";
|
||||
case 'minute':
|
||||
return "FORMAT({$date}, 'yyyy-MM-dd HH:mm:00')";
|
||||
}
|
||||
}
|
||||
}
|
||||
41
nova/src/Metrics/SqliteTrendDateExpression.php
Normal file
41
nova/src/Metrics/SqliteTrendDateExpression.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
class SqliteTrendDateExpression extends TrendDateExpression
|
||||
{
|
||||
/**
|
||||
* Get the value of the expression.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
$offset = $this->offset();
|
||||
|
||||
if ($offset > 0) {
|
||||
$interval = '\'+'.$offset.' hour\'';
|
||||
} elseif ($offset === 0) {
|
||||
$interval = '\'+0 hour\'';
|
||||
} else {
|
||||
$interval = '\'-'.($offset * -1).' hour\'';
|
||||
}
|
||||
|
||||
switch ($this->unit) {
|
||||
case 'month':
|
||||
return "strftime('%Y-%m', datetime({$this->wrap($this->column)}, {$interval}))";
|
||||
case 'week':
|
||||
return "strftime('%Y-', datetime({$this->wrap($this->column)}, {$interval})) ||
|
||||
(
|
||||
strftime('%W', datetime({$this->wrap($this->column)}, {$interval})) +
|
||||
(1 - strftime('%W', strftime('%Y', datetime({$this->wrap($this->column)}, {$interval})) || '-01-04'))
|
||||
)";
|
||||
case 'day':
|
||||
return "strftime('%Y-%m-%d', datetime({$this->wrap($this->column)}, {$interval}))";
|
||||
case 'hour':
|
||||
return "strftime('%Y-%m-%d %H:00', datetime({$this->wrap($this->column)}, {$interval}))";
|
||||
case 'minute':
|
||||
return "strftime('%Y-%m-%d %H:%M:00', datetime({$this->wrap($this->column)}, {$interval}))";
|
||||
}
|
||||
}
|
||||
}
|
||||
47
nova/src/Metrics/Table.php
Normal file
47
nova/src/Metrics/Table.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
class Table extends Metric
|
||||
{
|
||||
/**
|
||||
* The element's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'table-metric';
|
||||
|
||||
/**
|
||||
* The text to be displayed when the table is empty.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $emptyText = 'No Results Found...';
|
||||
|
||||
/**
|
||||
* Set the text to be displayed when the table is empty.
|
||||
*
|
||||
* @param string $text
|
||||
* @return $this
|
||||
*/
|
||||
public function emptyText($text)
|
||||
{
|
||||
$this->emptyText = $text;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the metric for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'emptyText' => Nova::__($this->emptyText),
|
||||
]);
|
||||
}
|
||||
}
|
||||
41
nova/src/Metrics/TransformsResults.php
Normal file
41
nova/src/Metrics/TransformsResults.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use Laravel\SerializableClosure\SerializableClosure;
|
||||
|
||||
trait TransformsResults
|
||||
{
|
||||
/**
|
||||
* The callback used to transform the value before display.
|
||||
*
|
||||
* @var \Closure|callable|null
|
||||
*/
|
||||
public $transformCallback;
|
||||
|
||||
/**
|
||||
* Set the callback used to transform the value before presentation.
|
||||
*
|
||||
* @param \Closure|callable $transformCallback
|
||||
* @return $this
|
||||
*/
|
||||
public function transform($transformCallback)
|
||||
{
|
||||
$this->transformCallback = new SerializableClosure($transformCallback);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the transformed value result.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function resolveTransformedValue($value)
|
||||
{
|
||||
return transform($value, $this->transformCallback ?? function ($value) {
|
||||
return $value;
|
||||
});
|
||||
}
|
||||
}
|
||||
742
nova/src/Metrics/Trend.php
Normal file
742
nova/src/Metrics/Trend.php
Normal file
@@ -0,0 +1,742 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Carbon\CarbonInterface;
|
||||
use DateTime;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use InvalidArgumentException;
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
abstract class Trend extends RangedMetric
|
||||
{
|
||||
use RoundingPrecision;
|
||||
|
||||
/**
|
||||
* Trend metric unit constants.
|
||||
*/
|
||||
const BY_MONTHS = 'month';
|
||||
|
||||
const BY_WEEKS = 'week';
|
||||
|
||||
const BY_DAYS = 'day';
|
||||
|
||||
const BY_HOURS = 'hour';
|
||||
|
||||
const BY_MINUTES = 'minute';
|
||||
|
||||
/**
|
||||
* The element's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'trend-metric';
|
||||
|
||||
/**
|
||||
* Create a new trend metric result.
|
||||
*
|
||||
* @param int|float|numeric-string|null $value
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function result($value = null)
|
||||
{
|
||||
return new TrendResult($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a count aggregate over months.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string|null $column
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function countByMonths($request, $model, $column = null)
|
||||
{
|
||||
return $this->count($request, $model, self::BY_MONTHS, $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a count aggregate over weeks.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string|null $column
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function countByWeeks($request, $model, $column = null)
|
||||
{
|
||||
return $this->count($request, $model, self::BY_WEEKS, $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a count aggregate over days.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string|null $column
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function countByDays($request, $model, $column = null)
|
||||
{
|
||||
return $this->count($request, $model, self::BY_DAYS, $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a count aggregate over hours.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string|null $column
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function countByHours($request, $model, $column = null)
|
||||
{
|
||||
return $this->count($request, $model, self::BY_HOURS, $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a count aggregate over minutes.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string|null $column
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function countByMinutes($request, $model, $column = null)
|
||||
{
|
||||
return $this->count($request, $model, self::BY_MINUTES, $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a count aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string $unit
|
||||
* @param string|null $column
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function count($request, $model, $unit, $column = null)
|
||||
{
|
||||
$resource = $model instanceof Builder ? $model->getModel() : new $model;
|
||||
|
||||
$column = $column ?? $resource->getQualifiedCreatedAtColumn();
|
||||
|
||||
return $this->aggregate($request, $model, $unit, 'count', $resource->getQualifiedKeyName(), $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a average aggregate over months.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function averageByMonths($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_MONTHS, 'avg', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a average aggregate over weeks.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function averageByWeeks($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_WEEKS, 'avg', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a average aggregate over days.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function averageByDays($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_DAYS, 'avg', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a average aggregate over hours.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function averageByHours($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_HOURS, 'avg', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a average aggregate over minutes.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function averageByMinutes($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_MINUTES, 'avg', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a average aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string $unit
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function average($request, $model, $unit, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, $unit, 'avg', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a sum aggregate over months.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function sumByMonths($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_MONTHS, 'sum', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a sum aggregate over weeks.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function sumByWeeks($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_WEEKS, 'sum', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a sum aggregate over days.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function sumByDays($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_DAYS, 'sum', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a sum aggregate over hours.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function sumByHours($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_HOURS, 'sum', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a sum aggregate over minutes.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function sumByMinutes($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_MINUTES, 'sum', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a sum aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string $unit
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function sum($request, $model, $unit, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, $unit, 'sum', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a max aggregate over months.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return TrendResult
|
||||
*/
|
||||
public function maxByMonths($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_MONTHS, 'max', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a max aggregate over weeks.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function maxByWeeks($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_WEEKS, 'max', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a max aggregate over days.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function maxByDays($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_DAYS, 'max', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a max aggregate over hours.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function maxByHours($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_HOURS, 'max', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a max aggregate over minutes.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function maxByMinutes($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_MINUTES, 'max', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a max aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string $unit
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function max($request, $model, $unit, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, $unit, 'max', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a min aggregate over months.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function minByMonths($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_MONTHS, 'min', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a min aggregate over weeks.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function minByWeeks($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_WEEKS, 'min', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a min aggregate over days.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function minByDays($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_DAYS, 'min', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a min aggregate over hours.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function minByHours($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_HOURS, 'min', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a min aggregate over minutes.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function minByMinutes($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, self::BY_MINUTES, 'min', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a min aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string $unit
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
public function min($request, $model, $unit, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, $unit, 'min', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing a aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string $unit
|
||||
* @param string $function
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\TrendResult
|
||||
*/
|
||||
protected function aggregate($request, $model, $unit, $function, $column, $dateColumn = null)
|
||||
{
|
||||
$query = $model instanceof Builder ? $model : (new $model)->newQuery();
|
||||
|
||||
$timezone = Nova::resolveUserTimezone($request) ?? $this->getDefaultTimezone($request);
|
||||
|
||||
$expression = (string) TrendDateExpressionFactory::make(
|
||||
$query, $dateColumn = $dateColumn ?? $query->getModel()->getQualifiedCreatedAtColumn(),
|
||||
$unit, $timezone
|
||||
);
|
||||
|
||||
$possibleDateResults = $this->getAllPossibleDateResults(
|
||||
$startingDate = $this->getAggregateStartingDate($request, $unit, $timezone),
|
||||
$endingDate = CarbonImmutable::now($timezone),
|
||||
$unit,
|
||||
$request->twelveHourTime === 'true',
|
||||
$request->range
|
||||
);
|
||||
|
||||
$wrappedColumn = $query->getQuery()->getGrammar()->wrap($column);
|
||||
|
||||
$results = $query
|
||||
->select(DB::raw("{$expression} as date_result, {$function}({$wrappedColumn}) as aggregate"))
|
||||
->tap(function ($query) use ($request) {
|
||||
return $this->applyFilterQuery($request, $query);
|
||||
})
|
||||
->whereBetween(
|
||||
$dateColumn, $this->formatQueryDateBetween([$startingDate, $endingDate])
|
||||
)->groupBy(DB::raw($expression))
|
||||
->orderBy('date_result')
|
||||
->get();
|
||||
|
||||
$possibleDateKeys = array_keys($possibleDateResults);
|
||||
|
||||
$results = array_merge($possibleDateResults, $results->mapWithKeys(function ($result) use ($request, $unit) {
|
||||
return [$this->formatAggregateResultDate(
|
||||
$result->date_result, $unit, $request->twelveHourTime === 'true'
|
||||
) => round($result->aggregate ?? 0, $this->roundingPrecision, $this->roundingMode)];
|
||||
})->reject(function ($value, $key) use ($possibleDateKeys) {
|
||||
return ! in_array($key, $possibleDateKeys);
|
||||
})->all());
|
||||
|
||||
return $this->result(Arr::last($results))->trend(
|
||||
$results
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the proper aggregate starting date.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $unit
|
||||
* @param mixed $timezone
|
||||
* @return \Carbon\CarbonInterface
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function getAggregateStartingDate($request, $unit, $timezone)
|
||||
{
|
||||
$now = CarbonImmutable::now($timezone);
|
||||
|
||||
$range = $request->range ?? 1;
|
||||
$ranges = collect($this->ranges())->keys()->values()->all();
|
||||
|
||||
if (count($ranges) > 0 && ! in_array($range, $ranges)) {
|
||||
$range = min($range, max($ranges));
|
||||
}
|
||||
|
||||
switch ($unit) {
|
||||
case 'month':
|
||||
return $now->subMonthsWithoutOverflow($range - 1)->firstOfMonth()->setTime(0, 0);
|
||||
|
||||
case 'week':
|
||||
return $now->subWeeks($range - 1)->startOfWeek()->setTime(0, 0);
|
||||
|
||||
case 'day':
|
||||
return $now->subDays($range - 1)->setTime(0, 0);
|
||||
|
||||
case 'hour':
|
||||
return with($now->subHours($range - 1), function ($now) {
|
||||
return $now->setTimeFromTimeString($now->hour.':00');
|
||||
});
|
||||
|
||||
case 'minute':
|
||||
return with($now->subMinutes($range - 1), function ($now) {
|
||||
return $now->setTimeFromTimeString($now->hour.':'.$now->minute.':00');
|
||||
});
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid trend unit provided.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the aggregate result date into a proper string.
|
||||
*
|
||||
* @param string $result
|
||||
* @param string $unit
|
||||
* @param bool $twelveHourTime
|
||||
* @return string
|
||||
*/
|
||||
protected function formatAggregateResultDate($result, $unit, $twelveHourTime)
|
||||
{
|
||||
switch ($unit) {
|
||||
case 'month':
|
||||
return $this->formatAggregateMonthDate($result);
|
||||
|
||||
case 'week':
|
||||
return $this->formatAggregateWeekDate($result);
|
||||
|
||||
case 'day':
|
||||
return with(Carbon::createFromFormat('Y-m-d', $result), function ($date) {
|
||||
return Nova::__($date->format('F')).' '.$date->format('j').', '.$date->format('Y');
|
||||
});
|
||||
|
||||
case 'hour':
|
||||
return with(Carbon::createFromFormat('Y-m-d H:00', $result), function ($date) use ($twelveHourTime) {
|
||||
return $twelveHourTime
|
||||
? Nova::__($date->format('F')).' '.$date->format('j').' - '.$date->format('g:00 A')
|
||||
: Nova::__($date->format('F')).' '.$date->format('j').' - '.$date->format('G:00');
|
||||
});
|
||||
|
||||
case 'minute':
|
||||
default:
|
||||
return with(Carbon::createFromFormat('Y-m-d H:i:00', $result), function ($date) use ($twelveHourTime) {
|
||||
return $twelveHourTime
|
||||
? Nova::__($date->format('F')).' '.$date->format('j').' - '.$date->format('g:i A')
|
||||
: Nova::__($date->format('F')).' '.$date->format('j').' - '.$date->format('G:i');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the aggregate month result date into a proper string.
|
||||
*
|
||||
* @param string $result
|
||||
* @return string
|
||||
*/
|
||||
protected function formatAggregateMonthDate($result)
|
||||
{
|
||||
[$year, $month] = explode('-', $result);
|
||||
|
||||
return with(Carbon::create((int) $year, (int) $month, 1), function ($date) {
|
||||
return Nova::__($date->format('F')).' '.$date->format('Y');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the aggregate week result date into a proper string.
|
||||
*
|
||||
* @param string $result
|
||||
* @return string
|
||||
*/
|
||||
protected function formatAggregateWeekDate($result)
|
||||
{
|
||||
[$year, $week] = explode('-', $result);
|
||||
|
||||
$isoDate = (new DateTime)->setISODate((int) $year, (int) $week)->setTime(0, 0);
|
||||
|
||||
[$startingDate, $endingDate] = [
|
||||
Carbon::instance($isoDate),
|
||||
Carbon::instance($isoDate)->endOfWeek(),
|
||||
];
|
||||
|
||||
return Nova::__($startingDate->format('F')).' '.$startingDate->format('j').' - '.
|
||||
Nova::__($endingDate->format('F')).' '.$endingDate->format('j');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the possible date results for the given units.
|
||||
*
|
||||
* @param \Carbon\CarbonInterface $startingDate
|
||||
* @param \Carbon\CarbonInterface $endingDate
|
||||
* @param string $unit
|
||||
* @param bool $twelveHourTime
|
||||
* @param int $possibleDateRange
|
||||
* @return array<string, int>
|
||||
*/
|
||||
protected function getAllPossibleDateResults(CarbonInterface $startingDate, CarbonInterface $endingDate,
|
||||
$unit, $twelveHourTime, $possibleDateRange)
|
||||
{
|
||||
$nextDate = Carbon::instance($startingDate);
|
||||
|
||||
do {
|
||||
$possibleDateResults[
|
||||
$this->formatPossibleAggregateResultDate(
|
||||
$nextDate, $unit, $twelveHourTime
|
||||
)
|
||||
] = 0;
|
||||
|
||||
if ($unit === self::BY_MONTHS) {
|
||||
$nextDate->addMonthWithOverflow();
|
||||
} elseif ($unit === self::BY_WEEKS) {
|
||||
$nextDate->addWeek();
|
||||
} elseif ($unit === self::BY_DAYS) {
|
||||
$nextDate->addDay();
|
||||
} elseif ($unit === self::BY_HOURS) {
|
||||
$nextDate->addHour();
|
||||
} elseif ($unit === self::BY_MINUTES) {
|
||||
$nextDate->addMinute();
|
||||
}
|
||||
} while ($nextDate->lte($endingDate));
|
||||
|
||||
if (count($possibleDateResults) < $possibleDateRange) {
|
||||
$possibleDateResults[
|
||||
$this->formatPossibleAggregateResultDate(
|
||||
$nextDate, $unit, $twelveHourTime
|
||||
)
|
||||
] = 0;
|
||||
}
|
||||
|
||||
return $possibleDateResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the possible aggregate result date into a proper string.
|
||||
*
|
||||
* @param \Carbon\CarbonInterface $date
|
||||
* @param string $unit
|
||||
* @param bool $twelveHourTime
|
||||
* @return string
|
||||
*/
|
||||
protected function formatPossibleAggregateResultDate(CarbonInterface $date, $unit, $twelveHourTime)
|
||||
{
|
||||
switch ($unit) {
|
||||
case 'month':
|
||||
return Nova::__($date->format('F')).' '.$date->format('Y');
|
||||
|
||||
case 'week':
|
||||
return Nova::__($date->startOfWeek()->format('F')).' '.$date->startOfWeek()->format('j').' - '.
|
||||
Nova::__($date->endOfWeek()->format('F')).' '.$date->endOfWeek()->format('j');
|
||||
|
||||
case 'day':
|
||||
return Nova::__($date->format('F')).' '.$date->format('j').', '.$date->format('Y');
|
||||
|
||||
case 'hour':
|
||||
return $twelveHourTime
|
||||
? Nova::__($date->format('F')).' '.$date->format('j').' - '.$date->format('g:00 A')
|
||||
: Nova::__($date->format('F')).' '.$date->format('j').' - '.$date->format('G:00');
|
||||
|
||||
case 'minute':
|
||||
default:
|
||||
return $twelveHourTime
|
||||
? Nova::__($date->format('F')).' '.$date->format('j').' - '.$date->format('g:i A')
|
||||
: Nova::__($date->format('F')).' '.$date->format('j').' - '.$date->format('G:i');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default timezone.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
private function getDefaultTimezone($request)
|
||||
{
|
||||
return $request->timezone ?? config('app.timezone');
|
||||
}
|
||||
}
|
||||
112
nova/src/Metrics/TrendDateExpression.php
Normal file
112
nova/src/Metrics/TrendDateExpression.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
abstract class TrendDateExpression
|
||||
{
|
||||
/**
|
||||
* The value of the expression.
|
||||
*
|
||||
* @var string|int|float
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* The query builder being used to build the trend.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* The column being measured.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $column;
|
||||
|
||||
/**
|
||||
* The unit being measured.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $unit;
|
||||
|
||||
/**
|
||||
* The user's local timezone.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $timezone;
|
||||
|
||||
/**
|
||||
* Create a new raw query expression.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $column
|
||||
* @param string $unit
|
||||
* @param string $timezone
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Builder $query, $column, $unit, $timezone)
|
||||
{
|
||||
$this->unit = $unit;
|
||||
$this->query = $query;
|
||||
$this->column = $column;
|
||||
$this->timezone = $timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timezone offset for the user's timezone.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function offset()
|
||||
{
|
||||
$timezoneOffset = function ($timezone) {
|
||||
return (new DateTime(CarbonImmutable::now()->format('Y-m-d H:i:s'), new DateTimeZone($timezone)))->getOffset() / 60 / 60;
|
||||
};
|
||||
|
||||
if ($this->timezone) {
|
||||
$appOffset = $timezoneOffset(config('app.timezone'));
|
||||
$userOffset = $timezoneOffset($this->timezone);
|
||||
|
||||
return $userOffset - $appOffset;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the given value using the query's grammar.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
protected function wrap($value)
|
||||
{
|
||||
return $this->query->getQuery()->getGrammar()->wrap($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the expression.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function getValue();
|
||||
|
||||
/**
|
||||
* Get the value of the expression.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return (string) $this->getValue();
|
||||
}
|
||||
}
|
||||
46
nova/src/Metrics/TrendDateExpressionFactory.php
Normal file
46
nova/src/Metrics/TrendDateExpressionFactory.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class TrendDateExpressionFactory
|
||||
{
|
||||
use Macroable;
|
||||
|
||||
/**
|
||||
* Create a new trend expression instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $column
|
||||
* @param string $unit
|
||||
* @param string $timezone
|
||||
* @return \Laravel\Nova\Metrics\TrendDateExpression
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function make(Builder $query, $column, $unit, $timezone)
|
||||
{
|
||||
$driver = $query->getConnection()->getDriverName();
|
||||
|
||||
if (static::hasMacro($driver)) {
|
||||
return static::$driver($query, $column, $unit, $timezone);
|
||||
}
|
||||
|
||||
switch ($driver) {
|
||||
case 'sqlite':
|
||||
return new SqliteTrendDateExpression($query, $column, $unit, $timezone);
|
||||
case 'mysql':
|
||||
case 'mariadb':
|
||||
return new MySqlTrendDateExpression($query, $column, $unit, $timezone);
|
||||
case 'pgsql':
|
||||
return new PostgresTrendDateExpression($query, $column, $unit, $timezone);
|
||||
case 'sqlsrv':
|
||||
return new SqlSrvTrendDateExpression($query, $column, $unit, $timezone);
|
||||
default:
|
||||
throw new InvalidArgumentException('Trend metric helpers are not supported for this database.');
|
||||
}
|
||||
}
|
||||
}
|
||||
201
nova/src/Metrics/TrendResult.php
Normal file
201
nova/src/Metrics/TrendResult.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class TrendResult implements JsonSerializable
|
||||
{
|
||||
use TransformsResults;
|
||||
|
||||
/**
|
||||
* The value of the result.
|
||||
*
|
||||
* @var int|float|numeric-string|null
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* The trend data of the result.
|
||||
*
|
||||
* @var array<string, int|float|numeric-string|null>
|
||||
*/
|
||||
public $trend = [];
|
||||
|
||||
/**
|
||||
* The metric value prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $prefix;
|
||||
|
||||
/**
|
||||
* The metric value suffix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $suffix;
|
||||
|
||||
/**
|
||||
* Whether to run inflection on the suffix.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $suffixInflection = true;
|
||||
|
||||
/**
|
||||
* The metric value formatting.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $format;
|
||||
|
||||
/**
|
||||
* Create a new trend result instance.
|
||||
*
|
||||
* @param int|float|numeric-string|null $value
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($value = null)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary result amount for the trend.
|
||||
*
|
||||
* @param int|float|numeric-string|null $value
|
||||
* @return $this
|
||||
*/
|
||||
public function result($value = null)
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the latest value of the trend as the primary result.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function showLatestValue()
|
||||
{
|
||||
return $this->result(last($this->trend));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sum of all the values of the trend as the primary result.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function showSumValue()
|
||||
{
|
||||
return $this->result(array_sum(array_values($this->trend)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the trend of data for the metric.
|
||||
*
|
||||
* @param array<string, int|float|numeric-string|null> $trend
|
||||
* @return $this
|
||||
*/
|
||||
public function trend(array $trend)
|
||||
{
|
||||
$this->trend = $trend;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the metric represents a dollar value.
|
||||
*
|
||||
* @param string $symbol
|
||||
* @return $this
|
||||
*/
|
||||
public function dollars($symbol = '$')
|
||||
{
|
||||
return $this->prefix($symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the metric represents a euro value.
|
||||
*
|
||||
* @param string $symbol
|
||||
* @return $this
|
||||
*/
|
||||
public function euros($symbol = '€')
|
||||
{
|
||||
return $this->prefix($symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metric value prefix.
|
||||
*
|
||||
* @param string $prefix
|
||||
* @return $this
|
||||
*/
|
||||
public function prefix($prefix)
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metric value suffix.
|
||||
*
|
||||
* @param string $suffix
|
||||
* @return $this
|
||||
*/
|
||||
public function suffix($suffix)
|
||||
{
|
||||
$this->suffix = $suffix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't apply suffix inflections.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutSuffixInflection()
|
||||
{
|
||||
$this->suffixInflection = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metric value formatting.
|
||||
*
|
||||
* @param string $format
|
||||
* @return $this
|
||||
*/
|
||||
public function format($format)
|
||||
{
|
||||
$this->format = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the metric result for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'value' => $this->resolveTransformedValue($this->value),
|
||||
'trend' => collect($this->trend)->transform(function ($value) {
|
||||
return $this->resolveTransformedValue($value);
|
||||
})->all(),
|
||||
'prefix' => $this->prefix,
|
||||
'suffix' => $this->suffix,
|
||||
'suffixInflection' => $this->suffixInflection,
|
||||
'format' => $this->format,
|
||||
];
|
||||
}
|
||||
}
|
||||
335
nova/src/Metrics/Value.php
Normal file
335
nova/src/Metrics/Value.php
Normal file
@@ -0,0 +1,335 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Laravel\Nova\Nova;
|
||||
|
||||
abstract class Value extends RangedMetric
|
||||
{
|
||||
use RoundingPrecision;
|
||||
|
||||
/**
|
||||
* The element's component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component = 'value-metric';
|
||||
|
||||
/**
|
||||
* The element's icon.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $icon = 'chart-bar';
|
||||
|
||||
/**
|
||||
* Set the icon for the metric.
|
||||
*
|
||||
* @param string $icon
|
||||
* @return $this
|
||||
*/
|
||||
public function icon($icon)
|
||||
{
|
||||
$this->icon = $icon;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing the growth of an count aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string|null $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\ValueResult
|
||||
*/
|
||||
public function count($request, $model, $column = null, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'count', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing the growth of an average aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\ValueResult
|
||||
*/
|
||||
public function average($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'avg', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing the growth of a sum aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\ValueResult
|
||||
*/
|
||||
public function sum($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'sum', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing the growth of a maximum aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\ValueResult
|
||||
*/
|
||||
public function max($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'max', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing the growth of a minimum aggregate over time.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param \Illuminate\Database\Query\Expression|string $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\ValueResult
|
||||
*/
|
||||
public function min($request, $model, $column, $dateColumn = null)
|
||||
{
|
||||
return $this->aggregate($request, $model, 'min', $column, $dateColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value result showing the growth of a model over a given time frame.
|
||||
*
|
||||
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
|
||||
* @param \Illuminate\Database\Eloquent\Builder|class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
* @param string $function
|
||||
* @param \Illuminate\Database\Query\Expression|string|null $column
|
||||
* @param string|null $dateColumn
|
||||
* @return \Laravel\Nova\Metrics\ValueResult
|
||||
*/
|
||||
protected function aggregate($request, $model, $function, $column = null, $dateColumn = null)
|
||||
{
|
||||
$query = $model instanceof Builder ? $model : (new $model)->newQuery();
|
||||
|
||||
$query->tap(function ($query) use ($request) {
|
||||
return $this->applyFilterQuery($request, $query);
|
||||
});
|
||||
|
||||
$column = $column ?? $query->getModel()->getQualifiedKeyName();
|
||||
|
||||
if ($request->range === 'ALL') {
|
||||
return $this->result(
|
||||
round(
|
||||
(clone $query)->{$function}($column) ?? 0,
|
||||
$this->roundingPrecision,
|
||||
$this->roundingMode
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$dateColumn = $dateColumn ?? $query->getModel()->getQualifiedCreatedAtColumn();
|
||||
$timezone = Nova::resolveUserTimezone($request) ?? $this->getDefaultTimezone($request);
|
||||
$range = $request->range ?? 1;
|
||||
|
||||
$currentRange = $this->currentRange($range, $timezone);
|
||||
$previousRange = $this->previousRange($range, $timezone);
|
||||
|
||||
$previousValue = round(
|
||||
(clone $query)->whereBetween(
|
||||
$dateColumn, $this->formatQueryDateBetween($previousRange)
|
||||
)->{$function}($column) ?? 0,
|
||||
$this->roundingPrecision,
|
||||
$this->roundingMode
|
||||
);
|
||||
|
||||
return $this->result(
|
||||
round(
|
||||
(clone $query)->whereBetween(
|
||||
$dateColumn, $this->formatQueryDateBetween($currentRange)
|
||||
)->{$function}($column) ?? 0,
|
||||
$this->roundingPrecision,
|
||||
$this->roundingMode
|
||||
)
|
||||
)->previous($previousValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the previous range and calculate any short-cuts.
|
||||
*
|
||||
* @param string|int $range
|
||||
* @param string $timezone
|
||||
* @return array<int, \Carbon\CarbonInterface>
|
||||
*/
|
||||
protected function previousRange($range, $timezone)
|
||||
{
|
||||
if ($range == 'TODAY') {
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->subDay()->startOfDay(),
|
||||
CarbonImmutable::now($timezone)->subDay()->endOfDay(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($range == 'YESTERDAY') {
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->subDays(2)->startOfDay(),
|
||||
CarbonImmutable::now($timezone)->subDays(2)->endOfDay(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($range == 'THIS_WEEK') {
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->subWeek()->startOfWeek(),
|
||||
CarbonImmutable::now($timezone)->subWeek()->endOfWeek(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($range == 'MTD') {
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->subMonthWithoutOverflow()->startOfMonth(),
|
||||
CarbonImmutable::now($timezone)->subMonthWithoutOverflow(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($range == 'QTD') {
|
||||
return $this->previousQuarterRange($timezone);
|
||||
}
|
||||
|
||||
if ($range == 'YTD') {
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->subYear()->startOfYear(),
|
||||
CarbonImmutable::now($timezone)->subYear(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->subDays($range * 2),
|
||||
CarbonImmutable::now($timezone)->subDays($range)->subSecond(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the previous quarter range.
|
||||
*
|
||||
* @param string $timezone
|
||||
* @return array<int, \Carbon\CarbonImmutable>
|
||||
*/
|
||||
protected function previousQuarterRange($timezone)
|
||||
{
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->subQuarterWithOverflow()->startOfQuarter(),
|
||||
CarbonImmutable::now($timezone)->subQuarterWithOverflow()->subSecond(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the current range and calculate any short-cuts.
|
||||
*
|
||||
* @param string|int $range
|
||||
* @param string $timezone
|
||||
* @return array<int, \Carbon\CarbonInterface>
|
||||
*/
|
||||
protected function currentRange($range, $timezone)
|
||||
{
|
||||
if ($range == 'TODAY') {
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->startOfDay(),
|
||||
CarbonImmutable::now($timezone)->endOfDay(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($range == 'YESTERDAY') {
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->subDay()->startOfDay(),
|
||||
CarbonImmutable::now($timezone)->subDay()->endOfDay(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($range == 'THIS_WEEK') {
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->startOfWeek(),
|
||||
CarbonImmutable::now($timezone)->endOfWeek(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($range == 'MTD') {
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->startOfMonth(),
|
||||
CarbonImmutable::now($timezone),
|
||||
];
|
||||
}
|
||||
|
||||
if ($range == 'QTD') {
|
||||
return $this->currentQuarterRange($timezone);
|
||||
}
|
||||
|
||||
if ($range == 'YTD') {
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->startOfYear(),
|
||||
CarbonImmutable::now($timezone),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->subDays($range),
|
||||
CarbonImmutable::now($timezone),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the previous quarter range.
|
||||
*
|
||||
* @param string $timezone
|
||||
* @return array<int, \Carbon\CarbonImmutable>
|
||||
*/
|
||||
protected function currentQuarterRange($timezone)
|
||||
{
|
||||
return [
|
||||
CarbonImmutable::now($timezone)->startOfQuarter(),
|
||||
CarbonImmutable::now($timezone),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new value metric result.
|
||||
*
|
||||
* @param int|float|numeric-string|null $value
|
||||
* @return \Laravel\Nova\Metrics\ValueResult
|
||||
*/
|
||||
public function result($value)
|
||||
{
|
||||
return new ValueResult($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default timezone.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
private function getDefaultTimezone($request)
|
||||
{
|
||||
return $request->timezone ?? config('app.timezone');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the metric for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_merge(parent::jsonSerialize(), [
|
||||
'icon' => $this->icon,
|
||||
]);
|
||||
}
|
||||
}
|
||||
253
nova/src/Metrics/ValueResult.php
Normal file
253
nova/src/Metrics/ValueResult.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Nova\Metrics;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* @phpstan-type TNumbroFormat array{
|
||||
* average?: bool,
|
||||
* forceSign?: bool,
|
||||
* mantissa?: int,
|
||||
* negative?: string,
|
||||
* optionalMantissa?: bool,
|
||||
* output?: string,
|
||||
* spaceSeparated?: bool,
|
||||
* thousandSeparated?: bool,
|
||||
* trimMantissa?: bool
|
||||
* }
|
||||
*/
|
||||
class ValueResult implements JsonSerializable
|
||||
{
|
||||
use TransformsResults;
|
||||
|
||||
/**
|
||||
* The value of the result.
|
||||
*
|
||||
* @var int|float|numeric-string|null
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* The previous value.
|
||||
*
|
||||
* @var int|float|numeric-string|null
|
||||
*/
|
||||
public $previous;
|
||||
|
||||
/**
|
||||
* The previous value label.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $previousLabel;
|
||||
|
||||
/**
|
||||
* The metric value prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $prefix;
|
||||
|
||||
/**
|
||||
* The metric value suffix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $suffix;
|
||||
|
||||
/**
|
||||
* Whether to run inflection on the suffix.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $suffixInflection = true;
|
||||
|
||||
/**
|
||||
* The metric value formatting.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $format = '(0[.]00a)';
|
||||
|
||||
/**
|
||||
* Determines whether a value of 0 counts as "No Current Data".
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $zeroResult = false;
|
||||
|
||||
/**
|
||||
* Indicates if the metric value is copyable inside Nova.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $copyable = false;
|
||||
|
||||
/**
|
||||
* The metric tooltip value formatting.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $tooltipFormat = '(0[.]00a)';
|
||||
|
||||
/**
|
||||
* Create a new value result instance.
|
||||
*
|
||||
* @param int|float|numeric-string|null $value
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the previous value for the metric.
|
||||
*
|
||||
* @param int|float|numeric-string|null $previous
|
||||
* @param string $label
|
||||
* @return $this
|
||||
*/
|
||||
public function previous($previous, $label = null)
|
||||
{
|
||||
$this->previous = $previous;
|
||||
$this->previousLabel = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the metric represents a dollar value.
|
||||
*
|
||||
* @param string $symbol
|
||||
* @return $this
|
||||
*/
|
||||
public function dollars($symbol = '$')
|
||||
{
|
||||
return $this->currency($symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the metric represents a currency value.
|
||||
*
|
||||
* @param string $symbol
|
||||
* @return $this
|
||||
*/
|
||||
public function currency($symbol = '$')
|
||||
{
|
||||
return $this->prefix($symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metric value prefix.
|
||||
*
|
||||
* @param string $prefix
|
||||
* @return $this
|
||||
*/
|
||||
public function prefix($prefix)
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metric value suffix.
|
||||
*
|
||||
* @param string $suffix
|
||||
* @return $this
|
||||
*/
|
||||
public function suffix($suffix)
|
||||
{
|
||||
$this->suffix = $suffix;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't apply suffix inflections.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function withoutSuffixInflection()
|
||||
{
|
||||
$this->suffixInflection = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metric value formatting.
|
||||
*
|
||||
* @param array<string, mixed>|string $format
|
||||
* @return $this
|
||||
*
|
||||
* @phpstan-param TNumbroFormat|string $format
|
||||
*/
|
||||
public function format($format)
|
||||
{
|
||||
$this->format = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the metric value tooltip formatting.
|
||||
*
|
||||
* @param string $format
|
||||
* @return $this
|
||||
*/
|
||||
public function tooltipFormat($format)
|
||||
{
|
||||
$this->tooltipFormat = $format;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the zeroResult value.
|
||||
*
|
||||
* @param bool $zeroResult
|
||||
* @return $this
|
||||
*/
|
||||
public function allowZeroResult($zeroResult = true)
|
||||
{
|
||||
$this->zeroResult = $zeroResult;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the metric value to be copyable to the clipboard inside Nova.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function copyable()
|
||||
{
|
||||
$this->copyable = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the metric result for JSON serialization.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'copyable' => $this->copyable,
|
||||
'format' => $this->format,
|
||||
'prefix' => $this->prefix,
|
||||
'previous' => $this->resolveTransformedValue($this->previous),
|
||||
'previousLabel' => $this->previousLabel,
|
||||
'suffix' => $this->suffix,
|
||||
'suffixInflection' => $this->suffixInflection,
|
||||
'tooltipFormat' => $this->tooltipFormat,
|
||||
'value' => $this->resolveTransformedValue($this->value),
|
||||
'zeroResult' => $this->zeroResult,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user