You are here

abstract class AppAnalyticsFormBase in Apigee Edge 8

App analytics form builder for developer- and team apps.

Hierarchy

Expanded class hierarchy of AppAnalyticsFormBase

1 file declares its use of AppAnalyticsFormBase
TeamAppAnalyticsForm.php in modules/apigee_edge_teams/src/Form/TeamAppAnalyticsForm.php

File

src/Form/AppAnalyticsFormBase.php, line 42

Namespace

Drupal\apigee_edge\Form
View source
abstract class AppAnalyticsFormBase extends FormBase {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The SDK connector service.
   *
   * @var \Drupal\apigee_edge\SDKConnectorInterface
   */
  protected $connector;

  /**
   * The PrivateTempStore factory.
   *
   * @var \Drupal\Core\TempStore\PrivateTempStore
   */
  protected $store;

  /**
   * The URL generator.
   *
   * @var \Drupal\Core\Routing\UrlGeneratorInterface
   */
  protected $urlGenerator;

  /**
   * Constructs a new AppAnalyticsFormBase.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\apigee_edge\SDKConnectorInterface $sdk_connector
   *   The SDK connector service.
   * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempstore_private
   *   The private temp store factory.
   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
   *   The URL generator.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, SDKConnectorInterface $sdk_connector, PrivateTempStoreFactory $tempstore_private, UrlGeneratorInterface $url_generator) {
    $this->entityTypeManager = $entity_type_manager;
    $this->connector = $sdk_connector;
    $this->store = $tempstore_private
      ->get('apigee_edge.analytics');
    $this->urlGenerator = $url_generator;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('entity_type.manager'), $container
      ->get('apigee_edge.sdk_connector'), $container
      ->get('tempstore.private'), $container
      ->get('url_generator'));
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state, ?AppInterface $app = NULL) {

    // Little sanity check, child classes must set this parameter from route
    // before they call parent.
    if ($app === NULL) {
      $this
        ->messenger()
        ->addError($this
        ->t('Something went wrong.'));
      $this
        ->logger('apigee_edge')
        ->critical('App parameter was missing when the app analytics form got built.');
      return $form;
    }
    $config = $this
      ->config('apigee_edge.common_app_settings');
    $analytics_environment = $config
      ->get('analytics_environment');
    $analytics_available_environments = $config
      ->get('analytics_available_environments');
    $form_state
      ->disableRedirect();
    $form['#attached']['library'][] = 'apigee_edge/apigee_edge.analytics';
    $form['#attributes']['class'][] = 'apigee-edge-app-analytics';
    $form['controls'] = [
      '#type' => 'container',
    ];
    $form['controls']['label_container'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'controls-label',
        ],
      ],
    ];
    $form['controls']['label_container']['label'] = [
      '#markup' => $this
        ->t('Filter:'),
    ];
    $form['controls']['environment'] = [
      '#type' => 'value',
      '#value' => $analytics_environment,
    ];
    if (count($analytics_available_environments) > 1) {
      $form['controls']['environment'] = [
        '#type' => 'select',
        '#required' => TRUE,
        '#title' => t('Environment'),
        '#title_display' => 'invisible',
        '#default_value' => $analytics_environment,
        '#options' => array_combine($analytics_available_environments, $analytics_available_environments),
      ];
    }
    $form['controls']['metrics'] = [
      '#type' => 'select',
      '#options' => [
        'avg(total_response_time)' => $this
          ->t('Average response time'),
        'max(total_response_time)' => $this
          ->t('Max response time'),
        'min(total_response_time)' => $this
          ->t('Min response time'),
        'sum(message_count)' => $this
          ->t('Message count'),
        'sum(is_error)' => $this
          ->t('Error count'),
      ],
      '#default_value' => 'avg(total_response_time)',
      '#title' => t('Metrics'),
      '#title_display' => 'invisible',
    ];
    $form['controls']['since'] = [
      '#type' => 'datetime',
    ];
    $form['controls']['date_separator_container'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'date-separator',
        ],
      ],
    ];
    $form['controls']['date_separator_container']['date_separator'] = [
      '#markup' => '-',
    ];
    $form['controls']['until'] = [
      '#type' => 'datetime',
    ];
    $form['controls']['quick_date_picker'] = [
      '#type' => 'select',
      '#options' => [
        '1d' => $this
          ->t('Last Day'),
        '1w' => $this
          ->t('Last 7 Days'),
        '2w' => $this
          ->t('Last 2 Weeks'),
        'custom' => $this
          ->t('Custom range'),
      ],
      '#title' => t('Date range'),
      '#title_display' => 'invisible',
    ];
    $form['controls']['submit'] = [
      '#type' => 'submit',
      '#value' => $this
        ->t('Apply'),
    ];
    $offset = date('Z') / 3600;
    if ($offset > 0) {
      $offset = "+{$offset}";
    }
    elseif ($offset === 0) {
      $offset = "±{$offset}";
    }
    $form['timezone'] = [
      '#type' => 'html_tag',
      '#tag' => 'div',
      '#value' => $this
        ->t('Your timezone: @timezone (UTC@offset)', [
        '@timezone' => date_default_timezone_get(),
        '@offset' => $offset,
      ]),
    ];
    $form['export_csv'] = [
      '#type' => 'link',
      '#title' => $this
        ->t('Export CSV'),
      '#attributes' => [
        'role' => 'button',
      ],
    ];
    $form['chart'] = [
      '#type' => 'container',
      '#attributes' => [
        'id' => 'chart_container',
      ],
    ];
    $metric = $this
      ->getRequest()->query
      ->get('metric');
    $since = $this
      ->getRequest()->query
      ->get('since');
    $until = $this
      ->getRequest()->query
      ->get('until');
    $environment = $this
      ->getRequest()->query
      ->get('environment');
    if ($this
      ->validateQueryString($form, $metric, $since, $until, $environment)) {
      $form['controls']['metrics']['#default_value'] = $metric;
      $since_datetime = DrupalDatetime::createFromTimestamp($since);
      $since_datetime
        ->setTimezone(new \Datetimezone(date_default_timezone_get()));
      $until_datetime = DrupalDatetime::createFromTimestamp($until);
      $until_datetime
        ->setTimezone(new \Datetimezone(date_default_timezone_get()));
      $form['controls']['since']['#default_value'] = $since_datetime;
      $form['controls']['until']['#default_value'] = $until_datetime;
      $form['controls']['quick_date_picker']['#default_value'] = 'custom';
      $form['controls']['environment']['#default_value'] = $environment;
    }
    else {
      $default_since_value = new DrupalDateTime();
      $default_since_value
        ->sub(new \DateInterval('P1D'));
      $default_until_value = new DrupalDateTime();
      $form['controls']['since']['#default_value'] = $default_since_value;
      $form['controls']['until']['#default_value'] = $default_until_value;
      $metric = $form['controls']['metrics']['#default_value'];
      $since = $default_since_value
        ->getTimestamp();
      $until = $default_until_value
        ->getTimestamp();
      $environment = $analytics_environment;
    }
    if (empty($form_state
      ->getUserInput())) {

      // The last parameter allows to expose and make analytics environment
      // configurable later on the form.
      $this
        ->generateResponse($form, $app, $metric, $since, $until, $environment);
    }
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);
    $since = $form_state
      ->getValue('since');
    $until = $form_state
      ->getValue('until');
    if ($since instanceof DrupalDateTime && $until instanceof DrupalDateTime) {
      if ($since
        ->getTimestamp() !== $until
        ->getTimestamp()) {
        if ($since
          ->diff($until)->invert === 1) {
          $form_state
            ->setError($form['controls']['until'], $this
            ->t('The end date cannot be before the start date.'));
        }
      }
    }
  }

  /**
   * Validates the URL query string parameters.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param string $metric
   *   The filter parameter.
   * @param string $since
   *   The start date parameter.
   * @param string $until
   *   The end date parameter.
   * @param string $environment
   *   The environment parameter.
   *
   * @return bool
   *   TRUE if the parameters are correctly set, else FALSE.
   */
  protected function validateQueryString(array $form, $metric, $since, $until, $environment) : bool {
    if ($metric === NULL || $since === NULL || $until === NULL || $environment === NULL) {
      return FALSE;
    }
    try {
      if (!array_key_exists($metric, $form['controls']['metrics']['#options'])) {
        $this
          ->messenger()
          ->addError($this
          ->t('Invalid parameter metric in the URL.'));
        return FALSE;
      }
      $since = DrupalDateTime::createFromTimestamp($since);
      $until = DrupalDateTime::createFromTimestamp($until);
      if ($since
        ->diff($until)->invert === 1) {
        $this
          ->messenger()
          ->addError($this
          ->t('The end date cannot be before the start date.'));
        return FALSE;
      }
      if ($since
        ->diff(new DrupalDateTime())->invert === 1) {
        $this
          ->messenger()
          ->addError($this
          ->t('Start date cannot be in future. The current local time of the Developer Portal: @time', [
          '@time' => new DrupalDateTime(),
        ]));
        return FALSE;
      }
      if (!in_array($environment, $this
        ->config('apigee_edge.common_app_settings')
        ->get('analytics_available_environments'))) {
        $this
          ->messenger()
          ->addError($this
          ->t('Invalid parameter environment in the URL.'));
        return FALSE;
      }
    } catch (\InvalidArgumentException $exception) {
      $this
        ->messenger()
        ->addError($this
        ->t('Invalid URL query parameters.'));
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Requests analytics data and pass to the JavaScript chart drawing function.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\apigee_edge\Entity\AppInterface $app
   *   The app entity that analytics data gets displayed.
   * @param string $metric
   *   The filter parameter.
   * @param string $since
   *   The start date parameter.
   * @param string $until
   *   The end date parameter.
   * @param string $environment
   *   The analytics environment to query.
   *
   * @see apigee_edge.libraries.yml
   * @see apigee_edge.analytics.js
   */
  protected function generateResponse(array &$form, AppInterface $app, string $metric, string $since, string $until, string $environment) : void {
    $analytics = [];
    try {
      $analytics = $this
        ->getAnalytics($app, $metric, $since, $until, $environment);
    } catch (MomentException $e) {
      $this
        ->messenger()
        ->addError($this
        ->t('Invalid date parameters.'));
    } catch (\Exception $e) {
      $this
        ->messenger()
        ->addError($this
        ->t('Unable to retrieve analytics data. Please try again.'));
      watchdog_exception('apigee_edge', $e);
    }
    $date_time_zone = new \DateTimeZone(date_default_timezone_get());
    $timezone_offset = $date_time_zone
      ->getOffset(new \DateTime());
    $form['#attached']['drupalSettings']['analytics']['timezone_offset'] = $timezone_offset / 60;

    // Pass every necessary data to JavaScript.
    // Possible parameters:
    // - metric: name of the requested metric,
    // - timestamps: all time units in the given time interval,
    // - values: returned optimized metrics data.
    // - skip_zero_values: skip the zero analytics values or not,
    // - visualization_options: options for Google Charts draw() function,
    // - visualization_options_to_date: which property values should be
    // converted to JavaScript Date object on the client-side (timestamps),
    // - version: Google Charts library version (default is the current stable),
    // - language: to load a chart formatted for a specific locale,
    // - chart_container: ID attribute of the chart's HTML container element.
    if (isset($analytics['stats']['data'][0]['metric'][0]['values'])) {

      // Store analytics data in private temp storage.
      $analytics['metric'] = $form['controls']['metrics']['#options'][$metric];
      $this->store
        ->set($data_id = Crypt::randomBytesBase64(), $analytics);
      $form['export_csv']['#url'] = Url::fromRoute('apigee_edge.export_analytics.csv', [
        'data_id' => $data_id,
      ]);
      $form['#attached']['drupalSettings']['analytics']['metric'] = $form['controls']['metrics']['#options'][$metric];
      $form['#attached']['drupalSettings']['analytics']['timestamps'] = $analytics['TimeUnit'];
      $form['#attached']['drupalSettings']['analytics']['values'] = $analytics['stats']['data'][0]['metric'][0]['values'];
      $form['#attached']['drupalSettings']['analytics']['skip_zero_values'] = FALSE;
      $form['#attached']['drupalSettings']['analytics']['language'] = $this
        ->currentUser()
        ->getPreferredLangcode();
      $form['#attached']['drupalSettings']['analytics']['chart_container'] = $form['chart']['#attributes']['id'];

      // Visualization options for Google Charts draw() function,
      // must be JSON encoded before passing.
      // @see: https://developers.google.com/chart/interactive/docs/gallery/linechart#configuration-options
      $visualization_options = [
        'width' => '100%',
        'legend' => 'none',
        'interpolateNulls' => 'true',
        'hAxis' => [
          'viewWindow' => [
            'min' => !empty($analytics['TimeUnit']) ? min($analytics['TimeUnit']) : 0,
            'max' => !empty($analytics['TimeUnit']) ? max($analytics['TimeUnit']) : 0,
          ],
          'gridlines' => [
            'count' => -1,
            'units' => [
              'days' => [
                'format' => [
                  'MMM dd',
                ],
              ],
              'hours' => [
                'format' => [
                  'HH:mm',
                  'ha',
                ],
              ],
            ],
          ],
          'minorGridlines' => [
            'units' => [
              'hours' => [
                'format' => [
                  'hh:mm:ss a',
                  'ha',
                ],
              ],
            ],
          ],
        ],
      ];
      $form['#attached']['drupalSettings']['analytics']['visualization_options'] = json_encode($visualization_options);
      $visualization_options_to_date = [
        'hAxis.viewWindow.min',
        'hAxis.viewWindow.max',
      ];
      $form['#attached']['drupalSettings']['analytics']['visualization_options_to_date'] = $visualization_options_to_date;
    }
    else {
      $form['chart']['#attributes']['class'][] = 'chart-container-no-data';
      $form['chart']['no_data_text'] = [
        '#markup' => $this
          ->t('No performance data is available for the criteria you supplied.'),
      ];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $options = [
      'query' => [
        'metric' => $form_state
          ->getValue('metrics'),
        'since' => $form_state
          ->getValue('since')
          ->getTimeStamp(),
        'until' => $form_state
          ->getValue('until')
          ->getTimeStamp(),
        'environment' => $form_state
          ->getValue('environment'),
      ],
    ];
    $form_state
      ->setRedirect('<current>', [], $options);
  }

  /**
   * Retrieves the app analytics for the given criteria.
   *
   * @param \Drupal\apigee_edge\Entity\AppInterface $app
   *   The app entity that analytics data gets displayed.
   * @param string $metric
   *   The filter parameter.
   * @param string $since
   *   The start date parameter.
   * @param string $until
   *   The end date parameter.
   * @param string $environment
   *   The analytics environment to query.
   *
   * @return array
   *   The raw analytics API response for the given criteria.
   *
   * @throws \Moment\MomentException
   *   If provided date values are invalid.
   * @throws \Apigee\Edge\Exception\ApiException
   *   If analytics query fails.
   */
  protected final function getAnalytics(AppInterface $app, string $metric, string $since, string $until, string $environment) : array {
    $stats_controller = new StatsController($environment, $this->connector
      ->getOrganization(), $this->connector
      ->getClient());
    $stats_query = new StatsQuery([
      $metric,
    ], new Period(new \DateTimeImmutable('@' . $since), new \DateTimeImmutable('@' . $until)));
    $stats_query
      ->setFilter("({$this->getAnalyticsFilterCriteriaByAppOwner($app)} and developer_app eq '{$app->getName()}')")
      ->setTimeUnit('hour');
    return $stats_controller
      ->getOptimizedMetricsByDimensions([
      'apps',
    ], $stats_query);
  }

  /**
   * Returns the analytics filter criteria that limits the result by app owner.
   *
   * @param \Drupal\apigee_edge\Entity\AppInterface $app
   *   The app entity.
   *
   * @return string
   *   The analytics filter criteria for the app owner.
   *
   * @see getAnalytics()
   */
  protected abstract function getAnalyticsFilterCriteriaByAppOwner(AppInterface $app) : string;

}

Members

Namesort descending Modifiers Type Description Overrides
AppAnalyticsFormBase::$connector protected property The SDK connector service.
AppAnalyticsFormBase::$entityTypeManager protected property The entity type manager.
AppAnalyticsFormBase::$store protected property The PrivateTempStore factory.
AppAnalyticsFormBase::$urlGenerator protected property The URL generator. Overrides UrlGeneratorTrait::$urlGenerator
AppAnalyticsFormBase::buildForm public function Form constructor. Overrides FormInterface::buildForm 1
AppAnalyticsFormBase::create public static function Instantiates a new instance of this class. Overrides FormBase::create
AppAnalyticsFormBase::generateResponse protected function Requests analytics data and pass to the JavaScript chart drawing function.
AppAnalyticsFormBase::getAnalytics final protected function Retrieves the app analytics for the given criteria.
AppAnalyticsFormBase::getAnalyticsFilterCriteriaByAppOwner abstract protected function Returns the analytics filter criteria that limits the result by app owner. 2
AppAnalyticsFormBase::submitForm public function Form submission handler. Overrides FormInterface::submitForm
AppAnalyticsFormBase::validateForm public function Form validation handler. Overrides FormBase::validateForm
AppAnalyticsFormBase::validateQueryString protected function Validates the URL query string parameters.
AppAnalyticsFormBase::__construct public function Constructs a new AppAnalyticsFormBase.
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
FormBase::$configFactory protected property The config factory. 1
FormBase::$requestStack protected property The request stack. 1
FormBase::$routeMatch protected property The route match.
FormBase::config protected function Retrieves a configuration object.
FormBase::configFactory protected function Gets the config factory for this form. 1
FormBase::container private function Returns the service container.
FormBase::currentUser protected function Gets the current user.
FormBase::getRequest protected function Gets the request object.
FormBase::getRouteMatch protected function Gets the route match.
FormBase::logger protected function Gets the logger for a specific channel.
FormBase::redirect protected function Returns a redirect response object for the specified route. Overrides UrlGeneratorTrait::redirect
FormBase::resetConfigFactory public function Resets the configuration factory.
FormBase::setConfigFactory public function Sets the config factory for this form.
FormBase::setRequestStack public function Sets the request stack object to use.
FormInterface::getFormId public function Returns a unique string identifying the form. 236
LinkGeneratorTrait::$linkGenerator protected property The link generator. 1
LinkGeneratorTrait::getLinkGenerator Deprecated protected function Returns the link generator.
LinkGeneratorTrait::l Deprecated protected function Renders a link to a route given a route name and its parameters.
LinkGeneratorTrait::setLinkGenerator Deprecated public function Sets the link generator service.
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
RedirectDestinationTrait::$redirectDestination protected property The redirect destination service. 1
RedirectDestinationTrait::getDestinationArray protected function Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url.
RedirectDestinationTrait::getRedirectDestination protected function Returns the redirect destination service.
RedirectDestinationTrait::setRedirectDestination public function Sets the redirect destination service.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
UrlGeneratorTrait::getUrlGenerator Deprecated protected function Returns the URL generator service.
UrlGeneratorTrait::setUrlGenerator Deprecated public function Sets the URL generator service.
UrlGeneratorTrait::url Deprecated protected function Generates a URL or path for a specific route based on the given parameters.