mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-17 20:51:55 +01:00
implement a config error button + message modal that shows the errors (#1030)
This commit is contained in:
parent
a15b339f39
commit
a629b28194
6
frontend/app/modals/messagemodal.less
Normal file
6
frontend/app/modals/messagemodal.less
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
.message-modal {
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
24
frontend/app/modals/messagemodal.tsx
Normal file
24
frontend/app/modals/messagemodal.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { Modal } from "@/app/modals/modal";
|
||||||
|
import { modalsModel } from "@/app/store/modalmodel";
|
||||||
|
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import "./messagemodal.less";
|
||||||
|
|
||||||
|
const MessageModal = ({ children }: { children: ReactNode }) => {
|
||||||
|
function closeModal() {
|
||||||
|
modalsModel.popModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal className="message-modal" onOk={() => closeModal()} onClose={() => closeModal()}>
|
||||||
|
{children}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MessageModal.displayName = "MessageModal";
|
||||||
|
|
||||||
|
export { MessageModal };
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { MessageModal } from "@/app/modals/messagemodal";
|
||||||
import { AboutModal } from "./about";
|
import { AboutModal } from "./about";
|
||||||
import { TosModal } from "./tos";
|
import { TosModal } from "./tos";
|
||||||
import { UserInputModal } from "./userinputmodal";
|
import { UserInputModal } from "./userinputmodal";
|
||||||
@ -9,6 +10,7 @@ const modalRegistry: { [key: string]: React.ComponentType<any> } = {
|
|||||||
[TosModal.displayName || "TosModal"]: TosModal,
|
[TosModal.displayName || "TosModal"]: TosModal,
|
||||||
[UserInputModal.displayName || "UserInputModal"]: UserInputModal,
|
[UserInputModal.displayName || "UserInputModal"]: UserInputModal,
|
||||||
[AboutModal.displayName || "AboutModal"]: AboutModal,
|
[AboutModal.displayName || "AboutModal"]: AboutModal,
|
||||||
|
[MessageModal.displayName || "MessageModal"]: MessageModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getModalComponent = (key: string): React.ComponentType<any> | undefined => {
|
export const getModalComponent = (key: string): React.ComponentType<any> | undefined => {
|
||||||
|
@ -12,6 +12,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-error-message {
|
||||||
|
max-width: 500px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-bar-wrapper {
|
.tab-bar-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -53,6 +63,13 @@
|
|||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-error-button {
|
||||||
|
height: 80%;
|
||||||
|
margin: auto 4px;
|
||||||
|
color: black;
|
||||||
|
flex: 0 0 fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
.add-tab-btn {
|
.add-tab-btn {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { Button } from "@/app/element/button";
|
||||||
|
import { modalsModel } from "@/app/store/modalmodel";
|
||||||
import { WindowDrag } from "@/element/windowdrag";
|
import { WindowDrag } from "@/element/windowdrag";
|
||||||
import { deleteLayoutModelForTab } from "@/layout/index";
|
import { deleteLayoutModelForTab } from "@/layout/index";
|
||||||
import { atoms, getApi, isDev, PLATFORM } from "@/store/global";
|
import { atoms, getApi, isDev, PLATFORM } from "@/store/global";
|
||||||
@ -37,6 +39,69 @@ interface TabBarProps {
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ConfigErrorMessage = () => {
|
||||||
|
const fullConfig = useAtomValue(atoms.fullConfigAtom);
|
||||||
|
|
||||||
|
if (fullConfig?.configerrors == null || fullConfig?.configerrors.length == 0) {
|
||||||
|
return (
|
||||||
|
<div className="config-error-message">
|
||||||
|
<h3>Configuration Clean</h3>
|
||||||
|
<p>There are no longer any errors detected in your config.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (fullConfig?.configerrors.length == 1) {
|
||||||
|
const singleError = fullConfig.configerrors[0];
|
||||||
|
return (
|
||||||
|
<div className="config-error-message">
|
||||||
|
<h3>Configuration Error</h3>
|
||||||
|
<div>
|
||||||
|
{singleError.file}: {singleError.err}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="config-error-message">
|
||||||
|
<h3>Configuration Error</h3>
|
||||||
|
<ul>
|
||||||
|
{fullConfig.configerrors.map((error, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
{error.file}: {error.err}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConfigErrorIcon = ({ buttonRef }: { buttonRef: React.RefObject<HTMLElement> }) => {
|
||||||
|
const fullConfig = useAtomValue(atoms.fullConfigAtom);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
modalsModel.pushModal("MessageModal", { children: <ConfigErrorMessage /> });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullConfig?.configerrors == null || fullConfig?.configerrors.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={buttonRef as React.RefObject<HTMLButtonElement>}
|
||||||
|
className="config-error-button red"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<i className="fa fa-solid fa-exclamation-triangle" />
|
||||||
|
Config Error
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="config-error" ref={buttonRef as React.RefObject<HTMLDivElement>}>
|
||||||
|
<i className="fa fa-solid fa-exclamation-triangle" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||||
const [tabIds, setTabIds] = useState<string[]>([]);
|
const [tabIds, setTabIds] = useState<string[]>([]);
|
||||||
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
||||||
@ -67,6 +132,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
|||||||
const tabWidthRef = useRef<number>(TAB_DEFAULT_WIDTH);
|
const tabWidthRef = useRef<number>(TAB_DEFAULT_WIDTH);
|
||||||
const scrollableRef = useRef<boolean>(false);
|
const scrollableRef = useRef<boolean>(false);
|
||||||
const updateStatusButtonRef = useRef<HTMLButtonElement>(null);
|
const updateStatusButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const configErrorButtonRef = useRef<HTMLElement>(null);
|
||||||
const prevAllLoadedRef = useRef<boolean>(false);
|
const prevAllLoadedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
const windowData = useAtomValue(atoms.waveWindow);
|
const windowData = useAtomValue(atoms.waveWindow);
|
||||||
@ -124,8 +190,10 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
|||||||
const windowDragLeftWidth = draggerLeftRef.current.getBoundingClientRect().width;
|
const windowDragLeftWidth = draggerLeftRef.current.getBoundingClientRect().width;
|
||||||
const addBtnWidth = addBtnRef.current.getBoundingClientRect().width;
|
const addBtnWidth = addBtnRef.current.getBoundingClientRect().width;
|
||||||
const updateStatusLabelWidth = updateStatusButtonRef.current?.getBoundingClientRect().width ?? 0;
|
const updateStatusLabelWidth = updateStatusButtonRef.current?.getBoundingClientRect().width ?? 0;
|
||||||
|
const configErrorWidth = configErrorButtonRef.current?.getBoundingClientRect().width ?? 0;
|
||||||
const spaceForTabs =
|
const spaceForTabs =
|
||||||
tabbarWrapperWidth - (windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth + updateStatusLabelWidth);
|
tabbarWrapperWidth -
|
||||||
|
(windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth + updateStatusLabelWidth + configErrorWidth);
|
||||||
|
|
||||||
const numberOfTabs = tabIds.length;
|
const numberOfTabs = tabIds.length;
|
||||||
const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH;
|
const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH;
|
||||||
@ -510,6 +578,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<WindowDrag ref={draggerRightRef} className="right" />
|
<WindowDrag ref={draggerRightRef} className="right" />
|
||||||
<UpdateStatusBanner buttonRef={updateStatusButtonRef} />
|
<UpdateStatusBanner buttonRef={updateStatusButtonRef} />
|
||||||
|
<ConfigErrorIcon buttonRef={configErrorButtonRef} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,7 @@ import "./updatebanner.less";
|
|||||||
|
|
||||||
const UpdateStatusBannerComponent = ({ buttonRef }: { buttonRef: React.RefObject<HTMLButtonElement> }) => {
|
const UpdateStatusBannerComponent = ({ buttonRef }: { buttonRef: React.RefObject<HTMLButtonElement> }) => {
|
||||||
const appUpdateStatus = useAtomValue(atoms.updaterStatusAtom);
|
const appUpdateStatus = useAtomValue(atoms.updaterStatusAtom);
|
||||||
const [updateStatusMessage, setUpdateStatusMessage] = useState<string>();
|
let [updateStatusMessage, setUpdateStatusMessage] = useState<string>();
|
||||||
const [dismissBannerTimeout, setDismissBannerTimeout] = useState<NodeJS.Timeout>();
|
const [dismissBannerTimeout, setDismissBannerTimeout] = useState<NodeJS.Timeout>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -52,7 +52,6 @@ const UpdateStatusBannerComponent = ({ buttonRef }: { buttonRef: React.RefObject
|
|||||||
function onClick() {
|
function onClick() {
|
||||||
getApi().installAppUpdate();
|
getApi().installAppUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateStatusMessage) {
|
if (updateStatusMessage) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -932,3 +932,17 @@ func WriteFileIfDifferent(fileName string, contents []byte) (bool, error) {
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetLineColFromOffset(barr []byte, offset int) (int, int) {
|
||||||
|
line := 1
|
||||||
|
col := 1
|
||||||
|
for i := 0; i < offset && i < len(barr); i++ {
|
||||||
|
if barr[i] == '\n' {
|
||||||
|
line++
|
||||||
|
col = 1
|
||||||
|
} else {
|
||||||
|
col++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line, col
|
||||||
|
}
|
||||||
|
@ -20,6 +20,13 @@ import (
|
|||||||
|
|
||||||
const SettingsFile = "settings.json"
|
const SettingsFile = "settings.json"
|
||||||
|
|
||||||
|
const AnySchema = `
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
type MetaSettingsType struct {
|
type MetaSettingsType struct {
|
||||||
waveobj.MetaMapType
|
waveobj.MetaMapType
|
||||||
}
|
}
|
||||||
@ -117,10 +124,37 @@ type FullConfigType struct {
|
|||||||
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
|
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func goBackWS(barr []byte, offset int) int {
|
||||||
|
if offset >= len(barr) {
|
||||||
|
offset = offset - 1
|
||||||
|
}
|
||||||
|
for i := offset - 1; i >= 0; i-- {
|
||||||
|
if barr[i] == ' ' || barr[i] == '\t' || barr[i] == '\n' || barr[i] == '\r' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTrailingCommaError(barr []byte, offset int) bool {
|
||||||
|
if offset >= len(barr) {
|
||||||
|
offset = offset - 1
|
||||||
|
}
|
||||||
|
offset = goBackWS(barr, offset)
|
||||||
|
if barr[offset] == '}' {
|
||||||
|
offset = goBackWS(barr, offset)
|
||||||
|
if barr[offset] == ',' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func readConfigHelper(fileName string, barr []byte, readErr error) (waveobj.MetaMapType, []ConfigError) {
|
func readConfigHelper(fileName string, barr []byte, readErr error) (waveobj.MetaMapType, []ConfigError) {
|
||||||
var cerrs []ConfigError
|
var cerrs []ConfigError
|
||||||
if readErr != nil && !os.IsNotExist(readErr) {
|
if readErr != nil && !os.IsNotExist(readErr) {
|
||||||
cerrs = append(cerrs, ConfigError{File: "defaults:" + fileName, Err: readErr.Error()})
|
cerrs = append(cerrs, ConfigError{File: fileName, Err: readErr.Error()})
|
||||||
}
|
}
|
||||||
if len(barr) == 0 {
|
if len(barr) == 0 {
|
||||||
return nil, cerrs
|
return nil, cerrs
|
||||||
@ -128,7 +162,20 @@ func readConfigHelper(fileName string, barr []byte, readErr error) (waveobj.Meta
|
|||||||
var rtn waveobj.MetaMapType
|
var rtn waveobj.MetaMapType
|
||||||
err := json.Unmarshal(barr, &rtn)
|
err := json.Unmarshal(barr, &rtn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cerrs = append(cerrs, ConfigError{File: "defaults:" + fileName, Err: err.Error()})
|
if syntaxErr, ok := err.(*json.SyntaxError); ok {
|
||||||
|
offset := syntaxErr.Offset
|
||||||
|
if offset > 0 {
|
||||||
|
offset = offset - 1
|
||||||
|
}
|
||||||
|
lineNum, colNum := utilfn.GetLineColFromOffset(barr, int(offset))
|
||||||
|
isTrailingComma := isTrailingCommaError(barr, int(offset))
|
||||||
|
if isTrailingComma {
|
||||||
|
err = fmt.Errorf("json syntax error at line %d, col %d: probably an extra trailing comma: %v", lineNum, colNum, syntaxErr)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("json syntax error at line %d, col %d: %v", lineNum, colNum, syntaxErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cerrs = append(cerrs, ConfigError{File: fileName, Err: err.Error()})
|
||||||
}
|
}
|
||||||
return rtn, cerrs
|
return rtn, cerrs
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user