Browse Source

Add Homelab Dashboard

Penta 2 weeks ago
parent
commit
5c7e1fdcad

+ 8 - 6
console-extensions.json

@@ -3,18 +3,20 @@
     "type": "console.page/route",
     "properties": {
       "exact": true,
-      "path": "/example",
-      "component": { "$codeRef": "ExamplePage" }
+      "path": "/homelab-dashboard",
+      "component": {
+        "$codeRef": "DashboardPage"
+      }
     }
   },
   {
     "type": "console.navigation/href",
     "properties": {
-      "id": "example",
-      "name": "%plugin__console-plugin-template~Plugin example%",
-      "href": "/example",
+      "id": "homelab-dashboard",
+      "name": "Homelab Dashboard",
+      "href": "/homelab-dashboard",
       "perspective": "admin",
       "section": "home"
     }
   }
-]
+]

+ 5 - 6
package.json

@@ -85,16 +85,15 @@
     }
   },
   "consolePlugin": {
-    "name": "console-plugin-template",
+    "name": "homelab-dashboard",
     "version": "0.0.1",
-    "displayName": "OpenShift Console Plugin Template",
-    "description": "Template project for OpenShift Console plugins. Edit package.json to change this message and the plugin name.",
+    "displayName": "Homelab Dashboard",
+    "description": "Custom administration dashboard for the homelab OpenShift cluster",
     "exposedModules": {
-      "ExamplePage": "./components/ExamplePage"
+      "DashboardPage": "./components/DashboardPage"
     },
     "dependencies": {
       "@console/pluginAPI": ">=4.22.0-0"
     }
-  },
-  "packageManager": "yarn@4.14.1"
+  }
 }

+ 185 - 0
src/components/DashboardPage.tsx

