diff --git a/modules/apigee_edge_teams/apigee_edge_teams.links.task.yml b/modules/apigee_edge_teams/apigee_edge_teams.links.task.yml
index f06a1608..72c8815a 100644
--- a/modules/apigee_edge_teams/apigee_edge_teams.links.task.yml
+++ b/modules/apigee_edge_teams/apigee_edge_teams.links.task.yml
@@ -106,3 +106,8 @@ apigee_edge_teams.team_app.analytics:
title: 'Analytics'
base_route: entity.team_app.canonical
weight: -1
+
+apigee_edge_teams.settings.team_member.sync:
+ route_name: apigee_edge_teams.settings.team_member.sync
+ title: 'Sync'
+ base_route: apigee_edge_teams.settings.team
diff --git a/modules/apigee_edge_teams/apigee_edge_teams.routing.yml b/modules/apigee_edge_teams/apigee_edge_teams.routing.yml
index 26ac0555..ecfa0df3 100644
--- a/modules/apigee_edge_teams/apigee_edge_teams.routing.yml
+++ b/modules/apigee_edge_teams/apigee_edge_teams.routing.yml
@@ -71,3 +71,27 @@ apigee_edge_teams.settings.team_app.cache:
_title: 'Caching'
requirements:
_permission: 'administer team'
+
+apigee_edge_teams.settings.team_member.sync:
+ path: '/admin/config/apigee-edge/app-settings/team-settings/sync'
+ defaults:
+ _form: '\Drupal\apigee_edge_teams\Form\TeamMemberSyncForm'
+ _title: 'Team Member Synchronization'
+ requirements:
+ _permission: 'administer team'
+
+apigee_edge_teams.team_member.run:
+ path: '/admin/config/apigee-edge/app-settings/team-settings/sync/run'
+ defaults:
+ _controller: '\Drupal\apigee_edge_teams\Controller\TeamMemberSyncController::run'
+ requirements:
+ _permission: 'administer team'
+ _csrf_token: 'TRUE'
+
+apigee_edge_teams.team_member.schedule:
+ path: '/admin/config/apigee-edge/app-settings/team-settings/sync/schedule'
+ defaults:
+ _controller: '\Drupal\apigee_edge_teams\Controller\TeamMemberSyncController::schedule'
+ requirements:
+ _permission: 'administer team'
+ _csrf_token: 'TRUE'
diff --git a/modules/apigee_edge_teams/apigee_edge_teams.services.yml b/modules/apigee_edge_teams/apigee_edge_teams.services.yml
index 6b304a35..5246435e 100644
--- a/modules/apigee_edge_teams/apigee_edge_teams.services.yml
+++ b/modules/apigee_edge_teams/apigee_edge_teams.services.yml
@@ -130,3 +130,7 @@ services:
arguments: ['@entity_type.manager', '@logger.channel.apigee_edge_teams']
tags:
- { name: paramconverter }
+
+ apigee_edge_teams.cli:
+ class: Drupal\apigee_edge_teams\CliService
+ arguments: ['@apigee_edge.apigee_edge_mgmt_cli_service']
diff --git a/modules/apigee_edge_teams/composer.json b/modules/apigee_edge_teams/composer.json
index 50d6e9db..ca2e104f 100644
--- a/modules/apigee_edge_teams/composer.json
+++ b/modules/apigee_edge_teams/composer.json
@@ -11,5 +11,12 @@
"sort-packages": true
},
"minimum-stability": "dev",
- "prefer-stable": true
+ "prefer-stable": true,
+ "extra": {
+ "drush": {
+ "services": {
+ "drush.services.yml": "^9"
+ }
+ }
+ }
}
diff --git a/modules/apigee_edge_teams/drush.services.yml b/modules/apigee_edge_teams/drush.services.yml
new file mode 100644
index 00000000..60f1a3f7
--- /dev/null
+++ b/modules/apigee_edge_teams/drush.services.yml
@@ -0,0 +1,6 @@
+services:
+ apigee_edge_teams.commands:
+ class: \Drupal\apigee_edge_teams\Commands\ApigeeEdgeCommands
+ arguments: ['@apigee_edge_teams.cli', '@apigee_edge.apigee_edge_mgmt_cli_service']
+ tags:
+ - { name: drush.command }
diff --git a/modules/apigee_edge_teams/src/CliService.php b/modules/apigee_edge_teams/src/CliService.php
new file mode 100644
index 00000000..3f5a8d5c
--- /dev/null
+++ b/modules/apigee_edge_teams/src/CliService.php
@@ -0,0 +1,73 @@
+apigeeEdgeManagementCliService = $apigeeEdgeManagementCliService;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sync(StyleInterface $io, callable $t) {
+ $io->title($t('Team Member synchronization'));
+ $batch = TeamMemberSyncController::getBatch();
+ $last_message = '';
+
+ foreach ($batch['operations'] as $operation) {
+ $context = [
+ 'finished' => 0,
+ ];
+
+ while ($context['finished'] < 1) {
+ call_user_func_array($operation[0], array_merge($operation[1], [&$context]));
+ if (isset($context['message']) && $context['message'] !== $last_message) {
+ $io->text($t($context['message']));
+ }
+ $last_message = $context['message'];
+
+ gc_collect_cycles();
+ }
+ }
+ }
+
+}
diff --git a/modules/apigee_edge_teams/src/CliServiceInterface.php b/modules/apigee_edge_teams/src/CliServiceInterface.php
new file mode 100644
index 00000000..9f4c4d0c
--- /dev/null
+++ b/modules/apigee_edge_teams/src/CliServiceInterface.php
@@ -0,0 +1,39 @@
+cliService = $cli_service;
+ }
+
+ /**
+ * Team Member synchronization.
+ *
+ * @command apigee-edge-teams:sync
+ *
+ * @usage drush apigee-edge-teams:sync
+ * Starts the team member synchronization.
+ */
+ public function sync() {
+ $this->cliService->sync($this->io(), 'dt');
+ }
+
+}
diff --git a/modules/apigee_edge_teams/src/Controller/TeamMemberSyncController.php b/modules/apigee_edge_teams/src/Controller/TeamMemberSyncController.php
new file mode 100644
index 00000000..948d8222
--- /dev/null
+++ b/modules/apigee_edge_teams/src/Controller/TeamMemberSyncController.php
@@ -0,0 +1,209 @@
+executor = $executor;
+ $this->messenger = $messenger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('apigee_edge.job_executor'),
+ $container->get('messenger')
+ );
+ }
+
+ /**
+ * Generates a job tag.
+ *
+ * @param string $type
+ * Tag type.
+ *
+ * @return string
+ * Job tag.
+ */
+ protected static function generateTag(string $type): string {
+ return "team_member_sync_{$type}_" . \Drupal::service('password_generator')->generate();
+ }
+
+ /**
+ * Returns the team member sync filter.
+ *
+ * @return null|string
+ * Filter condition or null if not set.
+ */
+ protected static function getFilter(): ?string {
+ return ((string) \Drupal::config('apigee_edge.sync')->get('filter')) ?: NULL;
+ }
+
+ /**
+ * Handler for 'apigee_edge_teams.team_member.schedule'.
+ *
+ * Runs a team member sync in the background.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The HTTP request.
+ *
+ * @return \Symfony\Component\HttpFoundation\RedirectResponse
+ * HTTP response doing a redirect.
+ */
+ public function schedule(Request $request): RedirectResponse {
+ $destination = $request->query->get('destination');
+
+ $job = new TeamMemberSync(static::getFilter());
+ $job->setTag($this->generateTag('background'));
+ apigee_edge_get_executor()->cast($job);
+
+ $this->messenger()->addStatus($this->t('Team Member synchronization is scheduled.'));
+
+ return new RedirectResponse($destination);
+ }
+
+ /**
+ * Handler for 'apigee_edge_teams.team_member.run'.
+ *
+ * Starts the team member sync batch process.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The HTTP request.
+ *
+ * @return \Symfony\Component\HttpFoundation\RedirectResponse
+ * HTTP response doing a redirect.
+ */
+ public function run(Request $request): RedirectResponse {
+ $destination = $request->query->get('destination');
+ $batch = static::getBatch();
+ batch_set($batch);
+ return batch_process($destination);
+ }
+
+ /**
+ * Gets the batch array.
+ *
+ * @return array
+ * The batch array.
+ */
+ public static function getBatch(): array {
+ $tag = static::generateTag('batch');
+
+ return [
+ 'title' => t('Synchronizing Team Member'),
+ 'operations' => [
+ [[static::class, 'batchGenerateJobs'], [$tag]],
+ [[static::class, 'batchExecuteJobs'], [$tag]],
+ ],
+ 'finished' => [static::class, 'batchFinished'],
+ ];
+ }
+
+ /**
+ * The first batch operation.
+ *
+ * This generates the team member sync jobs for the second operation.
+ *
+ * @param string $tag
+ * Job tag.
+ * @param array $context
+ * Batch context.
+ */
+ public static function batchGenerateJobs(string $tag, array &$context) {
+ $job = new TeamMemberSync(static::getFilter());
+ $job->setTag($tag);
+ apigee_edge_get_executor()->call($job);
+
+ $context['message'] = (string) $job;
+ $context['finished'] = 1.0;
+ }
+
+ /**
+ * The second batch operation.
+ *
+ * @param string $tag
+ * Job tag.
+ * @param array $context
+ * Batch context.
+ */
+ public static function batchExecuteJobs(string $tag, array &$context) {
+ if (!isset($context['sandbox'])) {
+ $context['sandbox'] = [];
+ }
+
+ $executor = apigee_edge_get_executor();
+ $job = $executor->select($tag);
+
+ if ($job === NULL) {
+ $context['finished'] = 1.0;
+ return;
+ }
+
+ $executor->call($job);
+
+ $context['message'] = (string) $job;
+ $context['finished'] = $executor->countJobs($tag, [Job::FAILED, Job::FINISHED]) / $executor->countJobs($tag);
+ }
+
+ /**
+ * Batch finish callback.
+ */
+ public static function batchFinished() {
+ \Drupal::messenger()->addStatus(t('Team members are synced in Drupal'));
+ }
+
+}
diff --git a/modules/apigee_edge_teams/src/Entity/TeamAccessHandler.php b/modules/apigee_edge_teams/src/Entity/TeamAccessHandler.php
index 82028d3b..f51335a7 100644
--- a/modules/apigee_edge_teams/src/Entity/TeamAccessHandler.php
+++ b/modules/apigee_edge_teams/src/Entity/TeamAccessHandler.php
@@ -108,7 +108,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
// Check if current developer is a member of the team and has the permision
// to view more than 100 teams.
// Should not run for developer with less than 100 teams.
- if ($account->hasPermission('view extensive team list') && (count($developer_team_ids) > 100)) {
+ if ($account->hasPermission('view extensive team list') && (count($developer_team_ids) >= 100)) {
$team_members = $this->teamMembershipManager->getMembers($entity->id());
if (in_array($account->getEmail(), $team_members)) {
$developer_team_access = TRUE;
diff --git a/modules/apigee_edge_teams/src/Form/TeamMemberSyncForm.php b/modules/apigee_edge_teams/src/Form/TeamMemberSyncForm.php
new file mode 100644
index 00000000..33e99cb6
--- /dev/null
+++ b/modules/apigee_edge_teams/src/Form/TeamMemberSyncForm.php
@@ -0,0 +1,160 @@
+sdkConnector = $sdk_connector;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('apigee_edge.sdk_connector')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'apigee_edge_team_member_sync_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ try {
+ $this->sdkConnector->testConnection();
+ }
+ catch (\Exception $exception) {
+ $this->messenger()->addError($this->t('Cannot connect to Apigee Edge server. Please ensure that Apigee Edge connection settings are correct.', [
+ ':link' => Url::fromRoute('apigee_edge.settings')->toString(),
+ ]));
+ return $form;
+ }
+
+ $form['#attached']['library'][] = 'apigee_edge/apigee_edge.admin';
+
+ $form['sync'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Synchronize team members'),
+ '#open' => TRUE,
+ ];
+
+ $form['sync']['description'] = [
+ '#type' => 'container',
+ 'p1' => [
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#value' => $this->t('Team member synchronization will:'),
+ ],
+ 'list' => [
+ '#theme' => 'item_list',
+ '#items' => [
+ $this->t('Caches team members in Drupal'),
+ ],
+ ],
+ 'p2' => [
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#value' => $this->t('The "Run team member sync" button will sync the team members, displaying a progress bar on the screen while running. The "Background team member sync" button will run the team member sync process in batches each time cron runs and may take multiple cron runs to complete.', [':cron_url' => Url::fromRoute('system.cron_settings')->toString()]),
+ ],
+ 'p3' => [
+ '#type' => 'html_tag',
+ '#tag' => 'p',
+ '#value' => $this->t('By running the sync, team member detail is stored in members cache table and will have expiry that is set in team caching. To show more than 100 teams for a member enable permission "View extensive teams list". ', [':team_caching' => Url::fromRoute('apigee_edge_teams.settings.team.cache')->toString()]),
+ ],
+ ];
+
+ $form['sync']['sync_submit'] = [
+ '#title' => $this->t('Run team member sync'),
+ '#type' => 'link',
+ '#url' => $this->buildUrl('apigee_edge_teams.team_member.run'),
+ '#attributes' => [
+ 'class' => [
+ 'button',
+ 'button--primary',
+ ],
+ ],
+ ];
+ $form['sync']['background_team_member_sync_submit'] = [
+ '#title' => $this->t('Background team member sync'),
+ '#type' => 'link',
+ '#url' => $this->buildUrl('apigee_edge_teams.team_member.schedule'),
+ '#attributes' => [
+ 'class' => [
+ 'button',
+ ],
+ ],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ }
+
+ /**
+ * Build URL for team member sync processes, using CSRF protection.
+ *
+ * @param string $route_name
+ * The name of the route.
+ *
+ * @return \Drupal\Core\Url
+ * The URL to redirect to.
+ */
+ protected function buildUrl(string $route_name): Url {
+ $url = Url::fromRoute($route_name);
+ $token = \Drupal::csrfToken()->get($url->getInternalPath());
+ $url->setOptions(['query' => ['destination' => 'admin/config/apigee-edge/app-settings/team-settings/sync', 'token' => $token]]);
+ return $url;
+ }
+
+}
diff --git a/modules/apigee_edge_teams/src/Job/TeamMemberCreateUpdate.php b/modules/apigee_edge_teams/src/Job/TeamMemberCreateUpdate.php
new file mode 100644
index 00000000..29d33331
--- /dev/null
+++ b/modules/apigee_edge_teams/src/Job/TeamMemberCreateUpdate.php
@@ -0,0 +1,56 @@
+team_ids = $team_ids;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function executeRequest() {
+ $member_controller = \Drupal::service('apigee_edge_teams.team_membership_manager');
+ $team_members = $member_controller->getMembers($this->team_ids);
+ }
+
+}
diff --git a/modules/apigee_edge_teams/src/Job/TeamMemberSync.php b/modules/apigee_edge_teams/src/Job/TeamMemberSync.php
new file mode 100644
index 00000000..24bab27f
--- /dev/null
+++ b/modules/apigee_edge_teams/src/Job/TeamMemberSync.php
@@ -0,0 +1,79 @@
+filter = $filter;
+ }
+
+ /**
+ * Executes the request itself.
+ */
+ protected function executeRequest(){}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute(): bool {
+ parent::execute();
+
+ $team_ids = array_keys(\Drupal::entityTypeManager()->getStorage('team')->loadMultiple());
+
+ foreach ($team_ids as $team_name) {
+ $update_team_member_job = new TeamMemberUpdate($team_name);
+ $update_team_member_job->setTag($this->getTag());
+ $this->scheduleJob($update_team_member_job);
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString(): string {
+ return t('Synchronizing Team Members in Drupal.')->render();
+ }
+
+}
diff --git a/modules/apigee_edge_teams/src/Job/TeamMemberUpdate.php b/modules/apigee_edge_teams/src/Job/TeamMemberUpdate.php
new file mode 100644
index 00000000..c7547924
--- /dev/null
+++ b/modules/apigee_edge_teams/src/Job/TeamMemberUpdate.php
@@ -0,0 +1,36 @@
+ $this->team_ids,
+ ])->render();
+ }
+
+}
diff --git a/modules/apigee_edge_teams/src/TeamPermissionHandler.php b/modules/apigee_edge_teams/src/TeamPermissionHandler.php
index 08dbcd57..f963fe0c 100644
--- a/modules/apigee_edge_teams/src/TeamPermissionHandler.php
+++ b/modules/apigee_edge_teams/src/TeamPermissionHandler.php
@@ -164,7 +164,7 @@ public function getDeveloperPermissionsByTeam(TeamInterface $team, AccountInterf
else {
// Check if current developer is a member of the team and has the permision
// to view more than 100 teams.
- if ($account->hasPermission('view extensive team list') && (count($developer_team_ids) > 100)) {
+ if ($account->hasPermission('view extensive team list') && (count($developer_team_ids) >= 100)) {
$team_members = $this->teamMembershipManager->getMembers($team->id());
if (in_array($account->getEmail(), $team_members)) {
$developer_team_access = TRUE;