Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Remove discover] Implement embeddable dashboard on Server Management / Cluster module #6532

Closed
22 tasks done
Tracked by #6477
asteriscos opened this issue Mar 19, 2024 · 8 comments · Fixed by #6561
Closed
22 tasks done
Tracked by #6477
Assignees
Labels
level/subtask Subtask issue type/enhancement Enhancement issue

Comments

@asteriscos
Copy link
Member

asteriscos commented Mar 19, 2024

Description

We have to implement the embeddable dashboard on Server Management / Cluster and deprecate any use of kibana-integrations components.

Warning

The embeddable panel id must be unique including general and agents visualizations. Otherwise, the visualizations will not refresh when we pin an agent, because they are cached by id

Warning

#6549 depends on this task

Current Cluster screens

Screen on cluster mode

image

image

image

image

Tasks

  • Implement the embeddable dashboard on Server Management / Cluster
    • Migrate visualizations to embeddable panels
    • Add new searchbar
    • Add conditionality to show the dashboard if there are results.
    • Add loadings
    • Add message that there are no results
    • Keep the "The cluster is disabled" message when in Manager mode
    • Change back button on Overview page (desirable)
    • Change index pattern selection mechanism (Dependence: [Remove discover] Change index pattern selector mechanism #6434)
    • Once the previous point have been applied, corroborate filter behavior with respect to hide alerts, allow agents and filter order (Implicit filters first, then pinned agent, if applicable, and then normal filters).
    • Clean obsolete code
  • Check the following:
    • Each visualization, if applicable, must have interaction so that it sets the date range of the searchbar when clicked and visualizations should refresh when a date range is selected in the search bar.
    • If there are no results, the corresponding message must appear that there are no results and the visualizations should not be rendered.
    • Check that the message "The cluster is disabled" appears when in the cluster is not enabled.
      NOTE: To test this it is necessary to change the cluster configuration in the imposter by setting the enabled option to no in the docker/imposter/cluster/cluster_status.json file and restart the imposter.
    • Check that the message "The cluster is not running" appears when in Manager mode
    • Check filter behavior with respect to hide alerts and allow agents.
    • Check the link to the list of agents on the Information card for both the name (Agents) and the value.
    • Check that the corresponding information is displayed when you click on "View Overview" and that you can go back from that view.
    • Check that when interacting with the Overview visualization (going to "View Overview") the corresponding filter is added.
    • Check that the list of nodes is shown when you click on both Nodes and the value and that you can go back from that view.
    • Check that fixed filters cannot be deleted or have the onClick functionality

Source task

@jbiset
Copy link
Member

jbiset commented Mar 20, 2024

Update 20/03/2024

The Server Management -> Cluster screen is different from the rest of the dashboards that are being migrated, since the sections of the screen are inserted with Angular through the management.html template. Therefore, we are analyzing migrating everything possible to React components without affecting other screens, preparing the context when replacing Angular. This is necessary not only for the replacement of the visualizations, but also for the replacement of the searchbar

Files involved:

  • plugins/main/public/services/routes.js
  • plugins/main/public/templates/management/management.html
  • plugins/main/public/components/index.js
  • plugins/main/public/components/management/cluster/cluster-visualization.js
  • plugins/main/public/components/management/cluster/cluster-disabled.js

@jbiset
Copy link
Member

jbiset commented Mar 21, 2024

Update 21/03/2024

Given the need to componentize the Cluster screen with React components. It was decided to modularize all parts of the Cluster screen in the management.html template and reduce it to a minimum. In this way, not only is any use of kibana-integrations components deprecated, but also partial progress is made with the elimination of Angular.

TO CONTINUE:

  • Reduce everything related to the Cluster screen in management.html to a minimum. (Worked on this task)
  • Migrate Cluster sections to React components
  • Implement the embeddable dashboard on Server Management / Cluster

@jbiset
Copy link
Member

jbiset commented Mar 22, 2024

Update 22/03/2024 - 25/03/2024 - 26/03/2024

Work was done on reducing to a minimum everything related to the Cluster screen in management.html.

MIGRATE CLUSTER MODULE TO REACT COMPONENT

BREADCRUMBS
<div
    layout="row"
    layout-align="start center"
    ng-if="mctrl.tab !== 'monitoring' && mctrl.tab !== 'configuration'"
  >
    <!-- Breadcrumbs -->
    <div
      layout="row"
      layout-padding
      ng-if="mctrl.tab === 'groups' && mctrl.currentGroup && mctrl.currentGroup.name"
    >
      <span
        class="wz-text-link cursor-pointer"
        ng-click="mctrl.switchTab('groups', true)"
        >{{ mctrl.tabNames[mctrl.tab] }}</span
      >
      <span> / {{ mctrl.currentGroup.name }} </span>
    </div>
    <!-- End breadcrumbs -->
  </div>

It does not affect Cluster as it is only rendered if mctrl.tab !== 'monitoring' && mctrl.tab !== 'configuration'". However, the HOC is added withGlobalBreadcrumb([{ text: cluster.breadcrumbLabel }])

CLUSTER DISABLED MESSAGE WHEN NOT AUTHORIZED
{/* Cluster disabled or not running */}
<div ng-if='!authorized'>
	<react-component
	name='WzEmptyPromptNoPermissions'
	props='{permissions}'
	></react-component>
</div>

Replaced with the HOC withUserAuthorizationPrompt([ { action: 'cluster:status', resource: '*:*:*' }, ])

CLUSTER DISABLED MESSAGE WHEN NOT ENABLED AND NOT RUNNING CLUSTER
{/* Cluster disabled or not running */}
<div ng-if='!isClusterEnabled || !isClusterRunning'>
	<react-component
	name='ClusterDisabled'
	props='{enabled: isClusterEnabled, running: isClusterRunning}'
	></react-component>
</div>

It is replaced with the corresponding component and the necessary resources of the cluster controller are migrated.

DISCOVER SEARCH BAR SECTION
<!-- Cluster enabled -->
    <div
      ng-show="isClusterEnabled && isClusterRunning && authorized"
      ng-if="mctrl.tab !== 'monitoring'"
      class="monitoring-discover"
    >
      <div ng-show="loading" style="padding: 16px">
        <react-component
          name="EuiProgress"
          props="{size: 'xs', color: 'primary'}"
        ></react-component>
      </div>

      <!-- Discover search bar section -->
      <kbn-dis
        class="hide-filter-control"
        ng-show="!loading && (!showNodes || currentNode)"
      ></kbn-dis>
      <!-- End Discover search bar section -->

      <!-- Loading status section -->
      <div
        class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceAround euiFlexGroup--directionRow euiFlexGroup--responsive"
      >
        <div
          class="euiFlexItem euiFlexItem--flexGrowZero euiTextAlign euiTextAlign--center"
          ng-show="!loading && !rendered && resultState === 'ready' && (!showNodes || (showNodes && currentNode))"
        >
          <span class="euiLoadingChart euiLoadingChart--large">
            <span class="euiLoadingChart__bar"></span>
            <span class="euiLoadingChart__bar"></span>
            <span class="euiLoadingChart__bar"></span>
            <span class="euiLoadingChart__bar"></span>
          </span>
          <div class="euiSpacer euiSpacer--m"></div>
          <div class="percentage">{{loadingStatus}}</div>
        </div>
      </div>
      <!-- End loading status section -->
    </div>

It is replaced by the new searchbar and ng-if is added to the management template so that it is not rendered in the tab monitoring (cluster)

CLUSTER MAIN SECTION
    <!-- main -->
    <div
      class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--responsive"
      ng-if="!loading && !showConfig && !showNodes && isClusterEnabled && isClusterRunning && authorized"
    >
      <!-- TODO - migrate to a React Component-->

      <!-- Overview card -->
      <div class="euiFlexItem">
        <div class="euiPanel euiPanel--paddingMedium">
          <span class="euiTitle euiTitle--small euiCard__title"
            >Details&nbsp;
            <button
              ng-click="goConfiguration()"
              class="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"
              type="button"
            >
              <span class="euiButtonEmpty__content"
                ><i class="fa fa-fw fa-pie-chart"></i
                ><span class="euiButtonEmpty__text">View Overview</span>
              </span>
            </button>
          </span>
          <div class="euiSpacer euiSpacer--m"></div>
          <div layout="row" class="wz-padding-top-10">
            <span flex="25">IP address</span>
            <span class="color-grey">{{configuration.nodes[0] || '-'}}</span>
          </div>
          <div layout="row" class="wz-padding-top-10">
            <span flex="25">Running</span>
            <span class="color-grey">{{ status || 'no' }}</span>
          </div>
          <div layout="row" class="wz-padding-top-10">
            <span flex="25">Version</span>
            <span class="color-grey">{{version}}</span>
          </div>
        </div>
      </div>
      <!-- End overview card -->

      <!-- Info card -->
      <div class="euiFlexItem">
        <div class="euiPanel euiPanel--paddingMedium">
          <span class="euiTitle euiTitle--small euiCard__title"
            >Information</span
          >
          <div class="euiSpacer euiSpacer--m"></div>
          <!-- Nodes -->
          <div
            layout="row"
            class="wz-padding-top-10 cursor-pointer"
            ng-click="goNodes()"
          >
            <span
              flex="25"
              class="wz-text-link"
              tooltip="Click to open the list of nodes"
              tooltip-placement="left"
              >Nodes</span
            >
            <span
              class="wz-text-link"
              tooltip="Click to open the list of nodes"
              tooltip-placement="right"
              >{{nodesCount}}</span
            >
          </div>
          <!-- Agents -->
          <div
            layout="row"
            class="wz-padding-top-10 cursor-pointer"
            ng-click="goAgents()"
          >
            <span
              flex="25"
              class="wz-text-link"
              tooltip="Click to open the list of agents"
              tooltip-placement="left"
              >Agents</span
            >
            <span
              class="wz-text-link"
              tooltip="Click to open the list of agents"
              tooltip-placement="right"
              >{{agentsCount}}</span
            >
          </div>
        </div>
      </div>
      <!-- End info card -->
    </div>
    <!-- end main -->

The elements are migrated to the new ClusterOverview class component and the necessary resources of the Cluster controller

THE PLACE FROM WHICH CLUSTER OVERVIEW IS RENDERER IS CHANGED

Initially, the rendering was carried out through the template management.html with the following code:

 <react-component
    name="ClusterOverview"
    props="mctrl.managementProps"
  ></react-component>

However, to avoid conflicts with other tasks and directly eliminate the use of the management.html template, the rendering is passed to the management-main.js component like other already migrated components, for example: statistics, logs, configuration, groups.

NODES DETAIL

There is a currently unused section that shows the details of a node. The functionality that selects the node does not currently exist, making this section obsolete:

<!-- nodes-detail -->
    <div ng-show="currentNode">
      <!-- Back button -->
      <div
        class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--responsive"
      >
        <div class="euiFlexItem euiFlexItem--flexGrowZero">
          <h2 class="euiTitle euiTitle--medium">
            <md-button
              class="md-icon-button md-icon-button-back wz-padding-right-16 btn btn-info"
              aria-label="Back"
              tooltip="Go back"
              tooltip-placement="bottom"
              ng-click="goNodes()"
              ><i class="fa fa-fw fa-arrow-left" aria-hidden="true"></i
            ></md-button>
            Node {{ currentNode.name }}
          </h2>
        </div>
      </div>
      <!-- Node alerts summary card -->
      <div
        class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--responsive"
      >
        <div
          class="height-250 euiPanel euiFlexItem"
          ng-class="{'no-opacity': resultState !== 'ready' || !rendered}"
          ng-show="!loading && currentNode && !showConfig"
        >
          <md-card-content class="wazuh-column">
            <span
              class="embPanel__header embPanel__title embPanel__dragger layout-row wz-headline-title"
              >{{ currentNode.name }} alerts summary</span
            >
            <react-component
              style="height: 100%"
              name="KibanaVisualization"
              props="{visId: 'Wazuh-App-Cluster-monitoring-Overview-Node', tab: 'monitoring'}"
            ></react-component>
          </md-card-content>
        </div>
      </div>
      <!-- End node alerts summary card -->

      <!-- Node info and files cards section -->
      <div
        class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--responsive"
        ng-show="currentNode && currentNode.healthCheck && currentNode.healthCheck"
      >
        <!-- Node information card -->
        <div
          class="euiFlexItem"
          ng-show="currentNode && currentNode.healthCheck && currentNode.healthCheck.info"
        >
          <div class="euiPanel euiPanel--paddingMedium">
            <span class="euiTitle euiTitle--small euiCard__title"
              >Node information</span
            >
            <div class="euiSpacer euiSpacer--m"></div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">IP</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.info.ip}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Version</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.info.version}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Type</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.info.type}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Name</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.info.name}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Active agents</span>
              <span class="color-grey"
                >{{ currentNode.healthCheck.info.n_active_agents }}</span
              >
            </div>
          </div>
        </div>

        <!-- Last files sync card -->
        <div
          class="euiFlexItem"
          ng-show="currentNode && currentNode.healthCheck && currentNode.healthCheck.status"
        >
          <div class="euiPanel euiPanel--paddingMedium">
            <span class="euiTitle euiTitle--small euiCard__title"
              >Last files integrity synchronization</span
            >
            <div class="euiSpacer euiSpacer--m"></div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Last sync</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_integrity.date_end_master}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Duration</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_integrity.duration}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Total shared files</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_integrity.total_files.shared}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Total missing files</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_integrity.total_files.missing}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Total extra but valid files</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_integrity.total_files.extra_valid}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Total extra files</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_integrity.total_files.extra}}</span
              >
            </div>
          </div>
        </div>
      </div>
      <!-- End node info and files cards section -->

      <!-- Node agents cards section -->
      <div
        class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--responsive"
        ng-show="currentNode && currentNode.healthCheck && currentNode.healthCheck.status"
      >
        <!-- Agents sync card -->
        <div class="euiFlexItem">
          <div class="euiPanel euiPanel--paddingMedium">
            <span class="euiTitle euiTitle--small euiCard__title"
              >Last agents information synchronization</span
            >
            <div class="euiSpacer euiSpacer--m"></div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Last sync</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_agentinfo.date_end_master}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Duration</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_agentinfo.duration}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Total agent info</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_agentinfo.total_agentinfo}}</span
              >
            </div>
          </div>
        </div>

        <!-- Groups sync card -->
        <div class="euiFlexItem">
          <div class="euiPanel euiPanel--paddingMedium">
            <span class="euiTitle euiTitle--small euiCard__title"
              >Last agents groups synchronization</span
            >
            <div class="euiSpacer euiSpacer--m"></div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Last sync</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_agentgroups.date_end_master}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Duration</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_agentgroups.duration}}</span
              >
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="30">Total agent info</span>
              <span class="color-grey"
                >{{currentNode.healthCheck.status.last_sync_agentgroups.total_agentgroups}}</span
              >
            </div>
          </div>
        </div>
      </div>
      <!-- End node agents cards section -->
    </div>
    <!-- end nodes-detail -->

