Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m45s
Implemented the first version of review output resolve actions. Included a ReviewOutputResolveActionMapper, commands to seed browser fixtures, updated CustomerReviewWorkspace, EnvironmentReviewResource, UI enforcement, and related views. Also added extensive unit, feature, and browser tests, and updated the design coverage matrix.
216 lines
7.2 KiB
PHP
216 lines
7.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\ResolutionGuidance;
|
|
|
|
final class ResolutionAction
|
|
{
|
|
public const string TYPE_NAVIGATION = 'navigation';
|
|
|
|
public const string TYPE_DOWNLOAD = 'download';
|
|
|
|
public const string TYPE_DISCLOSURE = 'disclosure';
|
|
|
|
public const string TYPE_NONE = 'none';
|
|
|
|
public const string TYPE_DOMAIN_ACTION = 'domain_action';
|
|
|
|
public const string TYPE_OPERATION_ACTION = 'operation_action';
|
|
|
|
/**
|
|
* @param array{
|
|
* key?:mixed,
|
|
* label?:mixed,
|
|
* type?:mixed,
|
|
* url?:mixed,
|
|
* icon?:mixed,
|
|
* kind?:mixed,
|
|
* action_name?:mixed,
|
|
* capability?:mixed,
|
|
* requires_confirmation?:mixed,
|
|
* audit_event?:mixed,
|
|
* operation_run_type?:mixed,
|
|
* disabled_reason?:mixed
|
|
* }|null $action
|
|
* @return array{
|
|
* key:string,
|
|
* label:string,
|
|
* type:string,
|
|
* url:?string,
|
|
* icon:string,
|
|
* kind:string,
|
|
* action_name:?string,
|
|
* capability:?string,
|
|
* requires_confirmation:bool,
|
|
* audit_event:?string,
|
|
* operation_run_type:?string,
|
|
* disabled_reason:?string
|
|
* }
|
|
*/
|
|
public static function fromArray(?array $action, string $fallbackKey, string $fallbackLabel = 'Unavailable'): array
|
|
{
|
|
$label = is_string($action['label'] ?? null) && trim((string) $action['label']) !== ''
|
|
? trim((string) $action['label'])
|
|
: $fallbackLabel;
|
|
$url = is_string($action['url'] ?? null) && trim((string) $action['url']) !== ''
|
|
? trim((string) $action['url'])
|
|
: null;
|
|
$rawType = is_string($action['type'] ?? null) && trim((string) $action['type']) !== ''
|
|
? trim((string) $action['type'])
|
|
: null;
|
|
$rawKind = is_string($action['kind'] ?? null) && trim((string) $action['kind']) !== ''
|
|
? trim((string) $action['kind'])
|
|
: null;
|
|
$key = is_string($action['key'] ?? null) && trim((string) $action['key']) !== ''
|
|
? trim((string) $action['key'])
|
|
: $fallbackKey;
|
|
$type = $rawType ?? self::typeFromKind($rawKind, $url);
|
|
$actionName = is_string($action['action_name'] ?? null) && trim((string) $action['action_name']) !== ''
|
|
? trim((string) $action['action_name'])
|
|
: null;
|
|
$capability = is_string($action['capability'] ?? null) && trim((string) $action['capability']) !== ''
|
|
? trim((string) $action['capability'])
|
|
: null;
|
|
$requiresConfirmation = (bool) ($action['requires_confirmation'] ?? false);
|
|
$auditEvent = is_string($action['audit_event'] ?? null) && trim((string) $action['audit_event']) !== ''
|
|
? trim((string) $action['audit_event'])
|
|
: null;
|
|
$operationRunType = is_string($action['operation_run_type'] ?? null) && trim((string) $action['operation_run_type']) !== ''
|
|
? trim((string) $action['operation_run_type'])
|
|
: null;
|
|
|
|
if (self::isUnsafeExecutable($type, $capability, $auditEvent, $requiresConfirmation, $operationRunType)) {
|
|
$type = self::fallbackType($rawKind, $url);
|
|
$actionName = null;
|
|
$capability = null;
|
|
$requiresConfirmation = false;
|
|
$auditEvent = null;
|
|
$operationRunType = null;
|
|
}
|
|
|
|
$kind = self::kindFromType($type, $rawKind);
|
|
$icon = is_string($action['icon'] ?? null) && trim((string) $action['icon']) !== ''
|
|
? trim((string) $action['icon'])
|
|
: self::iconForType($type);
|
|
$disabledReason = is_string($action['disabled_reason'] ?? null) && trim((string) $action['disabled_reason']) !== ''
|
|
? trim((string) $action['disabled_reason'])
|
|
: null;
|
|
|
|
return [
|
|
'key' => $key,
|
|
'label' => $label,
|
|
'type' => $type,
|
|
'url' => $url,
|
|
'icon' => $icon,
|
|
'kind' => $kind,
|
|
'action_name' => $actionName,
|
|
'capability' => $capability,
|
|
'requires_confirmation' => $requiresConfirmation,
|
|
'audit_event' => $auditEvent,
|
|
'operation_run_type' => $operationRunType,
|
|
'disabled_reason' => $disabledReason,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* key:string,
|
|
* label:string,
|
|
* type:string,
|
|
* url:null,
|
|
* icon:string,
|
|
* kind:string,
|
|
* action_name:null,
|
|
* capability:null,
|
|
* requires_confirmation:false,
|
|
* audit_event:null,
|
|
* operation_run_type:null,
|
|
* disabled_reason:?string
|
|
* }
|
|
*/
|
|
public static function none(string $key, string $label, ?string $disabledReason = null): array
|
|
{
|
|
return [
|
|
'key' => $key,
|
|
'label' => $label,
|
|
'type' => self::TYPE_NONE,
|
|
'url' => null,
|
|
'icon' => self::iconForType(self::TYPE_NONE),
|
|
'kind' => 'none',
|
|
'action_name' => null,
|
|
'capability' => null,
|
|
'requires_confirmation' => false,
|
|
'audit_event' => null,
|
|
'operation_run_type' => null,
|
|
'disabled_reason' => $disabledReason,
|
|
];
|
|
}
|
|
|
|
private static function typeFromKind(?string $kind, ?string $url): string
|
|
{
|
|
return match ($kind) {
|
|
'download' => self::TYPE_DOWNLOAD,
|
|
'disclosure' => self::TYPE_DISCLOSURE,
|
|
'none' => self::TYPE_NONE,
|
|
default => $url !== null ? self::TYPE_NAVIGATION : self::TYPE_NONE,
|
|
};
|
|
}
|
|
|
|
private static function fallbackType(?string $kind, ?string $url): string
|
|
{
|
|
return match (true) {
|
|
$kind === 'download' => self::TYPE_DOWNLOAD,
|
|
$kind === 'disclosure' => self::TYPE_DISCLOSURE,
|
|
$url !== null => self::TYPE_NAVIGATION,
|
|
default => self::TYPE_NONE,
|
|
};
|
|
}
|
|
|
|
private static function kindFromType(string $type, ?string $kind): string
|
|
{
|
|
if (is_string($kind) && $kind !== '') {
|
|
return $kind;
|
|
}
|
|
|
|
return match ($type) {
|
|
self::TYPE_DOWNLOAD => 'download',
|
|
self::TYPE_DISCLOSURE => 'disclosure',
|
|
self::TYPE_NONE => 'none',
|
|
default => 'environment_link',
|
|
};
|
|
}
|
|
|
|
private static function iconForType(string $type): string
|
|
{
|
|
return match ($type) {
|
|
self::TYPE_DOWNLOAD => 'heroicon-o-arrow-down-tray',
|
|
self::TYPE_DISCLOSURE => 'heroicon-o-information-circle',
|
|
self::TYPE_NONE => 'heroicon-o-minus-circle',
|
|
default => 'heroicon-o-arrow-top-right-on-square',
|
|
};
|
|
}
|
|
|
|
private static function isUnsafeExecutable(
|
|
string $type,
|
|
?string $capability,
|
|
?string $auditEvent,
|
|
bool $requiresConfirmation,
|
|
?string $operationRunType,
|
|
): bool {
|
|
if (! in_array($type, [self::TYPE_DOMAIN_ACTION, self::TYPE_OPERATION_ACTION], true)) {
|
|
return false;
|
|
}
|
|
|
|
if ($capability === null || $auditEvent === null) {
|
|
return true;
|
|
}
|
|
|
|
if (! $requiresConfirmation) {
|
|
return true;
|
|
}
|
|
|
|
return $type === self::TYPE_OPERATION_ACTION && $operationRunType === null;
|
|
}
|
|
}
|