Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fleet] Improve UX for policy secrets #171405

Merged
merged 10 commits into from Nov 16, 2023
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/get_doc_links.ts
Expand Up @@ -769,6 +769,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
uninstallAgent: `${SECURITY_SOLUTION_DOCS}uninstall-agent.html`,
installAndUninstallIntegrationAssets: `${FLEET_DOCS}install-uninstall-integration-assets.html`,
elasticAgentInputConfiguration: `${FLEET_DOCS}elastic-agent-input-configuration.html`,
policySecrets: `${FLEET_DOCS}agent-policy.html#agent-policy-secret-values`,
},
ecs: {
guide: `${ELASTIC_WEBSITE_URL}guide/en/ecs/current/index.html`,
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/types.ts
Expand Up @@ -527,6 +527,7 @@ export interface DocLinks {
uninstallAgent: string;
installAndUninstallIntegrationAssets: string;
elasticAgentInputConfiguration: string;
policySecrets: string;
}>;
readonly ecs: {
readonly guide: string;
Expand Down
Expand Up @@ -23,11 +23,16 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiLink,
EuiToolTip,
EuiIcon,
} from '@elastic/eui';
import styled from 'styled-components';

import { CodeEditor } from '@kbn/kibana-react-plugin/public';

import { useStartServices } from '../../../../../../../../hooks';

import { ExperimentalFeaturesService } from '../../../../../../services';

import { DATASET_VAR_NAME } from '../../../../../../../../../common/constants';
Expand All @@ -41,6 +46,16 @@ const FixedHeightDiv = styled.div`
height: 300px;
`;

const FormRow = styled(EuiFormRow)`
.euiFormRow__label {
flex: 1;
}

.euiFormRow__fieldWrapper > .euiPanel {
padding: ${(props) => props.theme.eui.euiSizeXS};
}
`;

interface InputFieldProps {
varDef: RegistryVarsEntry;
value: any;
Expand Down Expand Up @@ -125,11 +140,11 @@ export const PackagePolicyInputVarField: React.FunctionComponent<InputFieldProps
});
}

return (
<EuiFormRow
const formRow = (
<FormRow
isInvalid={isInvalid}
error={errors}
label={fieldLabel}
label={varDef.secret ? <SecretFieldLabel fieldLabel={fieldLabel} /> : fieldLabel}
labelAppend={
isOptional ? (
<EuiText size="xs" color="subdued">
Expand All @@ -138,13 +153,16 @@ export const PackagePolicyInputVarField: React.FunctionComponent<InputFieldProps
defaultMessage="Optional"
/>
</EuiText>
) : null
) : undefined
}
helpText={description && <ReactMarkdown children={description} />}
fullWidth
>
{field}
</EuiFormRow>
</FormRow>
);

return varDef.secret ? <SecretFieldWrapper>{formRow}</SecretFieldWrapper> : formRow;
}
);

Expand Down Expand Up @@ -296,6 +314,53 @@ function getInputComponent({
}
}

const SecretFieldWrapper = ({ children }: { children: React.ReactNode }) => {
const { docLinks } = useStartServices();

return (
<EuiPanel hasShadow={false} color="subdued" paddingSize="m">
{children}

<EuiSpacer size="l" />

<EuiText size="xs">
<EuiLink href={docLinks.links.fleet.policySecrets} target="_blank">
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.secretLearnMoreText"
defaultMessage="Learn more about policy secrets."
/>
</EuiLink>
</EuiText>
</EuiPanel>
);
};

const SecretFieldLabel = ({ fieldLabel }: { fieldLabel: string }) => {
return (
<>
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem grow={true} aria-label={fieldLabel}>
{fieldLabel}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.secretLearnMorePopoverContent"
defaultMessage="This value is a secret. After you save this integration policy, you won't be able to view the value again."
/>
}
>
<EuiIcon aria-label="Secret value" type="questionInCircle" color="subdued" />
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>

<EuiSpacer size="s" />
</>
);
};

