@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
212 lines (211 loc) • 10.8 kB
JavaScript
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());
} }));
}