Fleet UI: Cap policy/report name input at 255 + truncate long names with tooltip on details pages (#48154)
This commit is contained in:
@@ -103,6 +103,9 @@ Render software title names via `getDisplayedSoftwareName(name, display_name)` f
|
||||
- Style files use underscore prefix: `_styles.scss`
|
||||
- Prefer `gap` over `margin` for spacing between sibling elements when the parent is `display: flex`/`grid`. Use the layout mixins from `frontend/styles/var/mixins.scss`: `vertical-card-layout`, `vertical-form-layout`, `vertical-modal-layout`, `vertical-page-layout`, `vertical-page-tab-panel-layout`, `vertical-data-set-layout`
|
||||
|
||||
## Forms
|
||||
Cap free-text inputs' `maxLength` to the backend column length (check `server/datastore/mysql/schema.sql`, don't guess) via `inputOptions={{ maxLength: NAME_MAX_LENGTH }}` on `InputField`, using a local constant.
|
||||
|
||||
## Lists & rows
|
||||
User-typed free-text fields (`name`, `title`, `label`, `description`) inside an `UploadList` `ListItemComponent`, a `__row` flex container with sibling actions/badges, or a `TableContainer` open-text cell — wrap the value in `<TooltipTruncatedText value={...} />` and give the immediate parent `flex: 1; min-width: 0`.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import FileSaver from "file-saver";
|
||||
import Spinner from "components/Spinner";
|
||||
import { HumanTimeDiffWithFleetLaunchCutoff } from "components/HumanTimeDiffWithDateTip";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import TooltipTruncatedText from "components/TooltipTruncatedText";
|
||||
import {
|
||||
getPerformanceImpactDescription,
|
||||
getPerformanceImpactIndicatorTooltip,
|
||||
@@ -173,8 +174,10 @@ const HQRTable = ({
|
||||
const renderTableInfo = useCallback(
|
||||
() => (
|
||||
<div className={`${baseClass}__query-info`}>
|
||||
<div>
|
||||
<h2>{queryName}</h2>
|
||||
<div className={`${baseClass}__query-info-text`}>
|
||||
<h2>
|
||||
<TooltipTruncatedText value={queryName} fixedPositionStrategy />
|
||||
</h2>
|
||||
<h3>{queryDescription}</h3>
|
||||
</div>
|
||||
<PerformanceImpact queryStats={queryStats} queryId={queryId} />
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
|
||||
&__query-info {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: $pad-xsmall;
|
||||
gap: $pad-medium;
|
||||
|
||||
h2 {
|
||||
font-size: $small;
|
||||
font-weight: $bold;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
h3 {
|
||||
font-size: $x-small;
|
||||
@@ -38,6 +39,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__query-info-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import MainContent from "components/MainContent";
|
||||
import PageDescription from "components/PageDescription";
|
||||
import Spinner from "components/Spinner";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import TooltipTruncatedText from "components/TooltipTruncatedText";
|
||||
import Avatar from "components/Avatar";
|
||||
import ShowQueryModal from "components/modals/ShowQueryModal";
|
||||
import { getTicketOrWebhookInfo } from "pages/policies/helpers";
|
||||
@@ -367,7 +368,10 @@ const PolicyDetailsPage = ({
|
||||
<div className={`${baseClass}__title-bar`}>
|
||||
<div className={`${baseClass}__name-description`}>
|
||||
<h1 className={`${baseClass}__policy-name`}>
|
||||
{storedPolicy?.name}
|
||||
<TooltipTruncatedText
|
||||
value={storedPolicy?.name}
|
||||
fixedPositionStrategy
|
||||
/>
|
||||
{storedPolicy?.critical && (
|
||||
<TooltipWrapper
|
||||
tipContent="This policy has been marked as critical."
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&__name-description {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__platform-list {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -34,6 +39,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
overflow: hidden;
|
||||
|
||||
.tooltip-truncated-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.critical-policy-icon {
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -85,6 +85,23 @@ describe("PolicyForm - component", () => {
|
||||
expect(screen.queryByText("All hosts")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("caps the policy name input at 255 characters in edit mode", () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
config: createMockConfig(),
|
||||
isPremiumTier: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<PolicyForm {...defaultProps} />);
|
||||
|
||||
expect(screen.getByLabelText("Name")).toHaveAttribute("maxlength", "255");
|
||||
});
|
||||
|
||||
describe("in premium tier", () => {
|
||||
beforeEach(() => {
|
||||
mockServer.use(labelSummariesHandler);
|
||||
|
||||
@@ -64,6 +64,8 @@ import SaveNewPolicyModal from "../SaveNewPolicyModal";
|
||||
|
||||
const baseClass = "policy-form";
|
||||
|
||||
const NAME_MAX_LENGTH = 255;
|
||||
|
||||
interface IPolicyFormProps {
|
||||
router: InjectedRouter;
|
||||
teamIdForApi?: number;
|
||||
@@ -518,6 +520,7 @@ const PolicyForm = ({
|
||||
error={errors && errors.name}
|
||||
onChange={(value: string) => setLastEditedQueryName(value)}
|
||||
disabled={gitOpsModeEnabled}
|
||||
inputOptions={{ maxLength: NAME_MAX_LENGTH }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+17
@@ -87,6 +87,23 @@ describe("SaveNewPolicyModal", () => {
|
||||
expect(screen.queryByText("All hosts")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("caps the policy name input at 255 characters", () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
config: createMockConfig(),
|
||||
isPremiumTier: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<SaveNewPolicyModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByLabelText("Name")).toHaveAttribute("maxlength", "255");
|
||||
});
|
||||
|
||||
describe("in premium tier", () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
|
||||
@@ -39,6 +39,8 @@ import PolicyAutomationsFields, {
|
||||
import { usePolicyLabelTargets } from "pages/policies/hooks";
|
||||
import { POLICY_TARGET_EMPTY_STATE_DESCRIPTION } from "pages/policies/constants";
|
||||
|
||||
const NAME_MAX_LENGTH = 255;
|
||||
|
||||
export interface ISaveNewPolicyModalProps {
|
||||
baseClass: string;
|
||||
queryValue: string;
|
||||
@@ -342,6 +344,7 @@ const SaveNewPolicyModal = ({
|
||||
label="Name"
|
||||
autofocus
|
||||
disabled={disableForm}
|
||||
inputOptions={{ maxLength: NAME_MAX_LENGTH }}
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
|
||||
@@ -28,6 +28,7 @@ import Button from "components/buttons/Button";
|
||||
import BackButton from "components/BackButton";
|
||||
import MainContent from "components/MainContent";
|
||||
import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper";
|
||||
import TooltipTruncatedText from "components/TooltipTruncatedText";
|
||||
import QueryAutomationsStatusIndicator from "pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator";
|
||||
import DataError from "components/DataError/DataError";
|
||||
import LogDestinationIndicator from "components/LogDestinationIndicator/LogDestinationIndicator";
|
||||
@@ -246,9 +247,12 @@ const QueryDetailsPage = ({
|
||||
{!isLoading && !isApiError && (
|
||||
<>
|
||||
<div className={`${baseClass}__title-bar`}>
|
||||
<div className="name-description">
|
||||
<div className={`${baseClass}__name-description`}>
|
||||
<h1 className={`${baseClass}__query-name`}>
|
||||
{storedQuery?.name}
|
||||
<TooltipTruncatedText
|
||||
value={storedQuery?.name}
|
||||
fixedPositionStrategy
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
gap: $pad-medium;
|
||||
}
|
||||
|
||||
&__name-description {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__action-button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -21,6 +26,7 @@
|
||||
&__query-name {
|
||||
margin-top: 0;
|
||||
font-size: $large;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__settings {
|
||||
|
||||
@@ -111,6 +111,69 @@ describe("EditQueryForm - component", () => {
|
||||
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("caps the report name input at 255 characters in edit mode", () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
query: {
|
||||
lastEditedQueryId: mockQuery.id,
|
||||
lastEditedQueryName: mockQuery.name,
|
||||
lastEditedQueryDescription: mockQuery.description,
|
||||
lastEditedQueryBody: mockQuery.query,
|
||||
lastEditedQueryObserverCanRun: mockQuery.observer_can_run,
|
||||
lastEditedQueryFrequency: mockQuery.interval,
|
||||
lastEditedQueryAutomationsEnabled: mockQuery.automations_enabled,
|
||||
lastEditedQueryPlatforms: mockQuery.platform,
|
||||
lastEditedQueryMinOsqueryVersion: mockQuery.min_osquery_version,
|
||||
lastEditedQueryLoggingType: mockQuery.logging,
|
||||
setLastEditedQueryName: jest.fn(),
|
||||
setLastEditedQueryDescription: jest.fn(),
|
||||
setLastEditedQueryBody: jest.fn(),
|
||||
setLastEditedQueryObserverCanRun: jest.fn(),
|
||||
setLastEditedQueryFrequency: jest.fn(),
|
||||
setLastEditedQueryAutomationsEnabled: jest.fn(),
|
||||
setLastEditedQueryPlatforms: jest.fn(),
|
||||
setLastEditedQueryMinOsqueryVersion: jest.fn(),
|
||||
setLastEditedQueryLoggingType: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
isGlobalObserver: false,
|
||||
isGlobalAdmin: true,
|
||||
isGlobalMaintainer: false,
|
||||
isOnGlobalTeam: true,
|
||||
isPremiumTier: false,
|
||||
isSandboxMode: false,
|
||||
config: createMockConfig(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<EditQueryForm
|
||||
router={mockRouter}
|
||||
location={mockLocation}
|
||||
queryIdForEdit={1}
|
||||
apiTeamIdForQuery={1}
|
||||
showOpenSchemaActionText
|
||||
storedQuery={createMockQuery()}
|
||||
isStoredQueryLoading={false}
|
||||
isQuerySaving={false}
|
||||
isQueryUpdating={false}
|
||||
onSubmitNewQuery={jest.fn()}
|
||||
onOsqueryTableSelect={jest.fn()}
|
||||
onUpdate={jest.fn()}
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
showConfirmSaveChangesModal={false}
|
||||
setShowConfirmSaveChangesModal={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Name")).toHaveAttribute("maxlength", "255");
|
||||
});
|
||||
|
||||
it("disables live query button for globally disabled live queries", async () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
|
||||
@@ -76,6 +76,8 @@ import SaveAsNewQueryModal from "../SaveAsNewQueryModal";
|
||||
|
||||
const baseClass = "edit-query-form";
|
||||
|
||||
const NAME_MAX_LENGTH = 255;
|
||||
|
||||
interface IEditQueryFormProps {
|
||||
router: InjectedRouter;
|
||||
location: Location;
|
||||
@@ -464,6 +466,7 @@ const EditQueryForm = ({
|
||||
setLastEditedQueryName(lastEditedQueryName.trim());
|
||||
}}
|
||||
disabled={gitOpsModeEnabled}
|
||||
inputOptions={{ maxLength: NAME_MAX_LENGTH }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +106,23 @@ describe("SaveNewQueryModal", () => {
|
||||
await user.click(advancedOptionsButton);
|
||||
});
|
||||
|
||||
it("caps the report name input at 255 characters", () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
config: createMockConfig(),
|
||||
isPremiumTier: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<SaveNewQueryModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByLabelText("Name")).toHaveAttribute("maxlength", "255");
|
||||
});
|
||||
|
||||
it("displays error when query name is empty", async () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
|
||||
@@ -47,6 +47,9 @@ import labelsAPI, {
|
||||
import DiscardDataOption from "../DiscardDataOption";
|
||||
|
||||
const baseClass = "save-query-modal";
|
||||
|
||||
const NAME_MAX_LENGTH = 255;
|
||||
|
||||
export interface ISaveNewQueryModalProps {
|
||||
queryValue: string;
|
||||
apiTeamIdForQuery?: number; // query will be global if omitted
|
||||
@@ -238,6 +241,7 @@ const SaveNewQueryModal = ({
|
||||
inputClassName={`${baseClass}__name`}
|
||||
label="Name"
|
||||
autofocus
|
||||
inputOptions={{ maxLength: NAME_MAX_LENGTH }}
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
|
||||
Reference in New Issue
Block a user