Repository: bigstack-oss/cube-cos-ui
Pull request #184: 127 tunings page
Author: login: steven-chiu-bigstack
Labels: name: done
description: Merge the pull request
Created: 2025-03-18T09:14:30Z | Merged: 2025-04-11T02:40:26Z

Close #127 

https://github.com/user-attachments/assets/55961080-c77b-4284-a82b-014df24a3f07

## Skeleton

https://github.com/user-attachments/assets/52b67556-0847-44d3-99ca-81272fff36fe


## Known Issues

- #193 
- Keyword search does not always return the expected results at the moment due to the API implementation. This will be handled in [another issue](https://github.com/bigstack-oss/cube-cos-api/issues/154).
- The `status.isUpdating` field does not always work as expected in the API.

Linked issue #127: Page - Tunings Page
[Mockup](https://www.figma.com/design/kRJ5MUAkMF0AYgXCeChtZn/COS-management-UI-Redesign-v.3?node-id=3679-26264&p=f&t=F5Qe2mEExXogz176-0)

Dependencies:

- #25 

Diff:
diff --git a/packages/cube-frontend-api/cube-cos-openapi b/packages/cube-frontend-api/cube-cos-openapi
index f47f09258..170dab385 160000
--- a/packages/cube-frontend-api/cube-cos-openapi
+++ b/packages/cube-frontend-api/cube-cos-openapi
@@ -1 +1 @@
-Subproject commit f47f092581bfce194d2edd6141fed313ffc758cc
+Subproject commit 170dab385a20bdf88821fea42691e5fb3c188d41
diff --git a/packages/cube-frontend-api/sdk/api.ts b/packages/cube-frontend-api/sdk/api.ts
index 401fa02b4..f0b6bd8b2 100644
--- a/packages/cube-frontend-api/sdk/api.ts
+++ b/packages/cube-frontend-api/sdk/api.ts
@@ -653,6 +653,37 @@ export interface GetAbstractedEventsResponseDataLimit {
      */
     'description': string;
 }
+/**
+ * 
+ * @export
+ * @interface GetCpuUsageHistoryOfHostResponse
+ */
+export interface GetCpuUsageHistoryOfHostResponse {
+    /**
+     * 
+     * @type {number}
+     * @memberof GetCpuUsageHistoryOfHostResponse
+     */
+    'code': number;
+    /**
+     * 
+     * @type {TimeValuePair}
+     * @memberof GetCpuUsageHistoryOfHostResponse
+     */
+    'data': TimeValuePair;
+    /**
+     * 
+     * @type {string}
+     * @memberof GetCpuUsageHistoryOfHostResponse
+     */
+    'msg': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof GetCpuUsageHistoryOfHostResponse
+     */
+    'status': string;
+}
 /**
  * 
  * @export
@@ -2592,6 +2623,37 @@ export interface GetMeResponseData {
      */
     'name': string;
 }
+/**
+ * 
+ * @export
+ * @interface GetMemoryUsageHistoryOfHostResponse
+ */
+export interface GetMemoryUsageHistoryOfHostResponse {
+    /**
+     * 
+     * @type {number}
+     * @memberof GetMemoryUsageHistoryOfHostResponse
+     */
+    'code': number;
+    /**
+     * 
+     * @type {TimeValuePair}
+     * @memberof GetMemoryUsageHistoryOfHostResponse
+     */
+    'data': TimeValuePair;
+    /**
+     * 
+     * @type {string}
+     * @memberof GetMemoryUsageHistoryOfHostResponse
+     */
+    'msg': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof GetMemoryUsageHistoryOfHostResponse
+     */
+    'status': string;
+}
 /**
  * 
  * @export
@@ -2716,6 +2778,37 @@ export interface GetMemoryUsageSummaryOfVmsResponse {
      */
     'status': string;
 }
+/**
+ * @type GetMetricByHostOrVm200Response
+ * @export
+ */
+export type GetMetricByHostOrVm200Response = GetCpuUsageHistoryOfHostResponse | GetMemoryUsageHistoryOfHostResponse;
+
+/**
+ * 
+ * @export
+ * @interface GetMetricByHostOrVm500Response
+ */
+export interface GetMetricByHostOrVm500Response {
+    /**
+     * 
+     * @type {number}
+     * @memberof GetMetricByHostOrVm500Response
+     */
+    'code'?: number;
+    /**
+     * 
+     * @type {string}
+     * @memberof GetMetricByHostOrVm500Response
+     */
+    'msg'?: string;
+    /**
+     * 
+     * @type {string}
+     * @memberof GetMetricByHostOrVm500Response
+     */
+    'status'?: string;
+}
 /**
  * @type GetMetricByTypes200Response
  * @export
@@ -4792,10 +4885,10 @@ export interface ListTuningResponseDataTuningsInner {
     'value': string;
     /**
      * 
-     * @type {Array<string>}
+     * @type {Array<ListTuningResponseDataTuningsInnerHostsInner>}
      * @memberof ListTuningResponseDataTuningsInner
      */
-    'hosts': Array<string>;
+    'hosts': Array<ListTuningResponseDataTuningsInnerHostsInner>;
     /**
      * 
      * @type {string}
@@ -4827,6 +4920,31 @@ export interface ListTuningResponseDataTuningsInner {
      */
     'status': ListTuningResponseDataTuningsInnerStatus;
 }
+/**
+ * 
+ * @export
+ * @interface ListTuningResponseDataTuningsInnerHostsInner
+ */
+export interface ListTuningResponseDataTuningsInnerHostsInner {
+    /**
+     * 
+     * @type {string}
+     * @memberof ListTuningResponseDataTuningsInnerHostsInner
+     */
+    'name': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof ListTuningResponseDataTuningsInnerHostsInner
+     */
+    'role': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof ListTuningResponseDataTuningsInnerHostsInner
+     */
+    'ip': string;
+}
 /**
  * 
  * @export
@@ -4835,10 +4953,10 @@ export interface ListTuningResponseDataTuningsInner {
 export interface ListTuningResponseDataTuningsInnerLimitation {
     /**
      * 
-     * @type {string}
+     * @type {TuningLimitationType}
      * @memberof ListTuningResponseDataTuningsInnerLimitation
      */
-    'type': string;
+    'type': TuningLimitationType;
     /**
      * 
      * @type {ListTuningResponseDataTuningsInnerLimitationDefault}
@@ -4864,6 +4982,8 @@ export interface ListTuningResponseDataTuningsInnerLimitation {
      */
     'regex'?: string;
 }
+
+
 /**
  * @type ListTuningResponseDataTuningsInnerLimitationDefault
  * @export
@@ -4887,7 +5007,13 @@ export interface ListTuningResponseDataTuningsInnerStatus {
      * @type {string}
      * @memberof ListTuningResponseDataTuningsInnerStatus
      */
-    'updatedAt'?: string;
+    'current': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof ListTuningResponseDataTuningsInnerStatus
+     */
+    'updatedAt': string;
     /**
      * 
      * @type {boolean}
@@ -4965,10 +5091,10 @@ export interface ListTuningSpecResponseDataInner {
 export interface ListTuningSpecResponseDataInnerLimitation {
     /**
      * 
-     * @type {string}
+     * @type {TuningLimitationType}
      * @memberof ListTuningSpecResponseDataInnerLimitation
      */
-    'type': string;
+    'type': TuningLimitationType;
     /**
      * 
      * @type {ListTuningResponseDataTuningsInnerLimitationDefault}
@@ -4994,6 +5120,8 @@ export interface ListTuningSpecResponseDataInnerLimitation {
      */
     'regex'?: string;
 }
+
+
 /**
  * 
  * @export
@@ -6186,6 +6314,25 @@ export interface SupportFileSetStatus {
      */
     'isCreating': boolean;
 }
+/**
+ * 
+ * @export
+ * @interface TimeValuePair
+ */
+export interface TimeValuePair {
+    /**
+     * 
+     * @type {string}
+     * @memberof TimeValuePair
+     */
+    'time': string;
+    /**
+     * 
+     * @type {number}
+     * @memberof TimeValuePair
+     */
+    'value': number;
+}
 /**
  * 
  * @export
@@ -6387,6 +6534,21 @@ export interface TrySlackChannelResponse {
      */
     'status': string;
 }
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const TuningLimitationType = {
+    String: 'string',
+    Int: 'int',
+    Bool: 'bool'
+} as const;
+
+export type TuningLimitationType = typeof TuningLimitationType[keyof typeof TuningLimitationType];
+
+
 /**
  * 
  * @export
@@ -8267,8 +8429,8 @@ export const HealthApiAxiosParamCreator = function (configuration?: Configuratio
          * @param {GetHealthHistoryServiceTypeEnum} serviceType The name of the service to retrieve health history. use GET /api/v1/datacenters/{dataCenter}/services to get the service list and their modules.
          * @param {GetHealthHistoryModuleTypeEnum} moduleType The name of the module to retrieve health history. use GET /api/v1/datacenters/{dataCenter}/services/{serviceType} to get the module list.
          * @param {GetHealthHistoryPastEnum} [past] The past time of the health history to query, click \&#39;try it out\&#39; to see a few options, but can specify with the \&#39;s\&#39;(second), \&#39;m\&#39;(minute), \&#39;h\&#39;(hour), and \&#39;d\&#39;(day) suffix for other time ranges.
-         * @param {string} [start] The start time of the health history to query, the value should be in RFC3339 format (default is 24 hours ago).
-         * @param {string} [stop] The end time of the health history to query, the value should be in RFC3339 format (default is now).
+         * @param {string} [start] The start time of the event to query, the value should be in RFC3339 format (default is 24 hours ago).
+         * @param {string} [stop] The end time of the event to query, the value should be in RFC3339 format (default is now).
          * @param {boolean} [watch] The toggle to enable http chunked transfer for continuous server push.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
@@ -8367,8 +8529,8 @@ export const HealthApiAxiosParamCreator = function (configuration?: Configuratio
          * @param {string} dataCenter The name of the data center to operate
          * @param {GetServiceHealthHistoryServiceTypeEnum} serviceType The name of the service to retrieve health history. use GET /api/v1/datacenters/{dataCenter}/services to get the service list and their modules.
          * @param {boolean} [watch] The toggle to enable http chunked transfer for continuous server push.
-         * @param {string} [start] The start time of the health history to query, the value should be in RFC3339 format (default is 24 hours ago).
-         * @param {string} [stop] The end time of the health history to query, the value should be in RFC3339 format (default is now).
+         * @param {string} [start] The start time of the event to query, the value should be in RFC3339 format (default is 24 hours ago).
+         * @param {string} [stop] The end time of the event to query, the value should be in RFC3339 format (default is now).
          * @param {GetServiceHealthHistoryPastEnum} [past] The past time of the health history to query, click \&#39;try it out\&#39; to see a few options, but can specify with the \&#39;s\&#39;(second), \&#39;m\&#39;(minute), \&#39;h\&#39;(hour), and \&#39;d\&#39;(day) suffix for other time ranges.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
@@ -8512,8 +8674,8 @@ export const HealthApiFp = function(configuration?: Configuration) {
          * @param {GetHealthHistoryServiceTypeEnum} serviceType The name of the service to retrieve health history. use GET /api/v1/datacenters/{dataCenter}/services to get the service list and their modules.
          * @param {GetHealthHistoryModuleTypeEnum} moduleType The name of the module to retrieve health history. use GET /api/v1/datacenters/{dataCenter}/services/{serviceType} to get the module list.
          * @param {GetHealthHistoryPastEnum} [past] The past time of the health history to query, click \&#39;try it out\&#39; to see a few options, but can specify with the \&#39;s\&#39;(second), \&#39;m\&#39;(minute), \&#39;h\&#39;(hour), and \&#39;d\&#39;(day) suffix for other time ranges.
-         * @param {string} [start] The start time of the health history to query, the value should be in RFC3339 format (default is 24 hours ago).
-         * @param {string} [stop] The end time of the health history to query, the value should be in RFC3339 format (default is now).
+         * @param {string} [start] The start time of the event to query, the value should be in RFC3339 format (default is 24 hours ago).
+         * @param {string} [stop] The end time of the event to query, the value should be in RFC3339 format (default is now).
          * @param {boolean} [watch] The toggle to enable http chunked transfer for continuous server push.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
@@ -8544,8 +8706,8 @@ export const HealthApiFp = function(configuration?: Configuration) {
          * @param {string} dataCenter The name of the data center to operate
          * @param {GetServiceHealthHistoryServiceTypeEnum} serviceType The name of the service to retrieve health history. use GET /api/v1/datacenters/{dataCenter}/services to get the service list and their modules.
          * @param {boolean} [watch] The toggle to enable http chunked transfer for continuous server push.
-         * @param {string} [start] The start time of the health history to query, the value should be in RFC3339 format (default is 24 hours ago).
-         * @param {string} [stop] The end time of the health history to query, the value should be in RFC3339 format (default is now).
+         * @param {string} [start] The start time of the event to query, the value should be in RFC3339 format (default is 24 hours ago).
+         * @param {string} [stop] The end time of the event to query, the value should be in RFC3339 format (default is now).
          * @param {GetServiceHealthHistoryPastEnum} [past] The past time of the health history to query, click \&#39;try it out\&#39; to see a few options, but can specify with the \&#39;s\&#39;(second), \&#39;m\&#39;(minute), \&#39;h\&#39;(hour), and \&#39;d\&#39;(day) suffix for other time ranges.
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
@@ -8682,14 +8844,14 @@ export interface HealthApiGetHealthHistoryRequest {
     readonly past?: GetHealthHistoryPastEnum
 
     /**
-     * The start time of the health history to query, the value should be in RFC3339 format (default is 24 hours ago).
+     * The start time of the event to query, the value should be in RFC3339 format (default is 24 hours ago).
      * @type {string}
      * @memberof HealthApiGetHealthHistory
      */
     readonly start?: string
 
     /**
-     * The end time of the health history to query, the value should be in RFC3339 format (default is now).
+     * The end time of the event to query, the value should be in RFC3339 format (default is now).
      * @type {string}
      * @memberof HealthApiGetHealthHistory
      */
