This commit is contained in:
2024-09-01 18:54:23 +05:00
parent 76d18365a5
commit 061f09eca1
1597 changed files with 109451 additions and 1 deletions

View 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
View 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);
}
}

View 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();
}
}

View 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')";
}
}
}

View 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());
}
}

View 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++;
});
}
}

View 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(),
];
}
}

View 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')";
}
}
}

View 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);
}
}

View 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,
];
}
}

View 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(),
]);
}
}

View 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;
}
}

View 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;
}
}

View 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')";
}
}
}

View 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}))";
}
}
}

View 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),
]);
}
}

View 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
View 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');
}
}

View 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();
}
}

View 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.');
}
}
}

View 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
View 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,
]);
}
}

View 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,
];
}
}