@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
186 lines (185 loc) • 10.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { Icon } from '@iconify/react';
import { Editor, useMonaco } from '@monaco-editor/react';
import { Alert, Box, Button, CircularProgress, GlobalStyles, IconButton, Tooltip, useTheme, } from '@mui/material';
import { useDeferredValue, useEffect, useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Activity } from '../activity/Activity';
import { EditorDialog, Link } from '../common';
import ResourceTable from '../common/Resource/ResourceTable';
import { canRenderDetails } from '../resourceMap/details/KubeNodeDetails';
import { KubeIcon } from '../resourceMap/kubeIcon/KubeIcon';
import { searchWithQuery } from './utils/searchWithQuery';
import { useKubeLists } from './utils/useKubeLists';
import { useTypeDefinition } from './utils/useTypeDefinition';
const editorOptions = {
minimap: { enabled: false },
scrollbar: {
vertical: 'hidden',
horizontal: 'auto',
},
lineNumbers: 'off',
scrollBeyondLastLine: false,
wordWrap: 'off', // Ensure it stays on one line unless explicitly wrapped
folding: false,
glyphMargin: false,
lineDecorationsWidth: 16,
renderLineHighlight: 'none', // Don't highlight the current line
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
automaticLayout: true, // Adjust layout on container resize
fontSize: 16,
};
const maxResults = 1000;
export function ResourceSearch({ resources, selectedClusters, rawQuery, maxItemsPerResource, refetchIntervalMs, setRawQuery, }) {
const { t } = useTranslation();
const [isFocused, setIsFocused] = useState(false);
const theme = useTheme();
const monaco = useMonaco();
const { items: allItems, errors, isLoading, } = useKubeLists(resources, selectedClusters, maxItemsPerResource, refetchIntervalMs);
const jsonDataItems = useMemo(() => allItems.map(it => {
delete it.jsonData.metadata.managedFields;
return it.jsonData;
}), [allItems]);
const deferredQuery = useDeferredValue(rawQuery);
const [results, setResults] = useState({ items: [], query: '' });
// Perform search when query changes
useEffect(() => {
const interruptRef = { current: false };
searchWithQuery(allItems, deferredQuery, interruptRef).then(result => {
if (interruptRef.current === false) {
setResults({ items: result.results, query: deferredQuery });
}
});
return () => {
interruptRef.current = true;
};
}, [deferredQuery, allItems]);
const deferredResults = useDeferredValue(results);
const fullTypeDefinition = useTypeDefinition(jsonDataItems, 1000);
useEffect(() => {
if (monaco) {
monaco.languages.typescript.javascriptDefaults.setExtraLibs([
{
content: fullTypeDefinition,
filePath: 'globalTypes.d.ts',
},
]);
}
}, [fullTypeDefinition, monaco]);
const shouldHideTable =
// When items are still loading
isLoading ||
// When there's no query
rawQuery.trim().length === 0 ||
// Or results are not ready yet
(deferredResults.items.length === 0 && deferredResults.query !== rawQuery);
function handleEditorWillMount(monaco) {
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ESNext,
noLib: false,
allowNonTsExtensions: true,
lib: ['esnext'],
});
}
return (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: theme => ({
display: 'flex',
alignItems: 'center',
border: '1px solid',
borderColor: theme.palette.divider,
padding: 1,
borderRadius: '8px',
boxShadow: '2px 2px 15px rgba(0,0,0,0.07)',
position: 'relative',
}), children: [_jsx(GlobalStyles, { styles: {
'.monaco-editor, .monaco-editor-background, .monaco-editor .margin': {
background: 'transparent !important',
backgroundColor: 'transparent !important',
outlineColor: 'transparent !important',
},
} }), !isFocused && rawQuery === '' && (_jsx(Box, { sx: theme => ({
position: 'absolute',
opacity: 0.6,
left: theme.spacing(3),
}), children: _jsx(Trans, { children: "Search resources by query" }) })), _jsx(Box, { sx: { paddingY: 1, width: '100%' }, children: _jsx(Editor, { options: editorOptions, language: "javascript", beforeMount: handleEditorWillMount, value: rawQuery, theme: theme.palette.mode === 'dark' ? 'vs-dark' : 'light', wrapperProps: {
onFocus: () => setIsFocused(true),
onBlur: () => setIsFocused(false),
}, onChange: value => {
setRawQuery(value);
}, onMount: editor => {
editor.onDidContentSizeChange(size => {
const editorNode = editor.getDomNode();
if (editorNode) {
editorNode.style.height = size.contentHeight + 'px';
}
});
} }) }), _jsx(Tooltip, { title: _jsx(Trans, { children: "Clear" }), sx: { opacity: rawQuery.length ? 1 : 0 }, children: _jsx(IconButton, { onClick: () => setRawQuery(''), "aria-label": t('Clear'), children: _jsx(Icon, { icon: "mdi:close" }) }) })] }), _jsxs(Box, { sx: {
fontSize: '0.85rem',
opacity: 0.7,
ml: 1,
height: '32px',
display: 'flex',
gap: 1,
alignItems: 'center',
}, children: [resources.length === 0 && (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(Icon, { icon: "mdi:warning" }), _jsx(Trans, { children: "No resources selected" })] })), resources.length > 0 && isLoading && _jsx(CircularProgress, { size: 18 }), resources.length > 0 && !isLoading && (_jsx(Box, { children: t('Loaded {{0}} items', { 0: allItems.length }) })), errors.length > 0 && (_jsx(Tooltip, { title: errors
.slice(0, 5)
.map(it => it.resource.kind)
.join(', ') + (errors.length > 5 ? ', +' + (errors.length - 5) : ''), children: _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(Icon, { icon: "mdi:warning" }), " ", _jsx(Trans, { children: "Some resources failed to load" })] }) }))] }), deferredResults.items.length > maxResults && (_jsx(Alert, { severity: "warning", sx: {
color: theme.palette.text.primary,
}, children: t('Found {{0}} results. Showing first {{1}}', {
0: deferredResults.items.length,
1: maxResults,
}) })), _jsx(Box, { sx: {
overflowY: 'auto',
}, children: !shouldHideTable && (_jsx(ResourceTable, { id: "headlamp-advanced-search", defaultSortingColumn: { id: 'name', desc: false }, columns: [
'kind',
{
id: 'name',
label: t('translation|Name'),
gridTemplate: 'auto',
getValue: item => item.metadata.name,
render: item => canRenderDetails(item.kind) ? _jsx(Link, { kubeObject: item }) : item.metadata.name,
},
'namespace',
{
label: t('translation|Cluster'),
getValue: item => item.cluster,
render: item => _jsx(Box, { sx: { whiteSpace: 'nowrap' }, children: item.cluster }),
gridTemplate: 'min-content',
},
'age',
{
label: t('translation|Actions'),
getValue: () => '',
gridTemplate: 'min-content',
render: item => _jsx(ViewYaml, { item: item }),
},
], data: deferredResults.items.slice(0, maxResults) ?? [], hideColumns: selectedClusters.length > 1 ? undefined : ['cluster'] })) })] }));
}
export function ViewYaml({ item }) {
return (_jsx(_Fragment, { children: _jsx(Button, { size: "small", variant: "contained", color: "secondary", onClick: () => {
const id = 'view-yaml-' + item.metadata.uid;
Activity.launch({
id,
location: 'full',
icon: _jsx(KubeIcon, { kind: item.kind }),
title: item.kind + ': ' + item.metadata.name,
content: (_jsx(EditorDialog, { noDialog: true, open: true, item: item.jsonData, onClose: () => Activity.close(id), onSave: null })),
});
}, sx: { whiteSpace: 'nowrap' }, startIcon: _jsx(Icon, { icon: "mdi:eye" }), children: "YAML" }) }));
}