For now, since this information view for a selected node has not existed since at least version 4.4.0, it is not migrated. But it is left to consideration to analyze this use case.

ADDED TIMELION VISUALIZATIONS SECTION WITH EMBEDDABLES
<!-- Monitoring Timelion visualizations section -->
    <div
      class="height-400"
      ng-class="{'no-opacity': resultState === 'none' || !rendered}"
      ng-show="!loading && !showConfig && !showNodes && rendered"
    >
      <react-component name="ClusterTimelions" props="{}"></react-component>
    </div>

The ClusterTimelions component is added to the ClusterOverview, using DashboardByRenderer. Replacing the functionality of the previous code.

CONFIGURATION SECTION
    <!--  configuration -->
    <!-- end configuration -->
    <!-- Cards for overview monitoring section -->
    <div ng-show="showConfig">
      <div
        class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--responsive"
      >
        <div class="euiFlexItem euiFlexItem--flexGrowZero">
          <h2 class="euiTitle euiTitle--medium">
            <md-button
              class="md-icon-button md-icon-button-back wz-padding-right-16 btn btn-info"
              aria-label="Back"
              tooltip="Go back"
              tooltip-placement="bottom"
              ng-click="goBack()"
              ><i class="fa fa-fw fa-arrow-left" aria-hidden="true"></i
            ></md-button>
            Overview
          </h2>
        </div>
      </div>

      <div
        class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--responsive"
      >
        <!-- Overview visualization card -->
        <div
          class="euiPanel euiFlexItem euiFlexItem--flexGrowZero"
          style="min-width: 500px; overflow: hidden"
        >
          <span
            class="euiLoadingChart custom-charts-bar euiLoadingChart--large euiTextAlign--center"
            style="
              top: 50%;
              transform: translate(0px, -25px);
              position: relative;
            "
            ng-if="!isReady || !rendered"
          >
            <span class="euiLoadingChart__bar"></span>
            <span class="euiLoadingChart__bar"></span>
            <span class="euiLoadingChart__bar"></span>
            <span class="euiLoadingChart__bar"></span>
          </span>
          <md-card-content
            class="wazuh-column"
            ng-class="{'no-opacity-overview-monitoring': !isReady || !rendered }"
          >
            <span
              class="embPanel__header embPanel__title embPanel__dragger layout-row wz-headline-title"
              >Top 5 nodes
            </span>
            <react-component
              style="height: 100%"
              name="KibanaVisualization"
              props="{visId: 'Wazuh-App-Cluster-monitoring-Overview-Node-Pie', tab: 'monitoring'}"
            ></react-component>
            <span class="wz-padding-top-10 wz-text-center" ng-show="!rendered"
              >There are no results for selected time range. Try another
              one.</span
            >
          </md-card-content>
        </div>
        <!-- Cluster configuration card -->
        <div class="euiFlexItem">
          <div class="euiPanel euiPanel--paddingMedium">
            <span class="euiTitle euiTitle--small euiCard__title"
              >Configuration</span
            >
            <div class="euiSpacer euiSpacer--m"></div>
            <!-- Configuration options -->
            <div layout="row" class="wz-padding-top-10">
              <span flex="25">Disabled</span>
              <span class="color-grey">{{configuration.disabled}}</span>
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="25">Hidden</span>
              <span class="color-grey">{{configuration.hidden}}</span>
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="25">Name</span>
              <span class="color-grey">{{configuration.name}}</span>
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="25">Node name</span>
              <span class="color-grey">{{configuration.node_name}}</span>
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="25">Node type</span>
              <span class="color-grey">{{configuration.node_type}}</span>
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="25">Bind address</span>
              <span class="color-grey">{{configuration.bind_addr}}</span>
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="25">IP</span>
              <span class="color-grey">{{configuration.nodes[0] || '-'}}</span>
            </div>
            <div layout="row" class="wz-padding-top-10">
              <span flex="25">Port</span>
              <span class="color-grey">{{configuration.port}}</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- End cards for overview monitoring section -->
    <!-- end configuration -->