@@ -8752,14 +8914,14 @@ export interface HealthApiGetServiceHealthHistoryRequest {
     readonly watch?: boolean
 
     /**
-     * The start time of the health history to query, the value should be in RFC3339 format (default is 24 hours ago).
+     * The start time of the event to query, the value should be in RFC3339 format (default is 24 hours ago).
      * @type {string}
      * @memberof HealthApiGetServiceHealthHistory
      */
     readonly start?: string
 
     /**
-     * The end time of the health history to query, the value should be in RFC3339 format (default is now).
+     * The end time of the event to query, the value should be in RFC3339 format (default is now).
      * @type {string}
      * @memberof HealthApiGetServiceHealthHistory
      */
@@ -9764,6 +9926,76 @@ export class LogoutApi extends BaseAPI {
  */
 export const MetricsApiAxiosParamCreator = function (configuration?: Configuration) {
     return {
+        /**
+         * 
+         * @summary Retrieve the various metrics with different view from single host or single vm
+         * @param {string} dataCenter The name of the data center to operate
+         * @param {GetMetricByHostOrVmMetricTypeEnum} metricType The type of metric to query, the value can be \&#39;cpuUsage\&#39;, \&#39;memoryUsage\&#39;, \&#39;diskUsage\&#39;, \&#39;diskBandwidth\&#39;, \&#39;diskIops\&#39;, \&#39;diskLatency\&#39;, \&#39;diskReadIops\&#39;, \&#39;diskWriteIops\&#39;, \&#39;networkTrafficIn\&#39;, or \&#39;networkTrafficOut\&#39;.
+         * @param {GetMetricByHostOrVmViewTypeEnum} viewType The type of view to query, the value can be only \&#39;summary\&#39;, \&#39;history\&#39;, or \&#39;rank\&#39;.
+         * @param {GetMetricByHostOrVmEntityTypeEnum} entityType The type of entity to query, the value can be \&#39;hosts\&#39; or \&#39;vms\&#39;
+         * @param {string} entityIdOrName The id or name of the entity to query
+         * @param {GetMetricByHostOrVmPastEnum} [past] The past time of the health history to query, click \&#39;try it out\&#39; to see a few options, but can specify with the \&#39;s\&#39;(second), \&#39;m\&#39;(minute), \&#39;h\&#39;(hour), and \&#39;d\&#39;(day) suffix for other time ranges.
+         * @param {string} [start] The start time of the event to query, the value should be in RFC3339 format (default is 24 hours ago).
+         * @param {string} [stop] The end time of the event to query, the value should be in RFC3339 format (default is now).
+         * @param {boolean} [watch] The toggle to enable http chunked transfer for continuous server push.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getMetricByHostOrVm: async (dataCenter: string, metricType: GetMetricByHostOrVmMetricTypeEnum, viewType: GetMetricByHostOrVmViewTypeEnum, entityType: GetMetricByHostOrVmEntityTypeEnum, entityIdOrName: string, past?: GetMetricByHostOrVmPastEnum, start?: string, stop?: string, watch?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'dataCenter' is not null or undefined
+            assertParamExists('getMetricByHostOrVm', 'dataCenter', dataCenter)
+            // verify required parameter 'metricType' is not null or undefined
+            assertParamExists('getMetricByHostOrVm', 'metricType', metricType)
+            // verify required parameter 'viewType' is not null or undefined
+            assertParamExists('getMetricByHostOrVm', 'viewType', viewType)
+            // verify required parameter 'entityType' is not null or undefined
+            assertParamExists('getMetricByHostOrVm', 'entityType', entityType)
+            // verify required parameter 'entityIdOrName' is not null or undefined
+            assertParamExists('getMetricByHostOrVm', 'entityIdOrName', entityIdOrName)
+            const localVarPath = `/api/v1/datacenters/{dataCenter}/metrics/{metricType}/{viewType}/{entityType}/{entityId or Name}`
+                .replace(`{${"dataCenter"}}`, encodeURIComponent(String(dataCenter)))
+                .replace(`{${"metricType"}}`, encodeURIComponent(String(metricType)))
+                .replace(`{${"viewType"}}`, encodeURIComponent(String(viewType)))
+                .replace(`{${"entityType"}}`, encodeURIComponent(String(entityType)))
+                .replace(`{${"entityId or Name"}}`, encodeURIComponent(String(entityIdOrName)));
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            if (past !== undefined) {
+                localVarQueryParameter['past'] = past;
+            }
+
+            if (start !== undefined) {
+                localVarQueryParameter['start'] = start;
+            }
+
+            if (stop !== undefined) {
+                localVarQueryParameter['stop'] = stop;
+            }
+
+            if (watch !== undefined) {
+                localVarQueryParameter['watch'] = watch;
+            }
+
+
+    
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @summary Retrieve the various metrics with different view from hosts or vms
@@ -9874,6 +10106,27 @@ export const MetricsApiAxiosParamCreator = function (configuration?: Configurati
 export const MetricsApiFp = function(configuration?: Configuration) {
     const localVarAxiosParamCreator = MetricsApiAxiosParamCreator(configuration)
     return {
+        /**
+         * 
+         * @summary Retrieve the various metrics with different view from single host or single vm
+         * @param {string} dataCenter The name of the data center to operate
+         * @param {GetMetricByHostOrVmMetricTypeEnum} metricType The type of metric to query, the value can be \&#39;cpuUsage\&#39;, \&#39;memoryUsage\&#39;, \&#39;diskUsage\&#39;, \&#39;diskBandwidth\&#39;, \&#39;diskIops\&#39;, \&#39;diskLatency\&#39;, \&#39;diskReadIops\&#39;, \&#39;diskWriteIops\&#39;, \&#39;networkTrafficIn\&#39;, or \&#39;networkTrafficOut\&#39;.
+         * @param {GetMetricByHostOrVmViewTypeEnum} viewType The type of view to query, the value can be only \&#39;summary\&#39;, \&#39;history\&#39;, or \&#39;rank\&#39;.
+         * @param {GetMetricByHostOrVmEntityTypeEnum} entityType The type of entity to query, the value can be \&#39;hosts\&#39; or \&#39;vms\&#39;
+         * @param {string} entityIdOrName The id or name of the entity to query
+         * @param {GetMetricByHostOrVmPastEnum} [past] The past time of the health history to query, click \&#39;try it out\&#39; to see a few options, but can specify with the \&#39;s\&#39;(second), \&#39;m\&#39;(minute), \&#39;h\&#39;(hour), and \&#39;d\&#39;(day) suffix for other time ranges.
+         * @param {string} [start] The start time of the event to query, the value should be in RFC3339 format (default is 24 hours ago).
+         * @param {string} [stop] The end time of the event to query, the value should be in RFC3339 format (default is now).
+         * @param {boolean} [watch] The toggle to enable http chunked transfer for continuous server push.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getMetricByHostOrVm(dataCenter: string, metricType: GetMetricByHostOrVmMetricTypeEnum, viewType: GetMetricByHostOrVmViewTypeEnum, entityType: GetMetricByHostOrVmEntityTypeEnum, entityIdOrName: string, past?: GetMetricByHostOrVmPastEnum, start?: string, stop?: string, watch?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetMetricByHostOrVm200Response>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getMetricByHostOrVm(dataCenter, metricType, viewType, entityType, entityIdOrName, past, start, stop, watch, options);
+            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+            const localVarOperationServerBasePath = operationServerMap['MetricsApi.getMetricByHostOrVm']?.[localVarOperationServerIndex]?.url;
+            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+        },
         /**
          * 
          * @summary Retrieve the various metrics with different view from hosts or vms
@@ -9917,6 +10170,16 @@ export const MetricsApiFp = function(configuration?: Configuration) {
 export const MetricsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
     const localVarFp = MetricsApiFp(configuration)
     return {
+        /**
+         * 
+         * @summary Retrieve the various metrics with different view from single host or single vm
+         * @param {MetricsApiGetMetricByHostOrVmRequest} requestParameters Request parameters.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getMetricByHostOrVm(requestParameters: MetricsApiGetMetricByHostOrVmRequest, options?: RawAxiosRequestConfig): AxiosPromise<GetMetricByHostOrVm200Response> {
+            return localVarFp.getMetricByHostOrVm(requestParameters.dataCenter, requestParameters.metricType, requestParameters.viewType, requestParameters.entityType, requestParameters.entityIdOrName, requestParameters.past, requestParameters.start, requestParameters.stop, requestParameters.watch, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @summary Retrieve the various metrics with different view from hosts or vms
@@ -9940,6 +10203,76 @@ export const MetricsApiFactory = function (configuration?: Configuration, basePa
     };
 };
 
+/**
+ * Request parameters for getMetricByHostOrVm operation in MetricsApi.
+ * @export
+ * @interface MetricsApiGetMetricByHostOrVmRequest
+ */
+export interface MetricsApiGetMetricByHostOrVmRequest {
+    /**
+     * The name of the data center to operate
+     * @type {string}
+     * @memberof MetricsApiGetMetricByHostOrVm
+     */
+    readonly dataCenter: string
+
+    /**
+     * The type of metric to query, the value can be \&#39;cpuUsage\&#39;, \&#39;memoryUsage\&#39;, \&#39;diskUsage\&#39;, \&#39;diskBandwidth\&#39;, \&#39;diskIops\&#39;, \&#39;diskLatency\&#39;, \&#39;diskReadIops\&#39;, \&#39;diskWriteIops\&#39;, \&#39;networkTrafficIn\&#39;, or \&#39;networkTrafficOut\&#39;.
+     * @type {'cpuUsage' | 'memoryUsage' | 'diskUsage' | 'diskBandwidth' | 'diskIops' | 'diskLatency' | 'diskReadIops' | 'diskWriteIops' | 'networkTrafficIn' | 'networkTrafficOut'}
+     * @memberof MetricsApiGetMetricByHostOrVm
+     */
+    readonly metricType: GetMetricByHostOrVmMetricTypeEnum
+
+    /**
+     * The type of view to query, the value can be only \&#39;summary\&#39;, \&#39;history\&#39;, or \&#39;rank\&#39;.
+     * @type {'summary' | 'history' | 'rank'}
+     * @memberof MetricsApiGetMetricByHostOrVm
+     */
+    readonly viewType: GetMetricByHostOrVmViewTypeEnum
+
+    /**
+     * The type of entity to query, the value can be \&#39;hosts\&#39; or \&#39;vms\&#39;
+     * @type {'hosts' | 'vms'}
+     * @memberof MetricsApiGetMetricByHostOrVm
+     */
+    readonly entityType: GetMetricByHostOrVmEntityTypeEnum
+
+    /**
+     * The id or name of the entity to query
+     * @type {string}
+     * @memberof MetricsApiGetMetricByHostOrVm
+     */
+    readonly entityIdOrName: string
+
+    /**
+     * The past time of the health history to query, click \&#39;try it out\&#39; to see a few options, but can specify with the \&#39;s\&#39;(second), \&#39;m\&#39;(minute), \&#39;h\&#39;(hour), and \&#39;d\&#39;(day) suffix for other time ranges.
+     * @type {'1h' | '24h' | '7d' | '14d'}
+     * @memberof MetricsApiGetMetricByHostOrVm
+     */
+    readonly past?: GetMetricByHostOrVmPastEnum
+
+    /**
+     * The start time of the event to query, the value should be in RFC3339 format (default is 24 hours ago).
+     * @type {string}
+     * @memberof MetricsApiGetMetricByHostOrVm
+     */
+    readonly start?: string
+
+    /**
+     * The end time of the event to query, the value should be in RFC3339 format (default is now).
+     * @type {string}
+     * @memberof MetricsApiGetMetricByHostOrVm
+     */
+    readonly stop?: string
+
+    /**
+     * The toggle to enable http chunked transfer for continuous server push.
+     * @type {boolean}
+     * @memberof MetricsApiGetMetricByHostOrVm
+     */
+    readonly watch?: boolean
+}
+
 /**
  * Request parameters for getMetricByTypes operation in MetricsApi.
  * @export
@@ -10024,6 +10357,18 @@ export interface MetricsApiGetMetricsOverviewRequest {
  * @extends {BaseAPI}
  */
 export class MetricsApi extends BaseAPI {
+    /**
+     * 
+     * @summary Retrieve the various metrics with different view from single host or single vm
+     * @param {MetricsApiGetMetricByHostOrVmRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof MetricsApi
+     */
+    public getMetricByHostOrVm(requestParameters: MetricsApiGetMetricByHostOrVmRequest, options?: RawAxiosRequestConfig) {
+        return MetricsApiFp(this.configuration).getMetricByHostOrVm(requestParameters.dataCenter, requestParameters.metricType, requestParameters.viewType, requestParameters.entityType, requestParameters.entityIdOrName, requestParameters.past, requestParameters.start, requestParameters.stop, requestParameters.watch, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @summary Retrieve the various metrics with different view from hosts or vms
@@ -10049,6 +10394,49 @@ export class MetricsApi extends BaseAPI {
     }
 }
 
+/**
+ * @export
+ */
+export const GetMetricByHostOrVmMetricTypeEnum = {
+    CpuUsage: 'cpuUsage',
+    MemoryUsage: 'memoryUsage',
+    DiskUsage: 'diskUsage',
+    DiskBandwidth: 'diskBandwidth',
+    DiskIops: 'diskIops',
+    DiskLatency: 'diskLatency',
+    DiskReadIops: 'diskReadIops',
+    DiskWriteIops: 'diskWriteIops',
+    NetworkTrafficIn: 'networkTrafficIn',
+    NetworkTrafficOut: 'networkTrafficOut'
+} as const;
+export type GetMetricByHostOrVmMetricTypeEnum = typeof GetMetricByHostOrVmMetricTypeEnum[keyof typeof GetMetricByHostOrVmMetricTypeEnum];
+/**
+ * @export
+ */
+export const GetMetricByHostOrVmViewTypeEnum = {
+    Summary: 'summary',
+    History: 'history',
+    Rank: 'rank'
+} as const;
+export type GetMetricByHostOrVmViewTypeEnum = typeof GetMetricByHostOrVmViewTypeEnum[keyof typeof GetMetricByHostOrVmViewTypeEnum];
+/**
+ * @export
+ */
+export const GetMetricByHostOrVmEntityTypeEnum = {
+    Hosts: 'hosts',
+    Vms: 'vms'
+} as const;
+export type GetMetricByHostOrVmEntityTypeEnum = typeof GetMetricByHostOrVmEntityTypeEnum[keyof typeof GetMetricByHostOrVmEntityTypeEnum];
+/**
+ * @export
+ */
+export const GetMetricByHostOrVmPastEnum = {
+    _1h: '1h',
+    _24h: '24h',
+    _7d: '7d',
+    _14d: '14d'
+} as const;
+export type GetMetricByHostOrVmPastEnum = typeof GetMetricByHostOrVmPastEnum[keyof typeof GetMetricByHostOrVmPastEnum];
 /**
  * @export
  */
@@ -13062,7 +13450,7 @@ export const TuningsApiAxiosParamCreator = function (configuration?: Configurati
          * @summary Retrieve the list of tunings from a host or data center
          * @param {string} dataCenter The name of the data center to operate
          * @param {Array<string>} [host] The name of the host to retrieve the tunings, can specify multiple hosts to retrieve the tunings, for example: host&#x3D;example-node-0&amp;host&#x3D;example-node-1
-         * @param {string} [keyword] The keyword to search the tunings
+         * @param {string} [keyword] The keyword to search, can be any string
          * @param {number} [pageSize] The number of items per page (default is unlimit).
          * @param {number} [pageNum] The page number to retrieve
          * @param {boolean} [watch] The toggle to enable http chunked transfer for continuous server push.
@@ -13252,7 +13640,7 @@ export const TuningsApiFp = function(configuration?: Configuration) {
          * @summary Retrieve the list of tunings from a host or data center
          * @param {string} dataCenter The name of the data center to operate
          * @param {Array<string>} [host] The name of the host to retrieve the tunings, can specify multiple hosts to retrieve the tunings, for example: host&#x3D;example-node-0&amp;host&#x3D;example-node-1
-         * @param {string} [keyword] The keyword to search the tunings
+         * @param {string} [keyword] The keyword to search, can be any string
          * @param {number} [pageSize] The number of items per page (default is unlimit).
          * @param {number} [pageNum] The page number to retrieve
          * @param {boolean} [watch] The toggle to enable http chunked transfer for continuous server push.
@@ -13422,7 +13810,7 @@ export interface TuningsApiListTuningsRequest {
     readonly host?: Array<string>
 
     /**
-     * The keyword to search the tunings
+     * The keyword to search, can be any string
      * @type {string}
      * @memberof TuningsApiListTunings
      */
diff --git a/packages/cube-frontend-ui-library/src/components/CosDropdown/CosDropdown.tsx b/packages/cube-frontend-ui-library/src/components/CosDropdown/CosDropdown.tsx
index 556bdcf04..ef51edb8a 100644
--- a/packages/cube-frontend-ui-library/src/components/CosDropdown/CosDropdown.tsx
+++ b/packages/cube-frontend-ui-library/src/components/CosDropdown/CosDropdown.tsx
@@ -24,6 +24,7 @@ export type CosDropdownProps<Item, Type extends CosDropdownType> = {
   selectedItems: Item[]
   disabled?: boolean
   isLoading?: boolean
+  skeletonClassName?: string
   children: ReactNode
 } & CheckboxDropdownProps<Type> &
   SearchDropdownProps<Type>
@@ -56,6 +57,7 @@ export const CosDropdown = <Item, Type extends CosDropdownType>(
     selectedItems,
     disabled = false,
     isLoading = false,
+    skeletonClassName,
     children,
   } = props
 
@@ -109,8 +111,15 @@ export const CosDropdown = <Item, Type extends CosDropdownType>(
     }
   }, [handleClickOutside])
 
-  if (isLoading)
-    return <CosDropdownSkeleton variant={variant} hasLabel={!!label} />
+  if (isLoading) {
+    return (
+      <CosDropdownSkeleton
+        className={skeletonClassName}
+        variant={variant}
+        hasLabel={!!label}
+      />
+    )
+  }
 
   return (
     <CosDropdownContext.Provider
diff --git a/packages/cube-frontend-ui-library/src/components/CosDropdown/CosDropdownSkeleton.tsx b/packages/cube-frontend-ui-library/src/components/CosDropdown/CosDropdownSkeleton.tsx
index 1216e7b71..2340033de 100644
--- a/packages/cube-frontend-ui-library/src/components/CosDropdown/CosDropdownSkeleton.tsx
+++ b/packages/cube-frontend-ui-library/src/components/CosDropdown/CosDropdownSkeleton.tsx
@@ -1,3 +1,4 @@
+import { PropsWithClassName } from '@cube-frontend/utils'
 import { twMerge } from 'tailwind-merge'
 import { CosSkeleton } from '../CosSkeleton/CosSkeleton'
 import { CosDropdownVariant } from './utils'
@@ -5,7 +6,7 @@ import { skeleton } from './styles'
 
 type DropdownInputSkeletonProps = { variant: CosDropdownVariant }
 
-type CosDropdownSkeletonProps = {
+type CosDropdownSkeletonProps = PropsWithClassName & {
   variant?: CosDropdownVariant
   hasLabel?: boolean
 }
@@ -18,10 +19,10 @@ const DropdownInputSkeleton = (props: DropdownInputSkeletonProps) => {
 }
 
 export const CosDropdownSkeleton = (props: CosDropdownSkeletonProps) => {
-  const { variant = 'default', hasLabel = false } = props
+  const { className, variant = 'default', hasLabel = false } = props
 
   return (
-    <div className={twMerge(skeleton.container({ variant }))}>
+    <div className={twMerge(skeleton.container({ variant }), className)}>
       {hasLabel && <DropdownLabelSkeleton />}
       <DropdownInputSkeleton variant={variant} />
     </div>
diff --git a/packages/cube-frontend-ui-library/src/components/CosModal/CosModal.tsx b/packages/cube-frontend-ui-library/src/components/CosModal/CosModal.tsx
index d41539166..d7ad430ef 100644
--- a/packages/cube-frontend-ui-library/src/components/CosModal/CosModal.tsx
+++ b/packages/cube-frontend-ui-library/src/components/CosModal/CosModal.tsx
@@ -13,12 +13,16 @@ export type CosModalProps = {
   size?: CosModalSize
   isOpen: boolean
   title: string
+  /**
+   * @default true
+   */
+  isActionButtonVisible?: boolean
   /**
    * @default 'Action'
    */
   actionText?: string
   actionButtonProps?: Pick<CosButtonProps, 'loading' | 'disabled'>
-  onActionClick: () => void
+  onActionClick?: () => void
   onCloseClick: () => void
 }
 
@@ -30,6 +34,7 @@ export const CosModal = (props: CosModalProps) => {
     size = 'md',
     isOpen,
     title,
+    isActionButtonVisible = true,
     actionText = 'Action',
     actionButtonProps,
     onActionClick,
@@ -62,14 +67,16 @@ export const CosModal = (props: CosModalProps) => {
         </div>
         <div className="flex-1 overflow-auto p-7">{children}</div>
         <div className="flex items-center justify-end gap-x-2.5 border-t border-functional-border-divider px-7 py-4">
-          <CosButton
-            usage="text-only"
-            size="lg"
-            onClick={onActionClick}
-            {...actionButtonProps}
-          >
-            {actionText}
-          </CosButton>
+          {isActionButtonVisible && (
+            <CosButton
+              usage="text-only"
+              size="lg"
+              onClick={onActionClick}
+              {...actionButtonProps}
+            >
+              {actionText}
+            </CosButton>
+          )}
           <CosButton
             type="secondary"
             usage="text-only"
diff --git a/packages/cube-frontend-web-app/src/CosRoutes.tsx b/packages/cube-frontend-web-app/src/CosRoutes.tsx
index aab03f6e7..3fd802b4f 100644
--- a/packages/cube-frontend-web-app/src/CosRoutes.tsx
+++ b/packages/cube-frontend-web-app/src/CosRoutes.tsx
@@ -31,6 +31,9 @@ export const CosRoutes = () => {
         <Route path="/home/health/:module" element={<HealthDetailsPage />} />
         <Route path="/home/manage" element={<HomeManagePage />} />
       </Route>
+      <Route path="/events" element={<EventsLayout />}>
+        <Route path="/events/tunings" element={<EventsTuningsPage />} />
+      </Route>
       <Route path="/settings" element={<SettingsPage />} />
       <Route path="/integrations" element={<IntegrationsPage />} />
       <Route path={CosRoutesEnum.EVENTS_PAGE} element={<EventsLayout />}>
diff --git a/packages/cube-frontend-web-app/src/api/cosApi.ts b/packages/cube-frontend-web-app/src/api/cosApi.ts
index cc267a090..122bc5e7c 100644
--- a/packages/cube-frontend-web-app/src/api/cosApi.ts
+++ b/packages/cube-frontend-web-app/src/api/cosApi.ts
@@ -12,6 +12,7 @@ import {
   Configuration,
   ServicesApi,
   SettingsApi,
+  TuningsApi,
 } from '@cube-frontend/api'
 import devAccessTokenInterceptor from './devAccessTokenInterceptor'
 import { samlAuthErrorInterceptor } from './samlAuthErrorInterceptor'
@@ -66,6 +67,7 @@ export const nodesApi = createApiInstance(NodesApi)
 export const eventsApi = createApiInstance(EventsApi)
 export const servicesApi = createApiInstance(ServicesApi)
 export const settingsApi = createApiInstance(SettingsApi)
+export const tuningsApi = createApiInstance(TuningsApi)
 
 cosApi.interceptors.response.use(undefined, samlAuthErrorInterceptor)
 
diff --git a/packages/cube-frontend-web-app/src/pages/settings/_components/IconActionButton.tsx b/packages/cube-frontend-web-app/src/components/IconActionButton/IconActionButton.tsx
similarity index 100%
rename from packages/cube-frontend-web-app/src/pages/settings/_components/IconActionButton.tsx
rename to packages/cube-frontend-web-app/src/components/IconActionButton/IconActionButton.tsx
diff --git a/packages/cube-frontend-web-app/src/pages/events/EventsLayout.tsx b/packages/cube-frontend-web-app/src/pages/events/EventsLayout.tsx
index 4b6571c32..a8c7e3a70 100644
--- a/packages/cube-frontend-web-app/src/pages/events/EventsLayout.tsx
+++ b/packages/cube-frontend-web-app/src/pages/events/EventsLayout.tsx
@@ -13,7 +13,7 @@ export const EventsLayout = () => {
   const location = useLocation()
 
   return (
-    <div>
+    <div className="flex flex-col gap-y-4 px-2 py-1">
       <CosTabs>
         <Link to={links.events}>
           <CosTabs.Tab isActive={location.pathname === links.events}>
diff --git a/packages/cube-frontend-web-app/src/pages/events/chart/EventsChartPage.tsx b/packages/cube-frontend-web-app/src/pages/events/chart/EventsChartPage.tsx
index 5aa147a3f..c4e80ba5a 100644
--- a/packages/cube-frontend-web-app/src/pages/events/chart/EventsChartPage.tsx
+++ b/packages/cube-frontend-web-app/src/pages/events/chart/EventsChartPage.tsx
@@ -22,7 +22,7 @@ export const EventsChartPage = () => {
   const eventsFilter = getEventsFilter(eventsType)
 
   return (
-    <div className="mt-4 flex flex-col gap-4">
+    <div className="flex flex-col gap-4">
       <div className="flex items-center justify-between">
         <EventsContentSwitcher
           activeTab={eventsType}
diff --git a/packages/cube-frontend-web-app/src/pages/events/index/EventsIndexPage.tsx b/packages/cube-frontend-web-app/src/pages/events/index/EventsIndexPage.tsx
index e8dd3cfde..fe589caf8 100644
--- a/packages/cube-frontend-web-app/src/pages/events/index/EventsIndexPage.tsx
+++ b/packages/cube-frontend-web-app/src/pages/events/index/EventsIndexPage.tsx
@@ -1,9 +1,5 @@
 import { EventsTable } from './_components/EventsTable/EventsTable'
 
 export const EventsIndexPage = () => {
-  return (
-    <div className="mt-4">
-      <EventsTable />
-    </div>
-  )
+  return <EventsTable />
 }
diff --git a/packages/cube-frontend-web-app/src/pages/events/links.ts b/packages/cube-frontend-web-app/src/pages/events/links.ts
new file mode 100644
index 000000000..522f4b0df
--- /dev/null
+++ b/packages/cube-frontend-web-app/src/pages/events/links.ts
@@ -0,0 +1,7 @@
+// TODO: Replace this with the route constants once the Events Page PR is merged.
+export const links = {
+  events: '/events',
+  triggers: '/events/triggers',
+  tunings: '/events/tunings',
+  chart: '/events/chart',
+}
diff --git a/packages/cube-frontend-web-app/src/pages/events/tunings/EventsTuningsPage.tsx b/packages/cube-frontend-web-app/src/pages/events/tunings/EventsTuningsPage.tsx
index 53330840a..aff97c614 100644
--- a/packages/cube-frontend-web-app/src/pages/events/tunings/EventsTuningsPage.tsx
+++ b/packages/cube-frontend-web-app/src/pages/events/tunings/EventsTuningsPage.tsx
@@ -1,3 +1,200 @@
+import {
+  ListTuningResponseDataTuningsInnerHostsInner,
+  ListTuningResponseDataTuningsInnerStatus,
+} from '@cube-frontend/api'
+import {
+  CosButton,
+  CosHyperlink,
+  CosInlineNotification,
+  CosLoadingSpinner,
+  CosModal,
+  CosPagination,
+  CosStroke,
+  GetCosBasicTable,
+} from '@cube-frontend/ui-library'
+import Plus from '@cube-frontend/ui-library/icons/monochrome/plus.svg?react'
+import dayjs from 'dayjs'
+import { Link } from 'react-router'
+import { ActionCell } from './_components/tableCells/ActionCell'
+import { TuningsFilter } from './TuningsFilter'
+import { joinHostNames, maxHostsDisplayCount, TuningRow } from './tuningsUtils'
+import { useResetTuningModal } from './uesResetTuningModal'
+import { useListTuningsQuery } from './useListTuningsQuery'
+import { useOperationErrors } from './useOperationErrors'
+import { useTuningHostsModal } from './useTuningHostsModal'
+import { useTuningRows } from './useTuningRows'
+
+const TuningTable = GetCosBasicTable<TuningRow>()
+
 export const EventsTuningsPage = () => {
-  return <div> Events Tunings Page</div>
+  const {
+    query,
+    onKeywordChange,
+    onKeywordClear,
+    onModifyStatusItemClick,
+    onNodeItemClick,
+    onNodesAllCheckChange,
+    onPageChange,
+    onItemsPerPageChange,
+  } = useListTuningsQuery()
+
+  const { operationErrors, onOperationErrorOccur, onOperationErrorClose } =
+    useOperationErrors()
+
+  const {
+    isLoading,
+    rows,
+    page,
+    hasModifiedTuning,
+    onToggleChange,
+    resetTuning,
+  } = useTuningRows(query, onOperationErrorOccur)
+
+  const {
+    isHostsModalOpen,
+    rowForHostModal,
+    onShowHostsClick,
+    onHostsModalClose,
+  } = useTuningHostsModal(rows)
+
+  const renderHosts = (
+    hosts: ListTuningResponseDataTuningsInnerHostsInner[],
+    row: TuningRow,
+  ) => {
+    const previewHosts = joinHostNames(hosts.slice(0, maxHostsDisplayCount))
+    if (hosts.length > maxHostsDisplayCount) {
+      return (
+        <>
+          {`${previewHosts},`}
+          <CosHyperlink
+            variant="text-only"
+            onClick={() => onShowHostsClick(row)}
+          >
+            [...]
+          </CosHyperlink>
+        </>
+      )
+    }
+    return previewHosts
+  }
+
+  const renderUpdateTime = (
+    status: ListTuningResponseDataTuningsInnerStatus,
+  ) => {
+    const { updatedAt } = status
+    if (!updatedAt) {
+      return <span className="text-functional-text-light">Never</span>
+    }
+    return dayjs.respectTzOffset(updatedAt).format('YYYY/MM/DD')
+  }
+
+  const { isResetModalOpen, toBeResetRowId, onResetClick, onCloseResetModal } =
+    useResetTuningModal()
+
+  const onConfirmReset = async (): Promise<void> => {
+    if (toBeResetRowId) {
+      onCloseResetModal()
+      await resetTuning(toBeResetRowId)
+    }
+  }
+
+  return (
+    <div
+      className="flex flex-col gap-y-6 rounded-[5px] bg-grey-0 px-6 py-4"
+      style={{
+        boxShadow: '0px 0px 3px 0px rgba(0, 0, 0, 0.10)',
+      }}
+    >
+      <div className="flex flex-col gap-y-4">
+        <h4 className="secondary-h4">Tunings</h4>
+        <Link className="self-start" to="/events/tunings/create">
+          <CosButton usage="icon-left" Icon={Plus}>
+            Create Tuning
+          </CosButton>
+        </Link>
+      </div>
+      {operationErrors.map((error, index) => (
+        <CosInlineNotification
+          key={error.id}
+          type="error"
+          onClose={() => onOperationErrorClose(index)}
+        >
+          {error.message}
+        </CosInlineNotification>
+      ))}
+      <CosStroke type="dot" />
+      <TuningsFilter
+        query={query}
+        onKeywordChange={onKeywordChange}
+        onKeywordClear={onKeywordClear}
+        onModifyStatusItemClick={onModifyStatusItemClick}
+        onNodeItemClick={onNodeItemClick}
+        onNodesAllCheckChange={onNodesAllCheckChange}
+      />
+      <TuningTable isLoading={isLoading} rows={rows}>
+        <TuningTable.Column
+          property="name"
+          label="Name (Keys)"
+          emphasize={true}
+        >
+          {(name, row) => (
+            <div className="flex gap-x-2">
+              <span>{name}</span>
+              {row.status.isUpdating && (
+                <CosLoadingSpinner className="ml-2" variant="dot45" />
+              )}
+            </div>
+          )}
+        </TuningTable.Column>
+        <TuningTable.Column property="hosts" label="Hosts">
+          {renderHosts}
+        </TuningTable.Column>
+        <TuningTable.Column property="status" label="Update Time">
+          {renderUpdateTime}
+        </TuningTable.Column>
+        <TuningTable.Column property="description" label="Description" />
+        <TuningTable.Column property="value" label="Value" />
+        <TuningTable.Column>
+          {(_, row) => (
+            <ActionCell
+              row={row}
+              saveSpaceForResetButton={rows.length === 1 || hasModifiedTuning}
+              onToggleChange={(enabled) => onToggleChange(row.id, enabled)}
+              onResetClick={() => onResetClick(row.id)}
+            />
+          )}
+        </TuningTable.Column>
+      </TuningTable>
+      <CosPagination
+        totalItems={page?.totalItemCount ?? 0}
+        currentPage={query.currentPage}
+        itemsPerPage={query.itemsPerPage}
+        onPageChange={onPageChange}
+        onItemsPerPageChange={onItemsPerPageChange}
+      />
+      <CosModal
+        title="Hosts"
+        size="sm"
+        isOpen={isHostsModalOpen}
+        isActionButtonVisible={false}
+        onCloseClick={onHostsModalClose}
+      >
+        <div className="primary-body4 text-functional-text">
+          {joinHostNames(rowForHostModal?.hosts)}
+        </div>
+      </CosModal>
+      <CosModal
+        title="Reset Tuning"
+        size="sm"
+        isOpen={isResetModalOpen}
+        actionText="Reset"
+        onActionClick={onConfirmReset}
+        onCloseClick={onCloseResetModal}
+      >
+        <div className="primary-body4 text-functional-text">
+          Are you sure you want to reset this tuning to default?
+        </div>
+      </CosModal>
+    </div>
+  )
 }
diff --git a/packages/cube-frontend-web-app/src/pages/events/tunings/HostDropdown.tsx b/packages/cube-frontend-web-app/src/pages/events/tunings/HostDropdown.tsx
new file mode 100644
index 000000000..340a56dcb
--- /dev/null
+++ b/packages/cube-frontend-web-app/src/pages/events/tunings/HostDropdown.tsx
@@ -0,0 +1,94 @@
+import { Node, NodesApiGetNodesRequest } from '@cube-frontend/api'
+import { CosDropdown } from '@cube-frontend/ui-library'
+import { nodesApi } from '@cube-frontend/web-app/api/cosApi'
+import { DataCenterContext } from '@cube-frontend/web-app/context/DataCenterContext'
+import { useCosGetRequest } from '@cube-frontend/web-app/hooks/useCosRequest/useCosGetRequest'
+import { ChangeEvent, useContext, useMemo, useState } from 'react'
+
+type HostDropdownProps = {
+  selectedHosts: string[]
+  onItemClick: (node: Node) => void
+  onAllCheckChange: (nodes: Node[]) => void
+}
+
+export const HostDropdown = (props: HostDropdownProps) => {
+  const {
+    selectedHosts,
+    onItemClick,
+    onAllCheckChange: onAllCheckChangeProp,
+  } = props
+
+  const { name: dataCenter } = useContext(DataCenterContext)
+
+  const [searchValue, setSearchValue] = useState('')
+
+  const { isLoading, data: getNodesData } = useCosGetRequest(
+    nodesApi.getNodes,
+    (): NodesApiGetNodesRequest => ({
+      dataCenter,
+    }),
+  )
+
+  const matchedHosts = useMemo<Node[]>(() => {
+    const { nodes } = getNodesData ?? {}
+    if (!nodes) {
+      return []
+    }
+
+    if (!searchValue) {
+      return nodes
+    }
+
+    const loweredValue = searchValue.toLowerCase()
+    return nodes.filter((node) =>
+      node.hostname.toLowerCase().includes(loweredValue),
+    )
+  }, [getNodesData, searchValue])
+
+  const onAllCheckChange = (checked: boolean): void => {
+    if (!getNodesData) {
+      return
+    }
+    if (checked) {
+      onAllCheckChangeProp(getNodesData.nodes)
+    } else {
+      onAllCheckChangeProp([])
+    }
+  }
+
+  const onSearchChange = (e: ChangeEvent<HTMLInputElement>): void => {
+    setSearchValue(e.target.value)
+  }
+
+  const onClearClick = (): void => {
+    onAllCheckChangeProp([])
+  }
+
+  return (
+    <CosDropdown
+      type="search-checkbox"
+      isLoading={isLoading}
+      skeletonClassName="w-36"
+      selectedItems={selectedHosts}
+      searchValue={searchValue}
+      onAllCheckChange={onAllCheckChange}
+      onSearchChange={onSearchChange}
+      onClearClick={onClearClick}
+    >
+      <CosDropdown.Trigger className="h-[34px] w-36" placeholder="Hosts">
+        {selectedHosts.length ? 'Hosts' : undefined}
+      </CosDropdown.Trigger>
+      <CosDropdown.Menu>
+        {matchedHosts.map((node) => (
+          <CosDropdown.Item
+            key={node.hostname}
+            item={node.hostname}
+            onClick={() => onItemClick(node)}
+          >
+            {node.hostname}
+          </CosDropdown.Item>
+        ))}
+      </CosDropdown.Menu>
+    </CosDropdown>
+  )
+}
diff --git a/packages/cube-frontend-web-app/src/pages/events/tunings/TuningsFilter.tsx b/packages/cube-frontend-web-app/src/pages/events/tunings/TuningsFilter.tsx
new file mode 100644
index 000000000..52a7ed33d
--- /dev/null
+++ b/packages/cube-frontend-web-app/src/pages/events/tunings/TuningsFilter.tsx
@@ -0,0 +1,83 @@
+import { Node } from '@cube-frontend/api'
+import { CosDropdown, CosSearchBarFilter } from '@cube-frontend/ui-library'
+import { ChangeEvent } from 'react'
+import { HostDropdown } from './HostDropdown'
+import { ListTuningsQuery } from './useListTuningsQuery'
+
+const modifyStatuses = [undefined, true, false] as const
+
+type TuningsFilterProps = {
+  query: ListTuningsQuery
+  onKeywordChange: (e: ChangeEvent<HTMLInputElement>) => void
+  onKeywordClear: () => void
+  onModifyStatusItemClick: (modified: boolean | undefined) => void
+  onNodeItemClick: (node: Node) => void
+  onNodesAllCheckChange: (nodes: Node[]) => void
+}
+
+const modifiedTriggerText = (modified: boolean | undefined): string => {
+  if (modified === undefined) {
+    return 'Modify Status'
+  }
+  return modifiedOptionText(modified)
+}
+
+const modifiedOptionText = (modified: boolean | undefined): string => {
+  if (modified === undefined) {
+    return 'Any'
+  }
+  if (modified) {
+    return 'Modified'
+  }
+  return 'Unmodified'
+}
+
+export const TuningsFilter = (props: TuningsFilterProps) => {
+  const {
+    query,
+    onKeywordChange,
+    onKeywordClear,
+    onModifyStatusItemClick,
+    onNodeItemClick,
+    onNodesAllCheckChange: onNodesAllCheckChangeProp,
+  } = props
+
+  return (
+    <div className="flex items-center gap-x-2">
+      <CosSearchBarFilter
+        placeholder="Search"
+        value={query.keyword}
+        showDropdown={false}
+        onChange={onKeywordChange}
+        onInputClear={onKeywordClear}
+      />
+      <CosDropdown selectedItems={query.selectedModified}>
+        <CosDropdown.Trigger
+          className="h-[34px] w-40"
+          placeholder="Modify Status"
+        >
+          {modifiedTriggerText(query.selectedModified[0])}
+        </CosDropdown.Trigger>
+        <CosDropdown.Menu>
+          {modifyStatuses.map((modified) => {
+            const text = modifiedOptionText(modified)
+            return (
+              <CosDropdown.Item
+                key={text}
+                item={modified}
+                onClick={() => onModifyStatusItemClick(modified)}
+              >
+                {text}
+              </CosDropdown.Item>
+            )
+          })}
+        </CosDropdown.Menu>
+      </CosDropdown>
+      <HostDropdown
+        selectedHosts={query.hosts}
+        onItemClick={onNodeItemClick}
+        onAllCheckChange={onNodesAllCheckChangeProp}
+      />
+    </div>
+  )
+}
diff --git a/packages/cube-frontend-web-app/src/pages/events/tunings/_components/tableCells/ActionCell.tsx b/packages/cube-frontend-web-app/src/pages/events/tunings/_components/tableCells/ActionCell.tsx
new file mode 100644
index 000000000..65a5f4c47
--- /dev/null
+++ b/packages/cube-frontend-web-app/src/pages/events/tunings/_components/tableCells/ActionCell.tsx
@@ -0,0 +1,98 @@
+import {
+  CosToggle,
+  CosTooltip,
+  CosTooltipInformation,
+} from '@cube-frontend/ui-library'
+import ArrowRotateLeft from '@cube-frontend/ui-library/icons/monochrome/arrow_rotate_left_01.svg?react'
+import Edit from '@cube-frontend/ui-library/icons/monochrome/edit.svg?react'
+import { IconActionButton } from '@cube-frontend/web-app/components/IconActionButton/IconActionButton'
+import { cva } from 'class-variance-authority'
+import { useMemo } from 'react'
+import { Link } from 'react-router'
+import { TuningRow } from '../../tuningsUtils'
+
+export type ActionCellProps = {
+  row: TuningRow
+  saveSpaceForResetButton: boolean
+  onToggleChange: (enabled: boolean) => Promise<void>
+  onResetClick: () => void
+}
+
+const iconButton = cva('icon-md', {
+  variants: {
+    disabled: {
+      true: 'text-functional-disable-text',
+      false: 'cursor-pointer text-functional-text',
+    },
+  },
+})
+
+export const ActionCell = (props: ActionCellProps) => {
+  const { row, saveSpaceForResetButton, onToggleChange, onResetClick } = props
+
+  const {
+    isModified,
+    status: { isUpdating },
+  } = row
+
+  const toggleHoverContent = useMemo<CosTooltipInformation | undefined>(() => {
+    if (isModified) {
+      return undefined
+    }
+    return {
+      message: 'Default key is not allowed to turn off.',
+    }
+  }, [isModified])
+
+  const renderEditButton = () => {
+    const iconElement = (
+      <Edit className={iconButton({ disabled: isUpdating })} />
+    )
+    if (isUpdating) {
+      return iconElement
+    }
+    const queryString = `?hosts=${row.hosts.map((host) => encodeURIComponent(host.name)).join(',')}`
+    return (
+      <Link to={`/events/tunings/${row.name}${queryString}`}>
+        {iconElement}
+      </Link>
+    )
+  }
+
+  const renderResetButton = () => {
+    if (!isModified) {
+      return (
+        <IconActionButton
+          Icon={ArrowRotateLeft}
+          hoverMessage="Reset to default value"
+          disabled={isUpdating}
+          onClick={onResetClick}
+        />
+      )
+    }
+
+    if (saveSpaceForResetButton) {
+      return <span className="icon-md" />
+    }
+
+    return undefined
+  }
+
+  return (
+    <div className="flex items-center justify-end gap-x-4">
+      <CosTooltip hoverContent={toggleHoverContent}>
+        {/* Wrap the toggle with a <span> because the hover event doesn't work
+        when the toggle is disabled, but we still need it for the tooltip. */}
+        <span>
+          <CosToggle
+            isOn={row.enabled}
+            disabled={isUpdating}
+            onChange={onToggleChange}
+          />
+        </span>
+      </CosTooltip>
+      {renderEditButton()}
+      {renderResetButton()}
+    </div>
+  )
+}
diff --git a/packages/cube-frontend-web-app/src/pages/events/tunings/tuningsUtils.ts b/packages/cube-frontend-web-app/src/pages/events/tunings/tuningsUtils.ts
new file mode 100644
index 000000000..4c13ece44
--- /dev/null
+++ b/packages/cube-frontend-web-app/src/pages/events/tunings/tuningsUtils.ts
@@ -0,0 +1,100 @@
+import {
+  ListTuningResponseDataTuningsInner,
+  ListTuningResponseDataTuningsInnerHostsInner,
+} from '@cube-frontend/api'
+import { CosTableRow, DEFAULT_ITEMS_PER_PAGE } from '@cube-frontend/ui-library'
+import { uniqueId } from 'lodash'
+import { z } from 'zod'
+import { ListTuningsQuery } from './useListTuningsQuery'
+
+export type TuningRow = ListTuningResponseDataTuningsInner & CosTableRow
+
+const getRowId = (): string => uniqueId('tuning')
+
+export const tuningToRow = (
+  tuning: ListTuningResponseDataTuningsInner,
+): TuningRow => ({
+  ...structuredClone(tuning),
+  id: getRowId(),
+})
+
+export const maxHostsDisplayCount = 10
+
+export const joinHostNames = (
+  hosts: ListTuningResponseDataTuningsInnerHostsInner[] | undefined,
+): string | undefined => {
+  return hosts?.map((host) => host.name).join(',')
+}
+
+const querySchema = z.object({
+  keyword: z.string().nullable(),
+  modified: z
+    .enum(['true', 'false'])
+    .nullable()
+    .transform((value) => {
+      if (value === 'true') {
+        return true
+      }
+      if (value === 'false') {
+        return false
+      }
+      return undefined
+    }),
+  hosts: z
+    .string()
+    .array()
+    .nullable()
+    .transform((array) => {
+      return array?.filter((value) => !!value)
+    }),
+})
+
+enum ParamKeyEnum {
+  Keyword = 'keyword',
+  Modified = 'modified',
+  Hosts = 'hosts',
+}
+
+export const searchParamsToQuery = (
+  searchParams: URLSearchParams,
+): ListTuningsQuery => {
+  const keyword = searchParams.get(ParamKeyEnum.Keyword)
+  const modified = searchParams.get(ParamKeyEnum.Modified)
+  const hosts = searchParams.getAll(ParamKeyEnum.Hosts)
+
+  const parsedQuery = querySchema.safeParse({
+    keyword,
+    modified,
+    hosts,
+  }).data
+
+  return {
+    keyword: parsedQuery?.keyword ?? '',
+    selectedModified: [parsedQuery?.modified],
+    hosts: parsedQuery?.hosts ?? [],
+    currentPage: 1,
+    itemsPerPage: DEFAULT_ITEMS_PER_PAGE,
+  }
+}
+
+export const queryToSearchParams = (
+  query: ListTuningsQuery,
+): URLSearchParams => {
+  const { keyword, selectedModified, hosts } = query
+  const modified = selectedModified[0]
+  const nextSearchParams = new URLSearchParams()
+
+  if (keyword) {
+    nextSearchParams.set(ParamKeyEnum.Keyword, keyword)
+  }
+
+  if (modified !== undefined) {
+    nextSearchParams.set(ParamKeyEnum.Modified, modified.toString())
+  }
+
+  hosts.forEach((host) => {
+    nextSearchParams.append(ParamKeyEnum.Hosts, host)
+  })
+
+  return nextSearchParams
+}
diff --git a/packages/cube-frontend-web-app/src/pages/events/tunings/uesResetTuningModal.ts b/packages/cube-frontend-web-app/src/pages/events/tunings/uesResetTuningModal.ts
new file mode 100644
index 000000000..c05f83488
--- /dev/null
+++ b/packages/cube-frontend-web-app/src/pages/events/tunings/uesResetTuningModal.ts
@@ -0,0 +1,29 @@
+import { useState } from 'react'
+
+type UseResetTuningModal = {
+  isResetModalOpen: boolean
+  toBeResetRowId: string | undefined
+  onResetClick: (rowId: string) => void
+  onCloseResetModal: () => void
+}
+
+export const useResetTuningModal = (): UseResetTuningModal => {
+  const [toBeResetRowId, setToBeResetRowId] = useState<string | undefined>(
+    undefined,
+  )
+
+  const onResetClick = (rowId: string): void => {
+    setToBeResetRowId(rowId)
+  }
+
+  const onCloseResetModal = (): void => {
+    setToBeResetRowId(undefined)
+  }
+
+  return {
+    isResetModalOpen: !!toBeResetRowId,
+    toBeResetRowId,
+    onResetClick,
+    onCloseResetModal,
+  }
+}
diff --git a/packages/cube-frontend-web-app/src/pages/events/tunings/useListTuningsQuery.ts b/packages/cube-frontend-web-app/src/pages/events/tunings/useListTuningsQuery.ts
new file mode 100644
index 000000000..62f9566ba
--- /dev/null
+++ b/packages/cube-frontend-web-app/src/pages/events/tunings/useListTuningsQuery.ts
@@ -0,0 +1,115 @@
+import { Node } from '@cube-frontend/api'
+import { ItemsPerPage } from '@cube-frontend/ui-library'
+import { ChangeEvent, useEffect, useState } from 'react'
+import { useSearchParams } from 'react-router'
+import { queryToSearchParams, searchParamsToQuery } from './tuningsUtils'
+
+type UseListTuningsQuery = {
+  query: ListTuningsQuery
+  onKeywordChange: (e: ChangeEvent<HTMLInputElement>) => void
+  onKeywordClear: () => void
+  onModifyStatusItemClick: (value: boolean | undefined) => void
+  onNodeItemClick: (node: Node) => void
+  onNodesAllCheckChange: (nodes: Node[]) => void
+  onPageChange: (page: number) => void
+  onItemsPerPageChange: (itemsPerPage: ItemsPerPage) => void
+}
+
+export type ListTuningsQuery = {
+  keyword: string
+  selectedModified: [boolean | undefined]
+  hosts: string[]
+  currentPage: number
+  itemsPerPage: ItemsPerPage
+}
+
+export const useListTuningsQuery = (): UseListTuningsQuery => {
+  const [searchParams, setSearchParams] = useSearchParams()
+
+  const [query, setQuery] = useState<ListTuningsQuery>(() =>
+    searchParamsToQuery(searchParams),
+  )
+
+  useEffect(() => {
+    const timeoutId = setTimeout(() => {
+      const nextSearchParams = queryToSearchParams(query)
+      setSearchParams(nextSearchParams, { replace: true })
+    }, 250)
+
+    return () => {
+      clearTimeout(timeoutId)
+    }
+  }, [query, setSearchParams])
+
+  const onKeywordChange = (e: ChangeEvent<HTMLInputElement>): void => {
+    const { value } = e.target
+    setQuery((prev) => ({
+      ...prev,
+      keyword: value,
+      currentPage: 1,
+    }))
+  }
+
+  const onKeywordClear = (): void => {
+    setQuery((prev) => ({
+      ...prev,
+      keyword: '',
+      currentPage: 1,
+    }))
+  }
+
+  const onModifyStatusItemClick = (modified: boolean | undefined): void => {
+    setQuery((prev) => ({
+      ...prev,
+      selectedModified: [modified],
+      currentPage: 1,
+    }))
+  }
+
+  const onNodeItemClick = (node: Node): void => {
+    const { hostname } = node
+    const isSelected = query.hosts.includes(hostname)
+    setQuery((prev) => ({
+      ...prev,
+      hosts: isSelected
+        ? prev.hosts.filter((host) => host !== hostname)
+        : [...prev.hosts, hostname],
+      currentPage: 1,
+    }))
+  }
+
+  const onNodesAllCheckChange = (nodes: Node[]): void => {
+    const hosts = nodes.map((node) => node.hostname)
+    setQuery((prev) => ({
+      ...prev,
+      hosts,
+      currentPage: 1,
+    }))
+  }
+
+  const onPageChange = (page: number): void => {
+    setQuery((prev) => ({
+      ...prev,
+      currentPage: page,
+    }))
+  }
+
+  const onItemsPerPageChange = (itemsPerPage: ItemsPerPage): void => {
+    setQuery((prev) => ({
+      ...prev,
+      currentPage: 1,
+      itemsPerPage,
+    }))
+  }
+
+  return {
+    query,
+    onKeywordChange,
+    onKeywordClear,
+    onModifyStatusItemClick,
+    onNodeItemClick,
+    onNodesAllCheckChange,
+    onPageChange,
+    onItemsPerPageChange,
+  }
+}
diff --git a/packages/cube-frontend-web-app/src/pages/events/tunings/useOperationErrors.ts b/packages/cube-frontend-web-app/src/pages/events/tunings/useOperationErrors.ts
new file mode 100644
index 000000000..469a7ad74
--- /dev/null
+++ b/packages/cube-frontend-web-app/src/pages/events/tunings/useOperationErrors.ts
@@ -0,0 +1,53 @@
+import { uniqueId } from 'lodash'
+import { useState } from 'react'
+
+export type UseOperationErrors = {
+  operationErrors: OperationError[]
+  onOperationErrorOccur: (errorMessage: string) => void
+  onOperationErrorClose: (index: number) => void
+}
+
+type OperationError = {
+  id: string
+  message: string
+}
+
+const getNewId = (): string => {
+  return uniqueId('tuningOperationError')
+}
+
+const MAX_ERROR_COUNT = 5
+
+export const useOperationErrors = (): UseOperationErrors => {
+  const [operationErrors, setOperationErrors] = useState<OperationError[]>([])
+
+  const onOperationErrorOccur = (errorMessage: string): void => {
+    setOperationErrors((prev) => {
+      const next = [
+        ...prev,
+        {
+          id: getNewId(),
+          message: errorMessage,
+        },
+      ]
+      if (next.length > MAX_ERROR_COUNT) {
+        next.shift()
+      }
+      return next
+    })
+  }
+
+  const onOperationErrorClose = (index: number): void => {
+    setOperationErrors((prev) => {
+      const next = [...prev]
+      next.splice(index, 1)
+      return next
+    })
+  }
+
+  return {
+    operationErrors,
+    onOperationErrorOccur,
+    onOperationErrorClose,
+  }
+}
diff --git a/packages/cube-frontend-web-app/src/pages/events/tunings/useTuningHostsModal.ts b/packages/cube-frontend-web-app/src/pages/events/tunings/useTuningHostsModal.ts
new file mode 100644
index 000000000..b7d62303c
--- /dev/null
+++ b/packages/cube-frontend-web-app/src/pages/events/tunings/useTuningHostsModal.ts
@@ -0,0 +1,37 @@
+import { useMemo, useState } from 'react'
+import { TuningRow } from './tuningsUtils'
+
+type UseTuningHostsModal = {
+  isHostsModalOpen: boolean
+  rowForHostModal: TuningRow | undefined
+  onShowHostsClick: (row: TuningRow) => void
+  onHostsModalClose: () => void
+}
+
+export const useTuningHostsModal = (rows: TuningRow[]): UseTuningHostsModal => {
+  const [selectedRowId, setSelectedRowId] = useState<string | undefined>(
+    undefined,
+  )
+
+  const rowForHostModal = useMemo<TuningRow | undefined>(() => {
+    if (!selectedRowId) {
+      return undefined
+    }
+    return rows.find((row) => row.id == selectedRowId)
+  }, [rows, selectedRowId])
+
+  const onShowHostsClick = (row: TuningRow): void => {
+    setSelectedRowId(row.id)
+  }
+
+  const onHostsModalClose = (): void => {
+    setSelectedRowId(undefined)
+  }
+
+  return {
+    isHostsModalOpen: !!rowForHostModal,
+    rowForHostModal,
+    onShowHostsClick,
+    onHostsModalClose,
+  }
+}
diff --git a/packages/cube-frontend-web-app/src/pages/events/tunings/useTuningRows.ts b/packages/cube-frontend-web-app/src/pages/events/tunings/useTuningRows.ts
new file mode 100644
index 000000000..b0f007bf9
--- /dev/null
+++ b/packages/cube-frontend-web-app/src/pages/events/tunings/useTuningRows.ts
@@ -0,0 +1,172 @@
+import { Page, TuningsApiListTuningsRequest } from '@cube-frontend/api'
+import { tuningsApi } from '@cube-frontend/web-app/api/cosApi'
+import { DataCenterContext } from '@cube-frontend/web-app/context/DataCenterContext'
+import { isCosApiResponse } from '@cube-frontend/web-app/hooks/useCosRequest/cosRequestUtils'
+import { useCosGetRequest } from '@cube-frontend/web-app/hooks/useCosRequest/useCosGetRequest'
+import { useInterval } from '@cube-frontend/web-app/hooks/useInterval'
+import { isAxiosError } from 'axios'
+import { useContext, useEffect, useMemo, useState } from 'react'
+import { TuningRow, tuningToRow } from './tuningsUtils'
+import { ListTuningsQuery } from './useListTuningsQuery'
+
+type UseTuningRows = {
+  isLoading: boolean
+  rows: TuningRow[]
+  hasModifiedTuning: boolean
+  page: Page | undefined
+  onToggleChange: (rowId: string, enabled: boolean) => Promise<void>
+  resetTuning: (rowId: string) => Promise<void>
+}
+
+const parseErrorMessage = (error: unknown): string | undefined => {
+  if (isAxiosError(error) && isCosApiResponse(error.response)) {
+    return error.response.data?.msg
+  } else if (error instanceof Error) {
+    return error.message
+  }
+  return undefined
+}
+
+export const useTuningRows = (
+  query: ListTuningsQuery,
+  onOperationErrorOccur: (errorMessage: string) => void,
+): UseTuningRows => {
+  const { name: dataCenter } = useContext(DataCenterContext)
+
+  const [rows, setRows] = useState<TuningRow[]>([])
+
+  const {
+    data: listTuningsResponse,
+    hasResponseBeenReceived,
+    getResource: listTunings,
+  } = useCosGetRequest(
+    tuningsApi.listTunings,
+    (): TuningsApiListTuningsRequest => ({
+      dataCenter,
+      host: query.hosts,
+      keyword: query.keyword,
+      modified: query.selectedModified[0],
+      pageNum: query.currentPage,
+      pageSize: query.itemsPerPage,
+    }),
+  )
+
+  useInterval(listTunings, 5000)
+
+  useEffect(() => {
+    const tunings = listTuningsResponse?.tunings ?? []
+    setRows(tunings.map(tuningToRow))
+  }, [listTuningsResponse])
+
+  const hasModifiedTuning = useMemo<boolean>(() => {
+    return rows.some((row) => row.isModified)
+  }, [rows])
+
+  const onError = (error: unknown): void => {
+    const errorMessage = parseErrorMessage(error)
+    if (errorMessage) {
+      onOperationErrorOccur(errorMessage)
+    }
+  }
+
+  const patchRow = (id: string, payload: Partial<TuningRow>): void => {
+    setRows((prevRows) => {
+      const rowIndex = prevRows.findIndex((row) => row.id === id)
+      if (rowIndex < 0) {
+        return prevRows
+      }
+      const nextRows = [...prevRows]
+      Object.assign(nextRows[rowIndex], payload)
+      return nextRows
+    })
+  }
+
+  const onToggleChange = async (
+    rowId: string,
+    enabled: boolean,
+  ): Promise<void> => {
+    const row = rows.find((row) => row.id === rowId)
+    if (!row) {
+      return undefined
+    }
+
+    const enabledBeforeToggle = row.enabled
+
+    patchRow(rowId, {
+      enabled,
+      status: {
+        ...row.status,
+        isUpdating: true,
+      },
+    })
+
+    try {
+      await tuningsApi.enableOrDisableTuning({
+        dataCenter,
+        parameterName: row.name,
+        enableOrDisableTuningRequest: {
+          enable: enabled,
+          hosts: row.hosts.map((host) => host.name),
+        },
+      })
+      // Poll for updates instead of changing the `isUpdating` status on the
+      // client side after the API call, as the client doesn't know if the
+      // tuning entry is still updating or not.
+    } catch (error) {
+      console.error('Toggle tuning error: ', error)
+      patchRow(rowId, {
+        enabled: enabledBeforeToggle,
+        status: {
+          ...row.status,
+          isUpdating: false,
+        },
+      })
+      onError(error)
+    }
+  }
+
+  const resetTuning = async (rowId: string): Promise<void> => {
+    const row = rows.find((row) => row.id === rowId)
+    if (!row) {
+      return
+    }
+
+    patchRow(rowId, {
+      status: {
+        ...row.status,
+        isUpdating: true,
+      },
+    })
+
+    try {
+      await tuningsApi.resetTuning({
+        dataCenter,
+        parameterName: row.name,
+        resetTuningRequest: {
+          hosts: row.hosts.map((host) => host.name),
+        },
+      })
+      // Poll for updates instead of changing the `isUpdating` status on the
+      // client side after the API call, as the client doesn't know if the
+      // tuning entry is still updating or not.
+    } catch (error) {
+      console.error('Reset tuning error: ', error)
+      patchRow(rowId, {
+        status: {
+          ...row.status,
+          isUpdating: false,
+        },
+      })
+      onError(error)
+    }
+  }
+
+  return {
+    isLoading: !hasResponseBeenReceived,
+    rows,
+    hasModifiedTuning,
+    page: listTuningsResponse?.page,
+    onToggleChange,
+    resetTuning,
+  }
+}
diff --git a/packages/cube-frontend-web-app/src/pages/settings/_components/SlackChannels/tableCells/NonEditingActions.tsx b/packages/cube-frontend-web-app/src/pages/settings/_components/SlackChannels/tableCells/NonEditingActions.tsx
index a45dd39cd..229b6c957 100644
--- a/packages/cube-frontend-web-app/src/pages/settings/_components/SlackChannels/tableCells/NonEditingActions.tsx
+++ b/packages/cube-frontend-web-app/src/pages/settings/_components/SlackChannels/tableCells/NonEditingActions.tsx
@@ -1,7 +1,7 @@
 import Delete from '@cube-frontend/ui-library/icons/monochrome/delete.svg?react'
 import Edit from '@cube-frontend/ui-library/icons/monochrome/edit.svg?react'
 import Send from '@cube-frontend/ui-library/icons/monochrome/send.svg?react'
-import { IconActionButton } from '../../IconActionButton'
+import { IconActionButton } from '@cube-frontend/web-app/components/IconActionButton/IconActionButton'
 import { SlackChannelRow } from '../slackChannelsUtils'
 
 type NonEditingActionsProps = {
diff --git a/packages/cube-frontend-web-app/src/pages/settings/_components/email/recipients/tableCells/NonEditingActions.tsx b/packages/cube-frontend-web-app/src/pages/settings/_components/email/recipients/tableCells/NonEditingActions.tsx
index f30d0fe9d..1b2b19ec2 100644
--- a/packages/cube-frontend-web-app/src/pages/settings/_components/email/recipients/tableCells/NonEditingActions.tsx
+++ b/packages/cube-frontend-web-app/src/pages/settings/_components/email/recipients/tableCells/NonEditingActions.tsx
@@ -1,7 +1,7 @@
 import Delete from '@cube-frontend/ui-library/icons/monochrome/delete.svg?react'
 import Edit from '@cube-frontend/ui-library/icons/monochrome/edit.svg?react'
 import Send from '@cube-frontend/ui-library/icons/monochrome/send.svg?react'
-import { IconActionButton } from '../../../IconActionButton'
+import { IconActionButton } from '@cube-frontend/web-app/components/IconActionButton/IconActionButton'
 import { EmailRecipientRow } from '../emailRecipientsUtils'
 
 type NonEditingActionsProps = {
diff --git a/packages/cube-frontend-web-app/src/pages/settings/_components/email/senders/tableCells/NonEditingActions.tsx b/packages/cube-frontend-web-app/src/pages/settings/_components/email/senders/tableCells/NonEditingActions.tsx
index 5bbce3844..97633f1e9 100644
--- a/packages/cube-frontend-web-app/src/pages/settings/_components/email/senders/tableCells/NonEditingActions.tsx
+++ b/packages/cube-frontend-web-app/src/pages/settings/_components/email/senders/tableCells/NonEditingActions.tsx
@@ -1,6 +1,6 @@
 import { CosButton } from '@cube-frontend/ui-library'
 import Edit from '@cube-frontend/ui-library/icons/monochrome/edit.svg?react'
-import { IconActionButton } from '../../../IconActionButton'
+import { IconActionButton } from '@cube-frontend/web-app/components/IconActionButton/IconActionButton'
 import { EmailSenderRow } from '../emailSendersUtils'
 
 type NonEditingActionsProps = {


Reviews:
user: login: lizzy-liang-bigstack
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T08:45:52Z
user: login: lizzy-liang-bigstack
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T09:02:18Z
user: login: steven-chiu-bigstack
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T23:02:12Z
user: login: steven-chiu-bigstack
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T23:20:09Z
user: login: lizzy-liang-bigstack
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T01:12:27Z
user: login: raven-pan
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-10T09:13:05Z
user: login: steven-chiu-bigstack
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T01:46:41Z
user: login: steven-chiu-bigstack
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T02:06:47Z
user: login: steven-chiu-bigstack
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T02:07:23Z

Review comments:
diff_hunk: @@ -0,0 +1,182 @@
+import {
+  ListTuningResponseDataTuningsInnerHostsInner,
+  ListTuningResponseDataTuningsInnerStatus,
+} from '@cube-frontend/api'
+import {
+  CosButton,
+  CosHyperlink,
+  CosLoadingSpinner,
+  CosModal,
+  CosPagination,
+  CosStroke,
+  GetCosBasicTable,
+} from '@cube-frontend/ui-library'
+import Plus from '@cube-frontend/ui-library/icons/monochrome/plus.svg?react'
+import dayjs from 'dayjs'
+import { Link } from 'react-router'
+import { ActionCell } from './_components/tableCells/ActionCell'
+import { TuningsFilter } from './TuningsFilter'
+import { joinHostNames, maxHostsDisplayCount, TuningRow } from './tuningsUtils'
+import { useResetTuningModal } from './uesResetTuningModal'
+import { useListTuningsQuery } from './useListTuningsQuery'
+import { useTuningHostsModal } from './useTuningHostsModal'
+import { useTuningRows } from './useTuningRows'
+
+const TuningTable = GetCosBasicTable<TuningRow>()
+
+export const TuningsPage = () => {
+  const {
+    query,
+    onKeywordChange,
+    onKeywordClear,
+    onModifyStatusItemClick,
+    onNodeItemClick,
+    onNodesAllCheckChange,
+    onPageChange,
+    onItemsPerPageChange,
+  } = useListTuningsQuery()
+
+  const {
+    isLoading,
+    rows,
+    page,
+    hasModifiedTuning,
+    onToggleChange,
+    resetTuning,
+  } = useTuningRows(query)
+
+  const {
+    isHostsModalOpen,
+    rowForHostModal,
+    onShowHostsClick,
+    onHostsModalClose,
+  } = useTuningHostsModal(rows)
+
+  const renderHosts = (
+    hosts: ListTuningResponseDataTuningsInnerHostsInner[],
+    row: TuningRow,
+  ) => {
+    const previewHosts = joinHostNames(hosts.slice(0, maxHostsDisplayCount))
+    if (hosts.length > maxHostsDisplayCount) {
+      return (
+        <>
+          {`${previewHosts},`}
+          <CosHyperlink
+            variant="text-only"
+            onClick={() => onShowHostsClick(row)}
+          >
+            [...]
+          </CosHyperlink>
+        </>
+      )
+    }
+    return previewHosts
+  }
+
+  const renderUpdateTime = (
+    status: ListTuningResponseDataTuningsInnerStatus,
+  ) => {
+    const { updatedAt } = status
+    if (!updatedAt) {
+      return <span className="text-functional-text-light">Never</span>
+    }
+    return dayjs.respectTzOffset(updatedAt).format('YYYY/MM/DD')
+  }
+
+  const { isResetModalOpen, toBeResetRowId, onResetClick, onCloseResetModal } =
+    useResetTuningModal()
+
+  const onConfirmReset = async (): Promise<void> => {
+    if (toBeResetRowId) {
+      onCloseResetModal()
+      await resetTuning(toBeResetRowId)
+    }
+  }
+
+  return (
+    <div
+      className="flex flex-col gap-y-6 rounded-[5px] bg-grey-0 px-6 py-4"
+      style={{
+        boxShadow: '0px 0px 3px 0px rgba(0, 0, 0, 0.10)',
+      }}
+    >
+      <div className="flex flex-col gap-y-4">
+        <h4 className="secondary-h4">Tunings</h4>
+        <Link className="self-start" to="/events/tunings/create">
+          <CosButton usage="icon-left" Icon={Plus}>
+            Create Tuning
+          </CosButton>
+        </Link>
+      </div>
+      <CosStroke type="dot" />
+      <TuningsFilter
+        query={query}
+        onKeywordChange={onKeywordChange}
+        onKeywordClear={onKeywordClear}
+        onModifyStatusItemClick={onModifyStatusItemClick}
+        onNodeItemClick={onNodeItemClick}
+        onNodesAllCheckChange={onNodesAllCheckChange}
+      />
+      <TuningTable isLoading={isLoading} rows={rows}>
+        <TuningTable.Column
+          property="name"
+          label="Name (Keys)"
+          emphasize={true}
+        >
+          {(name, row) => (
+            <div className="flex gap-x-2">
+              <span>{name}</span>
+              {row.status.isUpdating && (
+                <CosLoadingSpinner className="ml-2" variant="dot45" />
+              )}
+            </div>
+          )}
+        </TuningTable.Column>
+        <TuningTable.Column property="hosts" label="Hosts">
+          {renderHosts}
+        </TuningTable.Column>
+        <TuningTable.Column property="status" label="Update Time">
+          {renderUpdateTime}
+        </TuningTable.Column>
+        <TuningTable.Column property="description" label="Description" />
+        <TuningTable.Column property="value" label="Value" />
+        <TuningTable.Column>
+          {(_, row) => (
+            <ActionCell
+              row={row}
+              saveSpaceForResetButton={rows.length === 1 || hasModifiedTuning}
+              onToggleChange={(enabled) => onToggleChange(row.id, enabled)}
+              onResetClick={() => onResetClick(row.id)}
+            />
+          )}
+        </TuningTable.Column>
+      </TuningTable>
+      <CosPagination
+        totalItems={page?.totalItemCount ?? 0}
+        currentPage={query.currentPage}
+        itemsPerPage={query.itemsPerPage}
+        onPageChange={onPageChange}
+        onItemsPerPageChange={onItemsPerPageChange}
+      />
+      <CosModal
+        title="Hosts"
+        size="sm"
+        isOpen={isHostsModalOpen}
+        isActionButtonVisible={false}
+        onCloseClick={onHostsModalClose}
+      >
+        {joinHostNames(rowForHostModal?.hosts)}
+      </CosModal>
+      <CosModal
+        title="Reset Tuning"
+        size="sm"
+        isOpen={isResetModalOpen}
+        actionText="Reset"
+        onActionClick={onConfirmReset}
+        onCloseClick={onCloseResetModal}
+      >
+        Are you sure you want to reset this tuning to default?
+      </CosModal>
path: packages/cube-frontend-web-app/src/pages/events/tunings/TuningsPage.tsx
user: login: lizzy-liang-bigstack
body: I think we need to add styles to the content text here. 
According to the design, it seems to be: `primary-body4 text-functional-text`.

- Current UI:
<img width="880" alt="Screenshot 2025-04-09 at 4 39 22 PM" src="https://github.com/user-attachments/assets/c87f0d1b-302b-4d71-a0e0-6b234c26fe32" />

- Design
<img width="990" alt="Screenshot 2025-04-09 at 4 42 49 PM" src="https://github.com/user-attachments/assets/43c3a9cd-1936-4606-a7b8-ef393c4eddc0" />

created_at: 2025-04-09T08:45:51Z
author_association: COLLABORATOR
diff_hunk: @@ -13,12 +13,16 @@ export type CosModalProps = {
   size?: CosModalSize
   isOpen: boolean
   title: string
+  /**
+   * @default true
+   */
+  isActionButtonVisible?: boolean
   /**
    * @default 'Action'
    */
   actionText?: string
   actionButtonProps?: Pick<CosButtonProps, 'loading' | 'disabled'>
-  onActionClick: () => void
+  onActionClick?: () => void
   onCloseClick: () => void
 }
path: packages/cube-frontend-ui-library/src/components/CosModal/CosModal.tsx
user: login: lizzy-liang-bigstack
body: Do you think it makes sense - to pass `onActionClick` only when `isActionButtonVisible` is `true`?
created_at: 2025-04-09T09:02:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,182 @@
+import {
+  ListTuningResponseDataTuningsInnerHostsInner,
+  ListTuningResponseDataTuningsInnerStatus,
+} from '@cube-frontend/api'
+import {
+  CosButton,
+  CosHyperlink,
+  CosLoadingSpinner,
+  CosModal,
+  CosPagination,
+  CosStroke,
+  GetCosBasicTable,
+} from '@cube-frontend/ui-library'
+import Plus from '@cube-frontend/ui-library/icons/monochrome/plus.svg?react'
+import dayjs from 'dayjs'
+import { Link } from 'react-router'
+import { ActionCell } from './_components/tableCells/ActionCell'
+import { TuningsFilter } from './TuningsFilter'
+import { joinHostNames, maxHostsDisplayCount, TuningRow } from './tuningsUtils'
+import { useResetTuningModal } from './uesResetTuningModal'
+import { useListTuningsQuery } from './useListTuningsQuery'
+import { useTuningHostsModal } from './useTuningHostsModal'
+import { useTuningRows } from './useTuningRows'
+
+const TuningTable = GetCosBasicTable<TuningRow>()
+
+export const TuningsPage = () => {
+  const {
+    query,
+    onKeywordChange,
+    onKeywordClear,
+    onModifyStatusItemClick,
+    onNodeItemClick,
+    onNodesAllCheckChange,
+    onPageChange,
+    onItemsPerPageChange,
+  } = useListTuningsQuery()
+
+  const {
+    isLoading,
+    rows,
+    page,
+    hasModifiedTuning,
+    onToggleChange,
+    resetTuning,
+  } = useTuningRows(query)
+
+  const {
+    isHostsModalOpen,
+    rowForHostModal,
+    onShowHostsClick,
+    onHostsModalClose,
+  } = useTuningHostsModal(rows)
+
+  const renderHosts = (
+    hosts: ListTuningResponseDataTuningsInnerHostsInner[],
+    row: TuningRow,
+  ) => {
+    const previewHosts = joinHostNames(hosts.slice(0, maxHostsDisplayCount))
+    if (hosts.length > maxHostsDisplayCount) {
+      return (
+        <>
+          {`${previewHosts},`}
+          <CosHyperlink
+            variant="text-only"
+            onClick={() => onShowHostsClick(row)}
+          >
+            [...]
+          </CosHyperlink>
+        </>
+      )
+    }
+    return previewHosts
+  }
+
+  const renderUpdateTime = (
+    status: ListTuningResponseDataTuningsInnerStatus,
+  ) => {
+    const { updatedAt } = status
+    if (!updatedAt) {
+      return <span className="text-functional-text-light">Never</span>
+    }
+    return dayjs.respectTzOffset(updatedAt).format('YYYY/MM/DD')
+  }
+
+  const { isResetModalOpen, toBeResetRowId, onResetClick, onCloseResetModal } =
+    useResetTuningModal()
+
+  const onConfirmReset = async (): Promise<void> => {
+    if (toBeResetRowId) {
+      onCloseResetModal()
+      await resetTuning(toBeResetRowId)
+    }
+  }
+
+  return (
+    <div
+      className="flex flex-col gap-y-6 rounded-[5px] bg-grey-0 px-6 py-4"
+      style={{
+        boxShadow: '0px 0px 3px 0px rgba(0, 0, 0, 0.10)',
+      }}
+    >
+      <div className="flex flex-col gap-y-4">
+        <h4 className="secondary-h4">Tunings</h4>
+        <Link className="self-start" to="/events/tunings/create">
+          <CosButton usage="icon-left" Icon={Plus}>
+            Create Tuning
+          </CosButton>
+        </Link>
+      </div>
+      <CosStroke type="dot" />
+      <TuningsFilter
+        query={query}
+        onKeywordChange={onKeywordChange}
+        onKeywordClear={onKeywordClear}
+        onModifyStatusItemClick={onModifyStatusItemClick}
+        onNodeItemClick={onNodeItemClick}
+        onNodesAllCheckChange={onNodesAllCheckChange}
+      />
+      <TuningTable isLoading={isLoading} rows={rows}>
+        <TuningTable.Column
+          property="name"
+          label="Name (Keys)"
+          emphasize={true}
+        >
+          {(name, row) => (
+            <div className="flex gap-x-2">
+              <span>{name}</span>
+              {row.status.isUpdating && (
+                <CosLoadingSpinner className="ml-2" variant="dot45" />
+              )}
+            </div>
+          )}
+        </TuningTable.Column>
+        <TuningTable.Column property="hosts" label="Hosts">
+          {renderHosts}
+        </TuningTable.Column>
+        <TuningTable.Column property="status" label="Update Time">
+          {renderUpdateTime}
+        </TuningTable.Column>
+        <TuningTable.Column property="description" label="Description" />
+        <TuningTable.Column property="value" label="Value" />
+        <TuningTable.Column>
+          {(_, row) => (
+            <ActionCell
+              row={row}
+              saveSpaceForResetButton={rows.length === 1 || hasModifiedTuning}
+              onToggleChange={(enabled) => onToggleChange(row.id, enabled)}
+              onResetClick={() => onResetClick(row.id)}
+            />
+          )}
+        </TuningTable.Column>
+      </TuningTable>
+      <CosPagination
+        totalItems={page?.totalItemCount ?? 0}
+        currentPage={query.currentPage}
+        itemsPerPage={query.itemsPerPage}
+        onPageChange={onPageChange}
+        onItemsPerPageChange={onItemsPerPageChange}
+      />
+      <CosModal
+        title="Hosts"
+        size="sm"
+        isOpen={isHostsModalOpen}
+        isActionButtonVisible={false}
+        onCloseClick={onHostsModalClose}
+      >
+        {joinHostNames(rowForHostModal?.hosts)}
+      </CosModal>
+      <CosModal
+        title="Reset Tuning"
+        size="sm"
+        isOpen={isResetModalOpen}
+        actionText="Reset"
+        onActionClick={onConfirmReset}
+        onCloseClick={onCloseResetModal}
+      >
+        Are you sure you want to reset this tuning to default?
+      </CosModal>
path: packages/cube-frontend-web-app/src/pages/events/tunings/TuningsPage.tsx
user: login: steven-chiu-bigstack
body: Good catch! I'll update the styles accordingly. 🙏 
created_at: 2025-04-09T23:02:12Z
author_association: COLLABORATOR
diff_hunk: @@ -13,12 +13,16 @@ export type CosModalProps = {
   size?: CosModalSize
   isOpen: boolean
   title: string
+  /**
+   * @default true
+   */
+  isActionButtonVisible?: boolean
   /**
    * @default 'Action'
    */
   actionText?: string
   actionButtonProps?: Pick<CosButtonProps, 'loading' | 'disabled'>
-  onActionClick: () => void
+  onActionClick?: () => void
   onCloseClick: () => void
 }
path: packages/cube-frontend-ui-library/src/components/CosModal/CosModal.tsx
user: login: steven-chiu-bigstack
body: Yes, but that'll make it less convenient if we ever want to show/hide the action button dynamically (we don't have that case yet.) For example, the following code should fail the TS check if we make the props typing stricter:

```tsx
const isActionButtonVisible = useMemo<boolean>(() => {
  return someBooleanCalculatedBasedOnOtherPropsOrStates
}, [...])

const onActionClick = () => {
  // ...
}

<CosModal isActionButtonVisible={isActionButtonVisible} onActionClick={onActionClick} />
```

The current interface makes TS accept the above code, so I'd prefer to keep it, if you don’t mind.
created_at: 2025-04-09T23:20:09Z
author_association: COLLABORATOR
diff_hunk: @@ -13,12 +13,16 @@ export type CosModalProps = {
   size?: CosModalSize
   isOpen: boolean
   title: string
+  /**
+   * @default true
+   */
+  isActionButtonVisible?: boolean
   /**
    * @default 'Action'
    */
   actionText?: string
   actionButtonProps?: Pick<CosButtonProps, 'loading' | 'disabled'>
-  onActionClick: () => void
+  onActionClick?: () => void
   onCloseClick: () => void
 }
path: packages/cube-frontend-ui-library/src/components/CosModal/CosModal.tsx
user: login: lizzy-liang-bigstack
body: No problem! 💃
created_at: 2025-04-10T01:12:27Z
author_association: COLLABORATOR
diff_hunk: @@ -13,7 +13,7 @@ export const EventsLayout = () => {
   const location = useLocation()
 
   return (
-    <div>
+    <div className="flex flex-col gap-y-4 px-2 py-1">
path: packages/cube-frontend-web-app/src/pages/events/EventsLayout.tsx
user: login: raven-pan
body: We need to remove the margin top from Event Chart page, so they can share same row gap.

https://github.com/user-attachments/assets/8b80b05e-4b2b-470f-908f-c9adb6c4ca32

created_at: 2025-04-10T07:45:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,149 @@
+import { Page, TuningsApiListTuningsRequest } from '@cube-frontend/api'
+import { tuningsApi } from '@cube-frontend/web-app/api/cosApi'
+import { DataCenterContext } from '@cube-frontend/web-app/context/DataCenterContext'
+import { useCosGetRequest } from '@cube-frontend/web-app/hooks/useCosRequest/useCosGetRequest'
+import { useInterval } from '@cube-frontend/web-app/hooks/useInterval'
+import { useContext, useEffect, useMemo, useState } from 'react'
+import { TuningRow, tuningToRow } from './tuningsUtils'
+import { ListTuningsQuery } from './useListTuningsQuery'
+
+type UseTuningRows = {
+  isLoading: boolean
+  rows: TuningRow[]
+  hasModifiedTuning: boolean
+  page: Page | undefined
+  onToggleChange: (rowId: string, enabled: boolean) => Promise<void>
+  resetTuning: (rowId: string) => Promise<void>
+}
+
+export const useTuningRows = (query: ListTuningsQuery): UseTuningRows => {
+  const { name: dataCenter } = useContext(DataCenterContext)
+
+  const [rows, setRows] = useState<TuningRow[]>([])
+
+  const {
+    data: listTuningsResponse,
+    hasResponseBeenReceived,
+    getResource: listTunings,
+  } = useCosGetRequest(
+    tuningsApi.listTunings,
+    (): TuningsApiListTuningsRequest => ({
+      dataCenter,
+      host: query.hosts,
+      keyword: query.keyword,
+      modified: query.selectedModified[0],
+      pageNum: query.currentPage,
+      pageSize: query.itemsPerPage,
+    }),
+  )
+
+  useInterval(listTunings, 5000)
+
+  useEffect(() => {
+    const tunings = listTuningsResponse?.tunings ?? []
+    setRows(tunings.map(tuningToRow))
+  }, [listTuningsResponse])
+
+  const hasModifiedTuning = useMemo<boolean>(() => {
+    return rows.some((row) => row.isModified)
+  }, [rows])
+
+  const patchRow = (id: string, payload: Partial<TuningRow>): void => {
+    setRows((prevRows) => {
+      const rowIndex = prevRows.findIndex((row) => row.id === id)
+      if (rowIndex < 0) {
+        return prevRows
+      }
+      const nextRows = [...prevRows]
+      Object.assign(nextRows[rowIndex], payload)
+      return nextRows
+    })
+  }
+
+  const onToggleChange = async (
+    rowId: string,
+    enabled: boolean,
+  ): Promise<void> => {
+    const row = rows.find((row) => row.id === rowId)
+    if (!row) {
+      return undefined
+    }
+
+    const enabledBeforeToggle = row.enabled
+
+    patchRow(rowId, {
+      enabled,
+      status: {
+        ...row.status,
+        isUpdating: true,
+      },
+    })
+
+    try {
+      await tuningsApi.enableOrDisableTuning({
+        dataCenter,
+        parameterName: row.name,
+        enableOrDisableTuningRequest: {
+          enable: enabled,
+          hosts: row.hosts.map((host) => host.name),
+        },
+      })
+      // Poll for updates instead of changing the `isUpdating` status on the
+      // client side after the API call, as the client doesn't know if the
+      // tuning entry is still updating or not.
+    } catch (error) {
+      console.error('Toggle tuning error: ', error)
+      patchRow(rowId, {
+        enabled: enabledBeforeToggle,
+        status: {
+          ...row.status,
+          isUpdating: false,
+        },
+      })
+    }
+  }
+
+  const resetTuning = async (rowId: string): Promise<void> => {
+    const row = rows.find((row) => row.id === rowId)
+    if (!row) {
+      return
+    }
+
+    patchRow(rowId, {
+      status: {
+        ...row.status,
+        isUpdating: true,
+      },
+    })
+
+    try {
+      await tuningsApi.resetTuning({
+        dataCenter,
+        parameterName: row.name,
+        resetTuningRequest: {
+          hosts: row.hosts.map((host) => host.name),
+        },
+      })
+      // Poll for updates instead of changing the `isUpdating` status on the
+      // client side after the API call, as the client doesn't know if the
+      // tuning entry is still updating or not.
+    } catch (error) {
+      console.error('Reset tuning error: ', error)
+      patchRow(rowId, {
+        status: {
+          ...row.status,
+          isUpdating: false,
+        },
+      })
+    }
+  }
+
+  return {
+    isLoading: !hasResponseBeenReceived,
path: packages/cube-frontend-web-app/src/pages/events/tunings/useTuningRows.ts
user: login: raven-pan
body: This will cause the tuning table to not show the loading state after the user changes the filter condition or pagination.
However, we can’t use `isLoading` because we have 5-second polling in the UI.
Maybe we can open a separate ticket to handle this issue later?


https://github.com/user-attachments/assets/29310f1a-be91-4491-a260-9793fdef32af


created_at: 2025-04-10T08:02:39Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,98 @@
+import {
+  CosToggle,
+  CosTooltip,
+  CosTooltipInformation,
+} from '@cube-frontend/ui-library'
+import ArrowRotateLeft from '@cube-frontend/ui-library/icons/monochrome/arrow_rotate_left_01.svg?react'
+import Edit from '@cube-frontend/ui-library/icons/monochrome/edit.svg?react'
+import { IconActionButton } from '@cube-frontend/web-app/pages/settings/_components/IconActionButton'
path: packages/cube-frontend-web-app/src/pages/events/tunings/_components/tableCells/ActionCell.tsx
user: login: raven-pan
body: we can move the file to common components folder.
created_at: 2025-04-10T08:58:54Z
author_association: COLLABORATOR
diff_hunk: @@ -13,7 +13,7 @@ export const EventsLayout = () => {
   const location = useLocation()
 
   return (
-    <div>
+    <div className="flex flex-col gap-y-4 px-2 py-1">
path: packages/cube-frontend-web-app/src/pages/events/EventsLayout.tsx
user: login: steven-chiu-bigstack
body: No problem, I'll remove it accordingly, thanks! 🙏 
created_at: 2025-04-11T01:46:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,149 @@
+import { Page, TuningsApiListTuningsRequest } from '@cube-frontend/api'
+import { tuningsApi } from '@cube-frontend/web-app/api/cosApi'
+import { DataCenterContext } from '@cube-frontend/web-app/context/DataCenterContext'
+import { useCosGetRequest } from '@cube-frontend/web-app/hooks/useCosRequest/useCosGetRequest'
+import { useInterval } from '@cube-frontend/web-app/hooks/useInterval'
+import { useContext, useEffect, useMemo, useState } from 'react'
+import { TuningRow, tuningToRow } from './tuningsUtils'
+import { ListTuningsQuery } from './useListTuningsQuery'
+
+type UseTuningRows = {
+  isLoading: boolean
+  rows: TuningRow[]
+  hasModifiedTuning: boolean
+  page: Page | undefined
+  onToggleChange: (rowId: string, enabled: boolean) => Promise<void>
+  resetTuning: (rowId: string) => Promise<void>
+}
+
+export const useTuningRows = (query: ListTuningsQuery): UseTuningRows => {
+  const { name: dataCenter } = useContext(DataCenterContext)
+
+  const [rows, setRows] = useState<TuningRow[]>([])
+
+  const {
+    data: listTuningsResponse,
+    hasResponseBeenReceived,
+    getResource: listTunings,
+  } = useCosGetRequest(
+    tuningsApi.listTunings,
+    (): TuningsApiListTuningsRequest => ({
+      dataCenter,
+      host: query.hosts,
+      keyword: query.keyword,
+      modified: query.selectedModified[0],
+      pageNum: query.currentPage,
+      pageSize: query.itemsPerPage,
+    }),
+  )
+
+  useInterval(listTunings, 5000)
+
+  useEffect(() => {
+    const tunings = listTuningsResponse?.tunings ?? []
+    setRows(tunings.map(tuningToRow))
+  }, [listTuningsResponse])
+
+  const hasModifiedTuning = useMemo<boolean>(() => {
+    return rows.some((row) => row.isModified)
+  }, [rows])
+
+  const patchRow = (id: string, payload: Partial<TuningRow>): void => {
+    setRows((prevRows) => {
+      const rowIndex = prevRows.findIndex((row) => row.id === id)
+      if (rowIndex < 0) {
+        return prevRows
+      }
+      const nextRows = [...prevRows]
+      Object.assign(nextRows[rowIndex], payload)
+      return nextRows
+    })
+  }
+
+  const onToggleChange = async (
+    rowId: string,
+    enabled: boolean,
+  ): Promise<void> => {
+    const row = rows.find((row) => row.id === rowId)
+    if (!row) {
+      return undefined
+    }
+
+    const enabledBeforeToggle = row.enabled
+
+    patchRow(rowId, {
+      enabled,
+      status: {
+        ...row.status,
+        isUpdating: true,
+      },
+    })
+
+    try {
+      await tuningsApi.enableOrDisableTuning({
+        dataCenter,
+        parameterName: row.name,
+        enableOrDisableTuningRequest: {
+          enable: enabled,
+          hosts: row.hosts.map((host) => host.name),
+        },
+      })
+      // Poll for updates instead of changing the `isUpdating` status on the
+      // client side after the API call, as the client doesn't know if the
+      // tuning entry is still updating or not.
+    } catch (error) {
+      console.error('Toggle tuning error: ', error)
+      patchRow(rowId, {
+        enabled: enabledBeforeToggle,
+        status: {
+          ...row.status,
+          isUpdating: false,
+        },
+      })
+    }
+  }
+
+  const resetTuning = async (rowId: string): Promise<void> => {
+    const row = rows.find((row) => row.id === rowId)
+    if (!row) {
+      return
+    }
+
+    patchRow(rowId, {
+      status: {
+        ...row.status,
+        isUpdating: true,
+      },
+    })
+
+    try {
+      await tuningsApi.resetTuning({
+        dataCenter,
+        parameterName: row.name,
+        resetTuningRequest: {
+          hosts: row.hosts.map((host) => host.name),
+        },
+      })
+      // Poll for updates instead of changing the `isUpdating` status on the
+      // client side after the API call, as the client doesn't know if the
+      // tuning entry is still updating or not.
+    } catch (error) {
+      console.error('Reset tuning error: ', error)
+      patchRow(rowId, {
+        status: {
+          ...row.status,
+          isUpdating: false,
+        },
+      })
+    }
+  }
+
+  return {
+    isLoading: !hasResponseBeenReceived,
path: packages/cube-frontend-web-app/src/pages/events/tunings/useTuningRows.ts
user: login: steven-chiu-bigstack
body: Thanks for bringing this up! It looks like there are 2 kinds of loading:

1. First time loading (no response has ever been received): Skeletons are okay here.
2. Any subsequent loading: Skeletons are not ideal, especially for polling with short interval.

We can discuss with the designers what should we do in case #2. Some common solutions are:

1. Makes the table slightly transparent.
2. Covers the table with a mask that has a spinner in the center.

I've created an [issue](https://github.com/bigstack-oss/cube-cos-ui/issues/237) for it.
created_at: 2025-04-11T02:06:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,98 @@
+import {
+  CosToggle,
+  CosTooltip,
+  CosTooltipInformation,
+} from '@cube-frontend/ui-library'
+import ArrowRotateLeft from '@cube-frontend/ui-library/icons/monochrome/arrow_rotate_left_01.svg?react'
+import Edit from '@cube-frontend/ui-library/icons/monochrome/edit.svg?react'
+import { IconActionButton } from '@cube-frontend/web-app/pages/settings/_components/IconActionButton'
path: packages/cube-frontend-web-app/src/pages/events/tunings/_components/tableCells/ActionCell.tsx
user: login: steven-chiu-bigstack
body: No problem. 👌 
created_at: 2025-04-11T02:07:22Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-28T01:03:14Z
body: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>Scanned Files</h2>
None

<!-- dependency-review-pr-comment-marker -->
author_association: NONE
user: login: lizzy-liang-bigstack
created_at: 2025-04-09T09:44:26Z
body: Just wanted to check—will the failed message implementation be handled in a separate ticket after the Notification components are completed?

<img width="823" alt="Screenshot 2025-04-09 at 5 30 12 PM" src="https://github.com/user-attachments/assets/55f8ab60-df29-4342-b60c-e28032b2b20d" />
author_association: COLLABORATOR
user: login: steven-chiu-bigstack
created_at: 2025-04-09T23:23:34Z
body: > Just wanted to check—will the failed message implementation be handled in a separate ticket after the Notification components are completed?

Right, I totally forgot about this! 💫 I think the designers added it to the mockup after this page is implemented. I'll update this PR later today to fix this (using some simple `div`s as notification before #22 is completed.)
Thanks for the reminder! 🙇‍♂️ 
author_association: COLLABORATOR
user: login: steven-chiu-bigstack
created_at: 2025-04-11T02:15:08Z
body: Hi all.

I've made some changes based on your feedback. The inline error notifications are also added to the Tunings page (those messages are parsed directly from the API response; I didn't capitalized them):

https://github.com/user-attachments/assets/4dd07aab-933e-43ab-969f-d059da99cd80

This PR is ready for review again, thanks! 🙏 

author_association: COLLABORATOR