UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

212 lines (211 loc) 10.8 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; /* * Copyright 2025 The Kubernetes Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Link from '@mui/material/Link'; import { useTheme } from '@mui/material/styles'; import Switch from '@mui/material/Switch'; import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { isElectron } from '../../../helpers/isElectron'; import { useFilterFunc } from '../../../lib/util'; import { reloadPage, setPluginSettings } from '../../../plugin/pluginsSlice'; import { useTypedSelector } from '../../../redux/reducers/reducers'; import { Link as HeadlampLink } from '../../common/'; import SectionBox from '../../common/SectionBox'; import SectionFilterHeader from '../../common/SectionFilterHeader'; import Table from '../../common/Table'; const EnableSwitch = (props) => { const theme = useTheme(); return (_jsx(Switch, { focusVisibleClassName: ".Mui-focusVisible", disableRipple: true, sx: { width: 42, height: 26, padding: 0, '& .MuiSwitch-switchBase': { padding: 0, margin: '2px', transitionDuration: '300ms', '&.Mui-checked': { transform: 'translateX(16px)', color: '#fff', '& + .MuiSwitch-track': { backgroundColor: theme.palette.mode === 'dark' ? '#2ECA45' : '#0078d4', opacity: 1, border: 0, }, '&.Mui-disabled + .MuiSwitch-track': { opacity: 0.5, }, }, '&.Mui-focusVisible .MuiSwitch-thumb': { color: '#33cf4d', border: '6px solid #fff', }, '&.Mui-disabled .MuiSwitch-thumb': { color: theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[600], }, '&.Mui-disabled + .MuiSwitch-track': { opacity: theme.palette.mode === 'light' ? 0.7 : 0.3, }, }, '& .MuiSwitch-thumb': { boxSizing: 'border-box', width: 22, height: 22, }, '& .MuiSwitch-track': { borderRadius: 26 / 2, backgroundColor: theme.palette.mode === 'light' ? '#E9E9EA' : '#39393D', opacity: 1, transition: theme.transitions.create(['background-color'], { duration: 500, }), }, }, ...props })); }; /** PluginSettingsPure is the main component to where we render the plugin data. */ export function PluginSettingsPure(props) { const { t } = useTranslation(['translation']); /** Plugin arr to be rendered to the page from prop data */ const pluginArr = props.plugins ? props.plugins : []; /** enableSave state enables the save button when changes are made to the plugin list */ const [enableSave, setEnableSave] = useState(false); /** * pluginChanges state is the array of plugin data and any current changes made by the user to a plugin's "Enable" field via toggler. * The name and origin fields are split for consistency. */ const [pluginChanges, setPluginChanges] = useState(() => pluginArr.map((plugin) => { const [author, name] = plugin.name.includes('@') ? plugin.name.split(/\/(.+)/) : [null, plugin.name]; return { ...plugin, displayName: name ?? plugin.name, origin: plugin.origin ?? author?.substring(1) ?? t('translation|Unknown'), }; })); /** * useEffect to control the rendering of the save button. * By default, the enableSave is set to false. * If props.plugins matches pluginChanges enableSave is set to false, disabling the save button. */ useEffect(() => { /** This matcher function compares the fields of name and isEnabled of each object in props.plugins to each object in pluginChanges */ function matcher(objA, objB) { return objA.name === objB.name && objA.isEnabled === objB.isEnabled; } /** * arrayComp returns true if each object in both arrays are identical by name and isEnabled. * If both arrays are identical in this scope, then no changes need to be saved. * If they do not match, there are changes in the pluginChanges array that can be saved and thus enableSave should be enabled. */ const arrayComp = props.plugins.every((val, key) => matcher(val, pluginChanges[key])); /** For storybook usage, determines if the save button should be enabled by default */ if (props.saveAlwaysEnable) { setEnableSave(true); } else { if (arrayComp) { setEnableSave(false); } if (!arrayComp) { setEnableSave(true); } } }, [pluginChanges]); /** * onSaveButton function to be called once the user clicks the Save button. * This function then takes the current state of the pluginChanges array and inputs it to the onSave prop function. */ function onSaveButtonHandler() { props.onSave(pluginChanges); } /** * On change function handler to control the enableSave state and update the pluginChanges state. * This function is called on every plugin toggle action and recreates the state for pluginChanges. * Once the user clicks a toggle, the Save button is also rendered via setEnableSave. */ function switchChangeHanlder(plug) { const plugName = plug.name; setPluginChanges((currentInfo) => currentInfo.map((p) => { if (p.name === plugName) { return { ...p, isEnabled: !p.isEnabled }; } return p; })); } return (_jsxs(_Fragment, { children: [_jsx(SectionBox, { title: _jsx(SectionFilterHeader, { title: t('translation|Plugins'), noNamespaceFilter: true }), children: _jsx(Table, { columns: [ { header: t('translation|Name'), accessorKey: 'name', muiTableBodyCellProps: { sx: { flexDirection: 'column', alignItems: 'flex-start', width: 'unset', }, }, Cell: ({ row: { original: plugin } }) => { return (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "subtitle1", children: _jsx(HeadlampLink, { routeName: 'pluginDetails', params: { name: plugin.name }, align: "right", children: plugin.displayName }) }), _jsx(Typography, { variant: "caption", children: plugin.version })] })); }, }, { header: t('translation|Description'), accessorKey: 'description', }, { header: t('translation|Origin'), Cell: ({ row: { original: plugin } }) => { const url = plugin?.homepage || plugin?.repository?.url; return plugin?.origin ? (url ? (_jsx(Link, { href: url, children: plugin.origin })) : (plugin?.origin)) : (t('translation|Unknown')); }, }, // TODO: Fetch the plugin status from the plugin settings store { header: t('translation|Status'), accessorFn: (plugin) => { if (plugin.isCompatible === false) { return t('translation|Incompatible'); } return plugin.isEnabled ? t('translation|Enabled') : t('translation|Disabled'); }, }, { header: t('translation|Enable'), accessorFn: (plugin) => plugin.isEnabled, Cell: ({ row: { original: plugin } }) => { if (!plugin.isCompatible || !isElectron()) { return null; } return (_jsx(EnableSwitch, { "aria-label": `Toggle ${plugin.name}`, checked: !!plugin.isEnabled, onChange: () => switchChangeHanlder(plugin), color: "primary", name: plugin.name })); }, }, ] // remove the enable column if we're not in app mode .filter(el => !(el.header === t('translation|Enable') && !isElectron())), data: pluginChanges, filterFunction: useFilterFunc(['.name']) }) }), enableSave && (_jsx(Box, { sx: { display: `flex`, justifyContent: `flex-end`, margin: `5px` }, children: _jsx(Button, { variant: "contained", color: "primary", sx: { margin: `5px` }, onClick: () => onSaveButtonHandler(), children: t('translation|Save & Apply') }) }))] })); } /** Container function for the PluginSettingsPure, onSave prop returns plugins */ export default function PluginSettings() { const dispatch = useDispatch(); const pluginSettings = useTypedSelector(state => state.plugins.pluginSettings); return (_jsx(PluginSettingsPure, { plugins: pluginSettings, onSave: plugins => { dispatch(setPluginSettings(plugins)); dispatch(reloadPage()); } })); }