CodeQL model editor: support saving single/selected models (#3156)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
- In the CodeQL model editor, you can now select individual method rows and save changes to only the selected rows, instead of having to save the entire library model. [#3156](https://github.com/github/vscode-codeql/pull/3156)
|
||||
- If you run a query without having selected a database, we show a more intuitive prompt to help you select a database. [#3214](https://github.com/github/vscode-codeql/pull/3214)
|
||||
- The UI for browsing and running CodeQL tests has moved to use VS Code's built-in test UI. This makes the CodeQL test UI more consistent with the test UIs for other languages.
|
||||
This change means that this extension no longer depends on the "Test Explorer UI" and "Test Adapter Converter" extensions. You can uninstall those two extensions if they are
|
||||
|
||||
@@ -37,7 +37,10 @@ export function DataGrid({ gridTemplateColumns, children }: DataGridProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDataGridRow = styled.div<{ $focused?: boolean }>`
|
||||
const StyledDataGridRow = styled.div<{
|
||||
$focused?: boolean;
|
||||
$selected?: boolean;
|
||||
}>`
|
||||
display: contents;
|
||||
|
||||
&:hover > * {
|
||||
@@ -48,14 +51,18 @@ const StyledDataGridRow = styled.div<{ $focused?: boolean }>`
|
||||
// Use !important to override the background color set by the hover state
|
||||
background-color: ${(props) =>
|
||||
props.$focused
|
||||
? "var(--vscode-editor-selectionBackground) !important"
|
||||
: "inherit"};
|
||||
? "var(--vscode-editor-findMatchHighlightBackground) !important"
|
||||
: props.$selected
|
||||
? "var(--vscode-editor-selectionBackground) !important"
|
||||
: "inherit"};
|
||||
}
|
||||
`;
|
||||
|
||||
interface DataGridRowProps {
|
||||
focused?: boolean;
|
||||
selected?: boolean;
|
||||
children: ReactNode;
|
||||
onClick?: () => void;
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
@@ -69,10 +76,22 @@ interface DataGridRowProps {
|
||||
*/
|
||||
export const DataGridRow = forwardRef(
|
||||
(
|
||||
{ focused, children, "data-testid": testId }: DataGridRowProps,
|
||||
{
|
||||
focused,
|
||||
selected,
|
||||
children,
|
||||
"data-testid": testId,
|
||||
onClick,
|
||||
}: DataGridRowProps,
|
||||
ref?: React.Ref<HTMLElement | undefined>,
|
||||
) => (
|
||||
<StyledDataGridRow $focused={focused} ref={ref} data-testid={testId}>
|
||||
<StyledDataGridRow
|
||||
$focused={focused}
|
||||
$selected={selected}
|
||||
ref={ref}
|
||||
data-testid={testId}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</StyledDataGridRow>
|
||||
),
|
||||
|
||||
@@ -25,6 +25,9 @@ type Props = {
|
||||
"aria-label"?: string;
|
||||
};
|
||||
|
||||
const stopClickPropagation = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
/**
|
||||
* A dropdown implementation styled to look like `VSCodeDropdown`.
|
||||
*
|
||||
@@ -50,6 +53,7 @@ export function Dropdown({
|
||||
value={disabled ? disabledValue : value}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
onClick={stopClickPropagation}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -71,11 +71,13 @@ export type LibraryRowProps = {
|
||||
methods: Method[];
|
||||
modeledMethodsMap: Record<string, ModeledMethod[]>;
|
||||
modifiedSignatures: Set<string>;
|
||||
selectedSignatures: Set<string>;
|
||||
inProgressMethods: Set<string>;
|
||||
viewState: ModelEditorViewState;
|
||||
hideModeledMethods: boolean;
|
||||
revealedMethodSignature: string | null;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
onMethodClick: (methodSignature: string) => void;
|
||||
onSaveModelClick: (methodSignatures: string[]) => void;
|
||||
onGenerateFromLlmClick: (
|
||||
dependencyName: string,
|
||||
@@ -92,11 +94,13 @@ export const LibraryRow = ({
|
||||
methods,
|
||||
modeledMethodsMap,
|
||||
modifiedSignatures,
|
||||
selectedSignatures,
|
||||
inProgressMethods,
|
||||
viewState,
|
||||
hideModeledMethods,
|
||||
revealedMethodSignature,
|
||||
onChange,
|
||||
onMethodClick,
|
||||
onSaveModelClick,
|
||||
onGenerateFromLlmClick,
|
||||
onStopGenerateFromLlmClick,
|
||||
@@ -228,16 +232,18 @@ export const LibraryRow = ({
|
||||
methods={methods}
|
||||
modeledMethodsMap={modeledMethodsMap}
|
||||
modifiedSignatures={modifiedSignatures}
|
||||
selectedSignatures={selectedSignatures}
|
||||
inProgressMethods={inProgressMethods}
|
||||
viewState={viewState}
|
||||
hideModeledMethods={hideModeledMethods}
|
||||
revealedMethodSignature={revealedMethodSignature}
|
||||
onChange={onChange}
|
||||
onMethodClick={onMethodClick}
|
||||
/>
|
||||
<SectionDivider />
|
||||
<ButtonsContainer>
|
||||
<VSCodeButton onClick={handleSave} disabled={!hasUnsavedChanges}>
|
||||
Save
|
||||
{selectedSignatures.size === 0 ? "Save" : "Save selected"}
|
||||
</VSCodeButton>
|
||||
</ButtonsContainer>
|
||||
</>
|
||||
|
||||
@@ -70,10 +70,12 @@ export type MethodRowProps = {
|
||||
methodCanBeModeled: boolean;
|
||||
modeledMethods: ModeledMethod[];
|
||||
methodIsUnsaved: boolean;
|
||||
methodIsSelected: boolean;
|
||||
modelingInProgress: boolean;
|
||||
viewState: ModelEditorViewState;
|
||||
revealedMethodSignature: string | null;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
onMethodClick: (methodSignature: string) => void;
|
||||
};
|
||||
|
||||
export const MethodRow = (props: MethodRowProps) => {
|
||||
@@ -103,9 +105,11 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
|
||||
method,
|
||||
modeledMethods: modeledMethodsProp,
|
||||
methodIsUnsaved,
|
||||
methodIsSelected,
|
||||
viewState,
|
||||
revealedMethodSignature,
|
||||
onChange,
|
||||
onMethodClick,
|
||||
} = props;
|
||||
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
@@ -186,6 +190,10 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
|
||||
<DataGridRow
|
||||
data-testid="modelable-method-row"
|
||||
focused={revealedMethodSignature === method.signature}
|
||||
selected={methodIsSelected}
|
||||
onClick={() => {
|
||||
onMethodClick(method.signature);
|
||||
}}
|
||||
>
|
||||
<DataGridCell
|
||||
gridRow={`span ${modeledMethods.length + validationErrors.length}`}
|
||||
@@ -196,11 +204,23 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
|
||||
<MethodClassifications method={method} />
|
||||
<MethodName {...props.method} />
|
||||
{viewState.mode === Mode.Application && (
|
||||
<UsagesButton onClick={jumpToMethod}>
|
||||
<UsagesButton
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
jumpToMethod();
|
||||
}}
|
||||
>
|
||||
{method.usages.length}
|
||||
</UsagesButton>
|
||||
)}
|
||||
<ViewLink onClick={jumpToMethod}>View</ViewLink>
|
||||
<ViewLink
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
jumpToMethod();
|
||||
}}
|
||||
>
|
||||
View
|
||||
</ViewLink>
|
||||
{props.modelingInProgress && <ProgressRing />}
|
||||
</ApiOrMethodRow>
|
||||
</DataGridCell>
|
||||
@@ -269,7 +289,10 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
|
||||
<CodiconRow
|
||||
appearance="icon"
|
||||
aria-label="Add new model"
|
||||
onClick={handleAddModelClick}
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
handleAddModelClick();
|
||||
}}
|
||||
disabled={addModelButtonDisabled}
|
||||
>
|
||||
<Codicon name="add" />
|
||||
@@ -278,7 +301,10 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
|
||||
<CodiconRow
|
||||
appearance="icon"
|
||||
aria-label="Remove model"
|
||||
onClick={removeModelClickedHandlers[index]}
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
removeModelClickedHandlers[index]();
|
||||
}}
|
||||
>
|
||||
<Codicon name="trash" />
|
||||
</CodiconRow>
|
||||
|
||||
@@ -95,6 +95,10 @@ export function ModelEditor({
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const [selectedSignatures, setSelectedSignatures] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const [inProgressMethods, setInProgressMethods] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
@@ -189,6 +193,19 @@ export function ModelEditor({
|
||||
[],
|
||||
);
|
||||
|
||||
const onMethodClick = useCallback(
|
||||
(methodSignature: string) => {
|
||||
const newSelectedSignatures = new Set(selectedSignatures);
|
||||
if (selectedSignatures.has(methodSignature)) {
|
||||
newSelectedSignatures.delete(methodSignature);
|
||||
} else {
|
||||
newSelectedSignatures.add(methodSignature);
|
||||
}
|
||||
setSelectedSignatures(newSelectedSignatures);
|
||||
},
|
||||
[selectedSignatures],
|
||||
);
|
||||
|
||||
const onRefreshClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: "refreshMethods",
|
||||
@@ -198,15 +215,31 @@ export function ModelEditor({
|
||||
const onSaveAllClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
t: "saveModeledMethods",
|
||||
methodSignatures:
|
||||
selectedSignatures.size === 0
|
||||
? undefined
|
||||
: Array.from(selectedSignatures),
|
||||
});
|
||||
}, [selectedSignatures]);
|
||||
|
||||
const onDeselectAllClick = useCallback(() => {
|
||||
setSelectedSignatures(new Set());
|
||||
}, []);
|
||||
|
||||
const onSaveModelClick = useCallback((methodSignatures: string[]) => {
|
||||
vscode.postMessage({
|
||||
t: "saveModeledMethods",
|
||||
methodSignatures,
|
||||
});
|
||||
}, []);
|
||||
const onSaveModelClick = useCallback(
|
||||
(methodSignatures: string[]) => {
|
||||
vscode.postMessage({
|
||||
t: "saveModeledMethods",
|
||||
methodSignatures:
|
||||
selectedSignatures.size === 0
|
||||
? methodSignatures
|
||||
: methodSignatures.filter((signature) =>
|
||||
selectedSignatures.has(signature),
|
||||
),
|
||||
});
|
||||
},
|
||||
[selectedSignatures],
|
||||
);
|
||||
|
||||
const onGenerateFromSourceClick = useCallback(() => {
|
||||
vscode.postMessage({
|
||||
@@ -309,7 +342,14 @@ export function ModelEditor({
|
||||
onClick={onSaveAllClick}
|
||||
disabled={modifiedSignatures.size === 0}
|
||||
>
|
||||
Save all
|
||||
{selectedSignatures.size === 0 ? "Save all" : "Save selected"}
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={onDeselectAllClick}
|
||||
disabled={selectedSignatures.size === 0}
|
||||
>
|
||||
Deselect all
|
||||
</VSCodeButton>
|
||||
<VSCodeButton appearance="secondary" onClick={onRefreshClick}>
|
||||
Refresh
|
||||
@@ -339,11 +379,13 @@ export function ModelEditor({
|
||||
methods={methods}
|
||||
modeledMethodsMap={modeledMethods}
|
||||
modifiedSignatures={modifiedSignatures}
|
||||
selectedSignatures={selectedSignatures}
|
||||
inProgressMethods={inProgressMethods}
|
||||
viewState={viewState}
|
||||
hideModeledMethods={hideModeledMethods}
|
||||
revealedMethodSignature={revealedMethodSignature}
|
||||
onChange={onChange}
|
||||
onMethodClick={onMethodClick}
|
||||
onSaveModelClick={onSaveModelClick}
|
||||
onGenerateFromLlmClick={onGenerateFromLlmClick}
|
||||
onStopGenerateFromLlmClick={onStopGenerateFromLlmClick}
|
||||
|
||||
@@ -16,22 +16,26 @@ export type ModeledMethodDataGridProps = {
|
||||
methods: Method[];
|
||||
modeledMethodsMap: Record<string, ModeledMethod[]>;
|
||||
modifiedSignatures: Set<string>;
|
||||
selectedSignatures: Set<string>;
|
||||
inProgressMethods: Set<string>;
|
||||
viewState: ModelEditorViewState;
|
||||
hideModeledMethods: boolean;
|
||||
revealedMethodSignature: string | null;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
onMethodClick: (methodSignature: string) => void;
|
||||
};
|
||||
|
||||
export const ModeledMethodDataGrid = ({
|
||||
methods,
|
||||
modeledMethodsMap,
|
||||
modifiedSignatures,
|
||||
selectedSignatures,
|
||||
inProgressMethods,
|
||||
viewState,
|
||||
hideModeledMethods,
|
||||
revealedMethodSignature,
|
||||
onChange,
|
||||
onMethodClick,
|
||||
}: ModeledMethodDataGridProps) => {
|
||||
const [methodsWithModelability, numHiddenMethods]: [
|
||||
Array<{ method: Method; methodCanBeModeled: boolean }>,
|
||||
@@ -80,10 +84,12 @@ export const ModeledMethodDataGrid = ({
|
||||
methodCanBeModeled={methodCanBeModeled}
|
||||
modeledMethods={modeledMethods}
|
||||
methodIsUnsaved={modifiedSignatures.has(method.signature)}
|
||||
methodIsSelected={selectedSignatures.has(method.signature)}
|
||||
modelingInProgress={inProgressMethods.has(method.signature)}
|
||||
viewState={viewState}
|
||||
revealedMethodSignature={revealedMethodSignature}
|
||||
onChange={onChange}
|
||||
onMethodClick={onMethodClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -13,11 +13,13 @@ export type ModeledMethodsListProps = {
|
||||
methods: Method[];
|
||||
modeledMethodsMap: Record<string, ModeledMethod[]>;
|
||||
modifiedSignatures: Set<string>;
|
||||
selectedSignatures: Set<string>;
|
||||
inProgressMethods: Set<string>;
|
||||
revealedMethodSignature: string | null;
|
||||
viewState: ModelEditorViewState;
|
||||
hideModeledMethods: boolean;
|
||||
onChange: (methodSignature: string, modeledMethods: ModeledMethod[]) => void;
|
||||
onMethodClick: (methodSignature: string) => void;
|
||||
onSaveModelClick: (methodSignatures: string[]) => void;
|
||||
onGenerateFromLlmClick: (
|
||||
packageName: string,
|
||||
@@ -36,11 +38,13 @@ export const ModeledMethodsList = ({
|
||||
methods,
|
||||
modeledMethodsMap,
|
||||
modifiedSignatures,
|
||||
selectedSignatures,
|
||||
inProgressMethods,
|
||||
viewState,
|
||||
hideModeledMethods,
|
||||
revealedMethodSignature,
|
||||
onChange,
|
||||
onMethodClick,
|
||||
onSaveModelClick,
|
||||
onGenerateFromLlmClick,
|
||||
onStopGenerateFromLlmClick,
|
||||
@@ -82,11 +86,13 @@ export const ModeledMethodsList = ({
|
||||
methods={grouped[libraryName]}
|
||||
modeledMethodsMap={modeledMethodsMap}
|
||||
modifiedSignatures={modifiedSignatures}
|
||||
selectedSignatures={selectedSignatures}
|
||||
inProgressMethods={inProgressMethods}
|
||||
viewState={viewState}
|
||||
hideModeledMethods={hideModeledMethods}
|
||||
revealedMethodSignature={revealedMethodSignature}
|
||||
onChange={onChange}
|
||||
onMethodClick={onMethodClick}
|
||||
onSaveModelClick={onSaveModelClick}
|
||||
onGenerateFromLlmClick={onGenerateFromLlmClick}
|
||||
onStopGenerateFromLlmClick={onStopGenerateFromLlmClick}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createMockModelEditorViewState } from "../../../../test/factories/model
|
||||
describe(LibraryRow.name, () => {
|
||||
const method = createMethod();
|
||||
const onChange = jest.fn();
|
||||
const onMethodClick = jest.fn();
|
||||
const onSaveModelClick = jest.fn();
|
||||
const onGenerateFromLlmClick = jest.fn();
|
||||
const onStopGenerateFromLlmClick = jest.fn();
|
||||
@@ -33,11 +34,13 @@ describe(LibraryRow.name, () => {
|
||||
],
|
||||
}}
|
||||
modifiedSignatures={new Set([method.signature])}
|
||||
selectedSignatures={new Set()}
|
||||
inProgressMethods={new Set()}
|
||||
viewState={viewState}
|
||||
hideModeledMethods={false}
|
||||
revealedMethodSignature={null}
|
||||
onChange={onChange}
|
||||
onMethodClick={onMethodClick}
|
||||
onSaveModelClick={onSaveModelClick}
|
||||
onGenerateFromLlmClick={onGenerateFromLlmClick}
|
||||
onStopGenerateFromLlmClick={onStopGenerateFromLlmClick}
|
||||
|
||||
@@ -30,6 +30,7 @@ describe(MethodRow.name, () => {
|
||||
provenance: "manual",
|
||||
};
|
||||
const onChange = jest.fn();
|
||||
const onMethodClick = jest.fn();
|
||||
|
||||
const viewState = createMockModelEditorViewState();
|
||||
|
||||
@@ -40,10 +41,12 @@ describe(MethodRow.name, () => {
|
||||
methodCanBeModeled={true}
|
||||
modeledMethods={[modeledMethod]}
|
||||
methodIsUnsaved={false}
|
||||
methodIsSelected={false}
|
||||
modelingInProgress={false}
|
||||
revealedMethodSignature={null}
|
||||
viewState={viewState}
|
||||
onChange={onChange}
|
||||
onMethodClick={onMethodClick}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { act, render as reactRender, screen } from "@testing-library/react";
|
||||
import { createMethod } from "../../../../test/factories/model-editor/method-factories";
|
||||
import { ModelEditor } from "../ModelEditor";
|
||||
import { createMockModelEditorViewState } from "../../../../test/factories/model-editor/view-state";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
|
||||
describe(ModelEditor.name, () => {
|
||||
const method1 = createMethod({
|
||||
library: "sql2o",
|
||||
libraryVersion: "1.6.0",
|
||||
signature: "org.sql2o.Connection#createQuery(String)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Connection",
|
||||
methodName: "createQuery",
|
||||
methodParameters: "(String)",
|
||||
supported: false,
|
||||
});
|
||||
const method2 = createMethod({
|
||||
library: "sql2o",
|
||||
libraryVersion: "1.6.0",
|
||||
signature: "org.sql2o.Query#executeScalar(Class)",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Query",
|
||||
methodName: "executeScalar",
|
||||
methodParameters: "(Class)",
|
||||
supported: false,
|
||||
});
|
||||
const method3 = createMethod({
|
||||
library: "sql2o",
|
||||
libraryVersion: "1.6.0",
|
||||
signature: "org.sql2o.Sql2o#open()",
|
||||
packageName: "org.sql2o",
|
||||
typeName: "Sql2o",
|
||||
methodName: "open",
|
||||
methodParameters: "()",
|
||||
supported: true,
|
||||
});
|
||||
|
||||
const viewState = createMockModelEditorViewState();
|
||||
|
||||
const render = () =>
|
||||
reactRender(
|
||||
<ModelEditor
|
||||
initialViewState={viewState}
|
||||
initialMethods={[method1, method2, method3]}
|
||||
/>,
|
||||
);
|
||||
|
||||
it("renders Save button when no rows are selected", () => {
|
||||
render();
|
||||
|
||||
expect(screen.getByText("Save all")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Save button when rows are selected", async () => {
|
||||
render();
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getAllByLabelText("Expand")[0]);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(screen.getAllByTestId("modelable-method-row")[0]);
|
||||
});
|
||||
|
||||
// The top-level Save button and the per-library Save button should have been updated.
|
||||
expect(screen.getAllByText("Save selected")).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -36,6 +36,7 @@ describe(ModeledMethodDataGrid.name, () => {
|
||||
supported: true,
|
||||
});
|
||||
const onChange = jest.fn();
|
||||
const onMethodClick = jest.fn();
|
||||
|
||||
const viewState = createMockModelEditorViewState();
|
||||
|
||||
@@ -55,11 +56,13 @@ describe(ModeledMethodDataGrid.name, () => {
|
||||
],
|
||||
}}
|
||||
modifiedSignatures={new Set([method1.signature])}
|
||||
selectedSignatures={new Set()}
|
||||
inProgressMethods={new Set()}
|
||||
viewState={viewState}
|
||||
hideModeledMethods={false}
|
||||
revealedMethodSignature={null}
|
||||
onChange={onChange}
|
||||
onMethodClick={onMethodClick}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ describe(ModeledMethodsList.name, () => {
|
||||
methodParameters: "(String)",
|
||||
});
|
||||
const onChange = jest.fn();
|
||||
const onMethodClick = jest.fn();
|
||||
const onSaveModelClick = jest.fn();
|
||||
const onGenerateFromLlmClick = jest.fn();
|
||||
const onStopGenerateFromLlmClick = jest.fn();
|
||||
@@ -56,11 +57,13 @@ describe(ModeledMethodsList.name, () => {
|
||||
],
|
||||
}}
|
||||
modifiedSignatures={new Set([method1.signature])}
|
||||
selectedSignatures={new Set()}
|
||||
inProgressMethods={new Set()}
|
||||
viewState={viewState}
|
||||
hideModeledMethods={false}
|
||||
revealedMethodSignature={null}
|
||||
onChange={onChange}
|
||||
onMethodClick={onMethodClick}
|
||||
onSaveModelClick={onSaveModelClick}
|
||||
onGenerateFromLlmClick={onGenerateFromLlmClick}
|
||||
onStopGenerateFromLlmClick={onStopGenerateFromLlmClick}
|
||||
|
||||
Reference in New Issue
Block a user