@@ -0,0 +1,185 @@
+import {
+  DocumentTitle,
+  K8sResourceCommon,
+  k8sPatch,
+  useK8sModel,
+  useK8sWatchResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+
+import {
+  Alert,
+  Card,
+  CardBody,
+  CardTitle,
+  PageSection,
+  Spinner,
+  Switch,
+  Title,
+} from '@patternfly/react-core';
+
+import type { FC } from 'react';
+import { useState } from 'react';
+
+const TUNED_NAMESPACE = 'openshift-cluster-node-tuning-operator';
+const TUNED_NAME = 'custom-tuned-profile';
+
+const POWERSAVE_PROFILE = 'homelab-powersave';
+const PERFORMANCE_PROFILE = 'homelab-performance';
+
+const TunedGVK = {
+  group: 'tuned.openshift.io',
+  version: 'v1',
+  kind: 'Tuned',
+};
+
+interface TunedResource extends K8sResourceCommon {
+  spec?: {
+    recommend?: Array<{
+      profile?: string;
+      [key: string]: unknown;
+    }>;
+  };
+}
+
+const DashboardPage: FC = () => {
+  const [patching, setPatching] = useState(false);
+  const [patchError, setPatchError] = useState<string>();
+
+  const [tunedModel, modelLoading] = useK8sModel(TunedGVK);
+
+  const [tuned, loaded, loadError] =
+    useK8sWatchResource<TunedResource>({
+      groupVersionKind: TunedGVK,
+      name: TUNED_NAME,
+      namespace: TUNED_NAMESPACE,
+    });
+
+  const recommendations = tuned?.spec?.recommend ?? [];
+
+  const profileIndex = recommendations.findIndex(
+    (recommendation) =>
+      recommendation.profile === POWERSAVE_PROFILE ||
+      recommendation.profile === PERFORMANCE_PROFILE,
+  );
+
+  const currentProfile =
+    profileIndex >= 0
+      ? recommendations[profileIndex]?.profile
+      : undefined;
+
+  const performanceEnabled =
+    currentProfile === PERFORMANCE_PROFILE;
+
+  const profileKnown =
+    currentProfile === POWERSAVE_PROFILE ||
+    currentProfile === PERFORMANCE_PROFILE;
+
+  const changePerformanceMode = async (enabled: boolean) => {
+    if (!tunedModel || !tuned || profileIndex < 0) {
+      return;
+    }
+
+    setPatching(true);
+    setPatchError(undefined);
+
+    try {
+      await k8sPatch({
+        model: tunedModel,
+        resource: tuned,
+        data: [
+          {
+            op: 'replace',
+            path: `/spec/recommend/${profileIndex}/profile`,
+            value: enabled
+              ? PERFORMANCE_PROFILE
+              : POWERSAVE_PROFILE,
+          },
+        ],
+      });
+    } catch (error) {
+      setPatchError(
+        error instanceof Error
+          ? error.message
+          : String(error),
+      );
+    } finally {
+      setPatching(false);
+    }
+  };
+
+  return (
+    <>
+      <DocumentTitle>Dashboard</DocumentTitle>
+
+      <PageSection>
+        <Title headingLevel="h1" size="2xl">
+          Homelab Dashboard
+        </Title>
+      </PageSection>
+
+      <PageSection>
+        <Card>
+          <CardTitle>Cluster performance</CardTitle>
+
+          <CardBody>
+            {!loaded || modelLoading ? (
+              <Spinner />
+            ) : loadError ? (
+              <Alert
+                isInline
+                variant="danger"
+                title="Unable to read TuneD configuration"
+              >
+                {String(loadError)}
+              </Alert>
+            ) : (
+              <>
+                <Switch
+                  id="cluster-performance-mode"
+                  label="Performance mode"
+                  isChecked={performanceEnabled}
+                  isDisabled={patching || !profileKnown}
+                  onChange={(_, checked) =>
+                    void changePerformanceMode(checked)
+                  }
+                /><br />
+
+                <p>
+                  Current profile:{' '}
+                  <strong>
+                    {currentProfile ?? 'Unknown'}
+                  </strong>
+                </p>
+
+                {patching && <Spinner size="md" />}
+
+                {!profileKnown && (
+                  <Alert
+                    isInline
+                    variant="warning"
+                    title="Unknown TuneD profile"
+                  >
+                    Expected {POWERSAVE_PROFILE} or{' '}
+                    {PERFORMANCE_PROFILE}.
+                  </Alert>
+                )}
+
+                {patchError && (
+                  <Alert
+                    isInline
+                    variant="danger"
+                    title="Unable to change performance mode"
+                  >
+                    {patchError}
+                  </Alert>
+                )}
+              </>
+            )}
+          </CardBody>
+        </Card>
+      </PageSection>
+    </>
+  );
+};
+
+export default DashboardPage;

+ 0 - 14
src/components/ExamplePage.spec.tsx

@@ -1,14 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import ExamplePage from './ExamplePage';
-
-describe('ExamplePage', () => {
-  it('renders the page heading', () => {
-    render(<ExamplePage />);
-    expect(screen.getByRole('heading', { name: 'Hello, plugin!' })).toBeInTheDocument();
-  });
-
-  it('renders the success message', () => {
-    render(<ExamplePage />);
-    expect(screen.getByText('Your plugin is working.')).toBeInTheDocument();
-  });
-});

+ 0 - 43
src/components/ExamplePage.tsx

@@ -1,43 +0,0 @@
-import { DocumentTitle, ListPageHeader } from '@openshift-console/dynamic-plugin-sdk';
-import { Trans, useTranslation } from 'react-i18next';
-import { Content, PageSection } from '@patternfly/react-core';
-import { CheckCircleIcon } from '@patternfly/react-icons';
-import type { FC } from 'react';
-
-import './example.css';
-
-const ExamplePage: FC = () => {
-  const { t } = useTranslation('plugin__console-plugin-template');
-
-  return (
-    <>
-      <DocumentTitle>{t('Hello, plugin!')}</DocumentTitle>
-      <ListPageHeader title={t('Hello, plugin!')} />
-      <PageSection>
-        <Content component="p">
-          <span className="console-plugin-template__nice">
-            <CheckCircleIcon /> {t('Success!')}
-          </span>{' '}
-          {t('Your plugin is working.')}
-        </Content>
-        <Content component="p">
-          <Trans t={t}>
-            This is a custom page contributed by the console plugin template. The extension that
-            adds the page is declared in console-extensions.json in the project root along with the
-            corresponding nav item. Update console-extensions.json to change or add extensions. Code
-            references in console-extensions.json must have a corresponding property{' '}
-            <code>exposedModules</code> in package.json mapping the reference to the module.
-          </Trans>
-        </Content>
-        <Content component="p">
-          <Trans t={t}>
-            After cloning this project, replace references to <code>console-template-plugin</code>{' '}
-            and other plugin metadata in package.json with values for your plugin.
-          </Trans>
-        </Content>
-      </PageSection>
-    </>
-  );
-};
-
-export default ExamplePage;

+ 0 - 7
src/components/example.css

@@ -1,7 +0,0 @@
-/* Prefixing your CSS classes with your plugin name is a best practice to avoid
- * collisions with other plugin styles. */
-.console-plugin-template__nice {
-  /* Use PatternFly semantic tokens for colors to support theming.
-   * https://www.patternfly.org/tokens/all-patternfly-tokens */
-  color: var(--pf-t--global--color--brand--default);
-}