Pass the configuration section to the ClusterOverview class component and then replace the corresponding tags with OpenSearch UI components

@jbiset
Copy link
Member

jbiset commented Mar 26, 2024

Update 26/03/2024

  • Migrated Timelions visualizations to embeddable panels
  • Replace html tags from template management.html with OpenSearch UI components
  • The structure is adjusted so that the order searchbar -> overview-cards -> Timelions visualizations is respected
  • Add loadings
  • Add message that there are no results

TO CONTINUE:

  • Check the definition of Alerts by node summary so that it shows all nodes
  • Complete View Overview rendering functionality

Current screen

image

@jbiset
Copy link
Member

jbiset commented Mar 27, 2024

Update 27/03/2024

  • View Overview rendering functionality is completed
  • Fixed definition of Timelions visualizations
  • Changed back button on Overview page
  • Added cluster nodes table rendering
  • Removed obsolete code from:
    • plugins/main/public/templates/management/management.html
    • plugins/main/public/controllers/management/index.js
    • plugins/main/public/components/index.js
  • Final aesthetic changes are made

Current Server Management -> Cluster

Evidence.webm

The task is blocked until the dependencies are resolved

@jbiset
Copy link
Member

jbiset commented Apr 8, 2024

Update 08/04/2024

  • Changed index pattern selection mechanism.
  • Corroborated filter behavior with respect to hide alerts, allow agents and filter order.
  • Changed DiscoverNoResults and Loading-Spinner common components.

@jbiset
Copy link
Member

jbiset commented Apr 16, 2024

Update 16/04/2024

  • Fixed configuration_cards itemsList
  • The code is improved according to suggestions to the PR
  • Unnecessary code is removed
  • Added condition so that the searchbar is not displayed on the node list screen

@jbiset
Copy link
Member

jbiset commented Apr 22, 2024

Update 22/04/2024

  • Discussed and resolved a visualization issue with Timeline visualizations when using SampleData.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
level/subtask Subtask issue type/enhancement Enhancement issue
Projects
Status: Done
2 participants