function SecretInputField({
varDef,
value,
Expand All @@ -313,10 +378,12 @@ function SecretInputField({
}: InputComponentProps) {
const [editMode, setEditMode] = useState(isEditPage && !value);
const valueOnFirstRender = useRef(value);

const lowercaseTitle = varDef.title?.toLowerCase();

if (isEditPage && !editMode) {
return (
<EuiPanel color="subdued" borderRadius="none" hasShadow={false}>
<>
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.fleet.editPackagePolicy.stepConfigure.fieldSecretValueSet"
Expand All @@ -342,7 +409,7 @@ function SecretInputField({
}}
/>
</EuiButtonEmpty>
</EuiPanel>
</>
);
}

Expand Down
Expand Up @@ -24,29 +24,144 @@ import {
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
EuiSpacer,
} from '@elastic/eui';
import styled from 'styled-components';

import type {
DryRunPackagePolicy,
PackagePolicy,
RegistryVarsEntry,
} from '../../../../../../../common';

import type { UpgradePackagePolicyDryRunResponse } from '../../../../../../../common/types/rest_spec';
import { useStartServices } from '../../../../hooks';

const FlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflowContent {
padding: 0;
}
`;

const HasNewSecretsCallOut = ({ newSecrets }: { newSecrets: RegistryVarsEntry[] }) => {
const { docLinks } = useStartServices();

return (
<EuiCallOut
title={i18n.translate('xpack.fleet.upgradePackagePolicy.statusCallOut.hasNewSecretsTitle', {
defaultMessage: 'New secrets added',
})}
color="primary"
iconType="iInCircle"
>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.hasNewSecrets"
defaultMessage="Some of this integration's form fields have been converted to secrets in this version. Your existing values are autofilled in each secret input during this upgrade, but you won't be able to view them again after saving. {learnMoreLink}"
values={{
learnMoreLink: (
<EuiLink href={docLinks.links.fleet.policySecrets} target="_blank">
Learn more.
</EuiLink>
),
}}
/>

<EuiSpacer size="s" />

<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.hasNewSecretsList"
defaultMessage="New secrets: {secrets}"
values={{
secrets: (
<ul>
{newSecrets.map((secret) => (
<li key={secret.title}>{secret.title}</li>
))}
</ul>
),
}}
/>
</EuiCallOut>
);
};

const HasConflictsCallout = ({
currentPackagePolicy,
proposedUpgradePackagePolicy,
onPreviousConfigurationClick,
}: {
currentPackagePolicy?: PackagePolicy;
proposedUpgradePackagePolicy?: DryRunPackagePolicy;
onPreviousConfigurationClick?: () => void;
}) => {
return (
<EuiCallOut
title={i18n.translate('xpack.fleet.upgradePackagePolicy.statusCallOut.errorTitle', {
defaultMessage: 'Review field conflicts',
})}
color="warning"
iconType="warning"
>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.errorContent"
defaultMessage="This integration has conflicting fields from version {currentVersion} to {upgradeVersion} Review the configuration and save to perform the upgrade. You may reference your {previousConfigurationLink} for comparison."
values={{
currentVersion: currentPackagePolicy?.package?.version,
upgradeVersion: proposedUpgradePackagePolicy?.package?.version,
previousConfigurationLink: (
<EuiLink onClick={onPreviousConfigurationClick}>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.previousConfigurationLink"
defaultMessage="previous configuration"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
);
};

const ReadyToUpgradeCallOut = ({
currentPackagePolicy,
proposedUpgradePackagePolicy,
}: {
currentPackagePolicy?: PackagePolicy;
proposedUpgradePackagePolicy?: DryRunPackagePolicy;
}) => {
return (
<EuiCallOut
title={i18n.translate('xpack.fleet.upgradePackagePolicy.statusCallOut.successTitle', {
defaultMessage: 'Ready to upgrade',
})}
color="success"
iconType="checkInCircleFilled"
>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.successContent"
defaultMessage="This integration is ready to be upgraded from version {currentVersion} to {upgradeVersion}. Review the changes below and save to upgrade."
values={{
currentVersion: currentPackagePolicy?.package?.version,
upgradeVersion: proposedUpgradePackagePolicy?.package?.version,
}}
/>
</EuiCallOut>
);
};

export const UpgradeStatusCallout: React.FunctionComponent<{
dryRunData: UpgradePackagePolicyDryRunResponse;
}> = ({ dryRunData }) => {
newSecrets: RegistryVarsEntry[];
}> = ({ dryRunData, newSecrets }) => {
const [isPreviousVersionFlyoutOpen, setIsPreviousVersionFlyoutOpen] = useState<boolean>(false);

if (!dryRunData) {
return null;
}

const isReadyForUpgrade = !dryRunData[0].hasErrors;

const hasNewSecrets = newSecrets.length > 0;
const [currentPackagePolicy, proposedUpgradePackagePolicy] = dryRunData[0].diff || [];
const isReadyForUpgrade = currentPackagePolicy && !dryRunData[0].hasErrors;

return (
<>
Expand All @@ -73,48 +188,23 @@ export const UpgradeStatusCallout: React.FunctionComponent<{
</EuiPortal>
)}

{isReadyForUpgrade && currentPackagePolicy ? (
<EuiCallOut
title={i18n.translate('xpack.fleet.upgradePackagePolicy.statusCallOut.successTitle', {
defaultMessage: 'Ready to upgrade',
})}
color="success"
iconType="checkInCircleFilled"
>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.successContent"
defaultMessage="This integration is ready to be upgraded from version {currentVersion} to {upgradeVersion}. Review the changes below and save to upgrade."
values={{
currentVersion: currentPackagePolicy?.package?.version,
upgradeVersion: proposedUpgradePackagePolicy?.package?.version,
}}
/>
</EuiCallOut>
{isReadyForUpgrade ? (
<ReadyToUpgradeCallOut
currentPackagePolicy={currentPackagePolicy}
proposedUpgradePackagePolicy={proposedUpgradePackagePolicy}
/>
) : (
<EuiCallOut
title={i18n.translate('xpack.fleet.upgradePackagePolicy.statusCallOut.errorTitle', {
defaultMessage: 'Review field conflicts',
})}
color="warning"
iconType="warning"
>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.errorContent"
defaultMessage="This integration has conflicting fields from version {currentVersion} to {upgradeVersion} Review the configuration and save to perform the upgrade. You may reference your {previousConfigurationLink} for comparison."
values={{
currentVersion: currentPackagePolicy?.package?.version,
upgradeVersion: proposedUpgradePackagePolicy?.package?.version,
previousConfigurationLink: (
<EuiLink onClick={() => setIsPreviousVersionFlyoutOpen(true)}>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.previousConfigurationLink"
defaultMessage="previous configuration"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
<HasConflictsCallout
currentPackagePolicy={currentPackagePolicy}
proposedUpgradePackagePolicy={proposedUpgradePackagePolicy}
onPreviousConfigurationClick={() => setIsPreviousVersionFlyoutOpen(true)}
/>
)}
{hasNewSecrets && (
<>
<EuiSpacer size="m" />
<HasNewSecretsCallOut newSecrets={newSecrets} />
</>
)}
</>
);
Expand Down
Expand Up @@ -99,7 +99,9 @@ export function usePackagePolicyWithRelatedData(
policy: { elasticsearch, ...restPackagePolicy },
} = await prepareInputPackagePolicyDataset(packagePolicy);
const result = await sendUpdatePackagePolicy(packagePolicyId, restPackagePolicy);

setFormState('SUBMITTED');

return result;
};
// Update package policy validation
Expand Down