Skip to content

Commit

Permalink
[Fleet] Can't select managed agent. Bulk upgrade agent UI changes (#9…
Browse files Browse the repository at this point in the history
…6087) (#96312)

## Summary

This PR implements several items from point 8 in elastic/observability-design#32 

The changes to modal titles and success/error behavior are only for bulk upgrade in this PR. Once we confirm the pattern, I'll apply it to unenroll and reassign in a follow up PR.

- [x] Disable the checkbox for agents enrolled in a hosted agent policy. We don't need to provide a tooltip description since the Agent policy "lock" icon appears in the table row.
- [x] If a user selects the top-left checkbox to select all agents on the page, and then clicks "select everything on all pages", we don't need to provide a count for total number of agents selected. The bulk actions button can say "all agents selected" (as discussed, calculating the total number of agents is problematic and can be tough on performance). Choosing a bulk action should filter out / not include any agents that are enrolled in a hosted agent policy.
- [x] Related to calculating total number of agents, we previously showed a count in the action modal's title. In the case where users have selected everything on all pages, the title can just say "all selected agents" -> i.e. "upgrade all selected agents"
- [x] If the result of a bulk action has mixed results, as in some percentage of agents are successful but others fail, show a warning toast that indicates how many succeeded and how many failed. See screenshot below.
- [x] Change the "experimental" badge for the upgrade agent action modal to be an icon only badge. You can use the `beaker` icon. The badge should use the same tooltip description that we use today indicating that this action is experimental.

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)
- [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios

## Per feature details & screenshots
<details>
  <summary>Disable the checkbox for agents enrolled in a hosted agent policy.</summary>
<img width="1016" alt="Screen Shot 2021-04-02 at 10 35 38 AM" src="https://user-images.githubusercontent.com/57655/113425437-d9e9e680-939f-11eb-9b1b-423d78dbc8fc.png">
<blockquote>We don't need to provide a tooltip description since the Agent policy "lock" icon appears in the table row.</blockquote>
I left the description in until we implement the "lock" icon.
</details>

<details>
  <summary>If a user selects the top-left checkbox to select all agents on the page, and then clicks "select everything on all pages", ... the bulk actions button can say "all agents selected" </summary>
<img width="998" alt="Screen Shot 2021-04-02 at 9 20 28 AM" src="https://user-images.githubusercontent.com/57655/113426911-1d455480-93a2-11eb-98e2-5a328ebd9d97.png">
<img width="1010" alt="Screen Shot 2021-04-02 at 9 20 45 AM" src="https://user-images.githubusercontent.com/57655/113426912-1d455480-93a2-11eb-92f3-7036d9e95f0c.png">

<blockquote>Choosing a bulk action should filter out / not include any agents that are enrolled in a hosted agent policy.</blockquote>
<img width="529" alt="Screen Shot 2021-04-02 at 10 58 43 AM" src="https://user-images.githubusercontent.com/57655/113427313-cb50fe80-93a2-11eb-948a-68dc165e567b.png">
<img width="593" alt="Screen Shot 2021-04-02 at 11 00 44 AM" src="https://user-images.githubusercontent.com/57655/113427317-cbe99500-93a2-11eb-9701-48a598350d4b.png">
<img width="1021" alt="Screen Shot 2021-04-02 at 10 59 27 AM" src="https://user-images.githubusercontent.com/57655/113427315-cb50fe80-93a2-11eb-8af3-822a23b91940.png">
<em>There are 7 rows, but only 6 were attempted because 1 is managed</em>
</details>

<details>
  <summary>When users have selected everything on all pages, the title can just say "all selected agents" -> i.e. "upgrade all selected agents"</summary>
<img width="843" alt="Screen Shot 2021-04-01 at 11 35 06 AM" src="https://user-images.githubusercontent.com/57655/113344391-04379780-92ff-11eb-86f8-62cdc0cde69d.png">
<em>the text inside the modal was also updated per the screenshots</em>

<h4>Single agent case</h4>
<img width="796" alt="Screen Shot 2021-04-01 at 11 35 18 AM" src="https://user-images.githubusercontent.com/57655/113344393-04d02e00-92ff-11eb-8b7e-1992a1ac695e.png">

<h4>Multiple items but not "all"</h4>
<img width="786" alt="Screen Shot 2021-04-01 at 11 35 33 AM" src="https://user-images.githubusercontent.com/57655/113344394-04d02e00-92ff-11eb-8f67-2f03fe8014c3.png">

</details>


<details>
  <summary>If the result of a bulk action has mixed results, as in some percentage of agents are successful but others fail, show a warning toast that indicates how many succeeded and how many failed</summary>

<h4>Mixed success & failure: show warning</h4>

<img width="1021" alt="Screen Shot 2021-04-02 at 10 59 27 AM" src="https://user-images.githubusercontent.com/57655/113427315-cb50fe80-93a2-11eb-8af3-822a23b91940.png">

<h4>All succeed. Variants for multiple vs all selected</h4>

<img width="376" alt="Screen Shot 2021-04-02 at 11 26 46 AM" src="https://user-images.githubusercontent.com/57655/113432299-2129a480-93ab-11eb-93f1-9a3edf2f1a2d.png">
<img width="342" alt="Screen Shot 2021-04-02 at 11 59 44 AM" src="https://user-images.githubusercontent.com/57655/113432300-21c23b00-93ab-11eb-85a8-222cef29a941.png">

<h4>All fail. Variants for multiple vs all selected</h4>

<img width="381" alt="Screen Shot 2021-04-02 at 11 14 48 AM" src="https://user-images.githubusercontent.com/57655/113428661-2daafe80-93a5-11eb-86b4-1993ebf9d2fa.png">

<img width="351" alt="Screen Shot 2021-04-02 at 11 19 17 AM" src="https://user-images.githubusercontent.com/57655/113428746-592de900-93a5-11eb-9cc0-5d919afd6d59.png">

</details>
<details>
  <summary>Change the "experimental" badge for the upgrade agent action modal to be an icon only badge. You can use the `beaker` icon. The badge should use the same tooltip description that we use today indicating that this action is experimental.</summary>
<img width="843" alt="Screen Shot 2021-04-01 at 11 35 06 AM" src="https://user-images.githubusercontent.com/57655/113344391-04379780-92ff-11eb-86f8-62cdc0cde69d.png">

</details>


Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: John Schulz <[email protected]>
  • Loading branch information
kibanamachine and John Schulz authored Apr 6, 2021
1 parent bb06fc2 commit 1d45885
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,21 @@ export const AgentBulkActions: React.FunctionComponent<{
},
];

const showSelectEverything =
selectionMode === 'manual' &&
selectedAgents.length === selectableAgents &&
selectableAgents < totalAgents;

const totalActiveAgents = totalAgents - totalInactiveAgents;
const agentCount = selectionMode === 'manual' ? selectedAgents.length : totalActiveAgents;
const agents = selectionMode === 'manual' ? selectedAgents : currentQuery;

return (
<>
{isReassignFlyoutOpen && (
<EuiPortal>
<AgentReassignAgentPolicyFlyout
agents={selectionMode === 'manual' ? selectedAgents : currentQuery}
agents={agents}
onClose={() => {
setIsReassignFlyoutOpen(false);
refreshAgents();
Expand All @@ -164,10 +173,8 @@ export const AgentBulkActions: React.FunctionComponent<{
{isUnenrollModalOpen && (
<EuiPortal>
<AgentUnenrollAgentModal
agents={selectionMode === 'manual' ? selectedAgents : currentQuery}
agentCount={
selectionMode === 'manual' ? selectedAgents.length : totalAgents - totalInactiveAgents
}
agents={agents}
agentCount={agentCount}
onClose={() => {
setIsUnenrollModalOpen(false);
refreshAgents();
Expand All @@ -179,10 +186,8 @@ export const AgentBulkActions: React.FunctionComponent<{
<EuiPortal>
<AgentUpgradeAgentModal
version={kibanaVersion}
agents={selectionMode === 'manual' ? selectedAgents : currentQuery}
agentCount={
selectionMode === 'manual' ? selectedAgents.length : totalAgents - totalInactiveAgents
}
agents={agents}
agentCount={agentCount}
onClose={() => {
setIsUpgradeModalOpen(false);
refreshAgents();
Expand Down Expand Up @@ -230,12 +235,9 @@ export const AgentBulkActions: React.FunctionComponent<{
>
<FormattedMessage
id="xpack.fleet.agentBulkActions.agentsSelected"
defaultMessage="{count, plural, one {# agent} other {# agents}} selected"
defaultMessage="{count, plural, one {# agent} other {# agents} =all {All agents}} selected"
values={{
count:
selectionMode === 'manual'
? selectedAgents.length
: Math.min(totalAgents - totalInactiveAgents, SO_SEARCH_LIMIT),
count: selectionMode === 'manual' ? selectedAgents.length : 'all',
}}
/>
</Button>
Expand All @@ -248,9 +250,7 @@ export const AgentBulkActions: React.FunctionComponent<{
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</EuiFlexItem>
{selectionMode === 'manual' &&
selectedAgents.length === selectableAgents &&
selectableAgents < totalAgents ? (
{showSelectEverything ? (
<EuiFlexItem grow={false}>
<Button
size="xs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,14 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
}, {} as { [k: string]: AgentPolicy });
}, [agentPolicies]);

const isAgentSelectable = (agent: Agent) => {
if (!agent.active) return false;

const agentPolicy = agentPolicies.find((p) => p.id === agent.policy_id);
const isManaged = agent.policy_id && agentPolicy?.is_managed === true;
return !isManaged;
};

const columns = [
{
field: 'local_metadata.host.hostname',
Expand All @@ -365,7 +373,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
defaultMessage: 'Agent policy',
}),
render: (policyId: string, agent: Agent) => {
const policyName = agentPolicies.find((p) => p.id === policyId)?.name;
const policyName = agentPoliciesIndexedById[policyId]?.name;
return (
<EuiFlexGroup gutterSize="s" alignItems="center" style={{ minWidth: 0 }}>
<EuiFlexItem grow={false} className="eui-textTruncate">
Expand Down Expand Up @@ -561,7 +569,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
totalAgents={totalAgents}
totalInactiveAgents={totalInactiveAgents}
agentStatus={agentsStatus}
selectableAgents={agents?.filter((agent) => agent.active).length || 0}
selectableAgents={agents?.filter(isAgentSelectable).length || 0}
selectionMode={selectionMode}
setSelectionMode={setSelectionMode}
currentQuery={kuery}
Expand Down Expand Up @@ -625,7 +633,17 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
setSelectedAgents(newAgents);
setSelectionMode('manual');
},
selectable: (agent: Agent) => agent.active,
selectable: isAgentSelectable,
selectableMessage: (selectable, agent) => {
if (selectable) return '';
if (!agent.active) {
return 'This agent is not active';
}
if (agent.policy_id && agentPoliciesIndexedById[agent.policy_id].is_managed) {
return 'This action is not available for agents enrolled in an externally managed agent policy';
}
return '';
},
}
: undefined
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
const { notifications } = useStartServices();
const [isSubmitting, setIsSubmitting] = useState(false);
const isSingleAgent = Array.isArray(agents) && agents.length === 1;
const isAllAgents = agents === '';
async function onSubmit() {
try {
setIsSubmitting(true);
const { error } = isSingleAgent
const { data, error } = isSingleAgent
? await sendPostAgentUpgrade((agents[0] as Agent).id, {
version,
})
Expand All @@ -47,22 +48,63 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
if (error) {
throw error;
}

const counts = Object.entries(data || {}).reduce(
(acc, [agentId, result]) => {
++acc.total;
++acc[result.success ? 'success' : 'error'];
return acc;
},
{
total: 0,
success: 0,
error: 0,
}
);
setIsSubmitting(false);
const successMessage = isSingleAgent
? i18n.translate('xpack.fleet.upgradeAgents.successSingleNotificationTitle', {
defaultMessage: 'Upgrading agent',
defaultMessage: 'Upgraded {count} agent',
values: { count: 1 },
})
: i18n.translate('xpack.fleet.upgradeAgents.successMultiNotificationTitle', {
defaultMessage: 'Upgrading agents',
defaultMessage:
'Upgraded {isMixed, select, true {{success} of {total}} other {{isAllAgents, select, true {all selected} other {{success}} }}} agents',
values: {
isMixed: counts.success !== counts.total,
success: counts.success,
total: counts.total,
isAllAgents,
},
});
notifications.toasts.addSuccess(successMessage);
if (counts.success === counts.total) {
notifications.toasts.addSuccess(successMessage);
} else if (counts.error === counts.total) {
notifications.toasts.addDanger(
i18n.translate('xpack.fleet.upgradeAgents.bulkResultAllErrorsNotificationTitle', {
defaultMessage:
'Error upgrading {count, plural, one {agent} other {{count} agents} =true {all selected agents}}',
values: { count: isAllAgents || agentCount },
})
);
} else {
notifications.toasts.addWarning({
title: successMessage,
text: i18n.translate('xpack.fleet.upgradeAgents.bulkResultErrorResultsSummary', {
defaultMessage:
'{count} {count, plural, one {agent was} other {agents were}} not successful',
values: { count: counts.error },
}),
});
}
onClose();
} catch (error) {
setIsSubmitting(false);
notifications.toasts.addError(error, {
title: i18n.translate('xpack.fleet.upgradeAgents.fatalErrorNotificationTitle', {
defaultMessage: 'Error upgrading {count, plural, one {agent} other {agents}}',
values: { count: agentCount },
defaultMessage:
'Error upgrading {count, plural, one {agent} other {{count} agents} =true {all selected agents}}',
values: { count: isAllAgents || agentCount },
}),
});
}
Expand All @@ -75,19 +117,20 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
<EuiFlexItem grow={false}>
{isSingleAgent ? (
<FormattedMessage
id="xpack.fleet.upgradeAgents.deleteSingleTitle"
defaultMessage="Upgrade agent?"
id="xpack.fleet.upgradeAgents.upgradeSingleTitle"
defaultMessage="Upgrade agent to latest version"
/>
) : (
<FormattedMessage
id="xpack.fleet.upgradeAgents.deleteMultipleTitle"
defaultMessage="Upgrade {count} agents?"
values={{ count: agentCount }}
id="xpack.fleet.upgradeAgents.upgradeMultipleTitle"
defaultMessage="Upgrade {count, plural, one {agent} other {{count} agents} =true {all selected agents}} to latest version"
values={{ count: isAllAgents || agentCount }}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBetaBadge
iconType="beaker"
label={
<FormattedMessage
id="xpack.fleet.upgradeAgents.experimentalLabel"
Expand Down Expand Up @@ -122,8 +165,8 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
) : (
<FormattedMessage
id="xpack.fleet.upgradeAgents.confirmMultipleButtonLabel"
defaultMessage="Upgrade {count} agents"
values={{ count: agentCount }}
defaultMessage="Upgrade {count, plural, one {agent} other {{count} agents} =true {all selected agents}}"
values={{ count: isAllAgents || agentCount }}
/>
)
}
Expand All @@ -132,7 +175,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
{isSingleAgent ? (
<FormattedMessage
id="xpack.fleet.upgradeAgents.upgradeSingleDescription"
defaultMessage="This action upgrades the agent running on '{hostName}' to version {version}. You can't undo this upgrade."
defaultMessage="This action will upgrade the agent running on '{hostName}' to version {version}. This action can not be undone. Are you sure you wish to continue?"
values={{
hostName: ((agents[0] as Agent).local_metadata.host as any).hostname,
version,
Expand All @@ -141,7 +184,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
) : (
<FormattedMessage
id="xpack.fleet.upgradeAgents.upgradeMultipleDescription"
defaultMessage="This action upgrades multiple agents to version {version}. You can't undo this upgrade."
defaultMessage="This action will upgrade multiple agents to version {version}. This action can not be undone. Are you sure you wish to continue?"
values={{ version }}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/server/services/agent_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ class AgentPolicyService {
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
options?: { user?: AuthenticatedUser }
): Promise<Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>>> {
): Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>> {
const currentPolicies = await soClient.find<AgentPolicySOAttributes>({
type: SAVED_OBJECT_TYPE,
fields: ['revision'],
Expand Down
26 changes: 18 additions & 8 deletions x-pack/plugins/fleet/server/services/agents/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,6 @@ export async function sendUpgradeAgentsActions(
} else if ('kuery' in options) {
givenAgents = await getAgents(esClient, options);
}
const givenOrder =
'agentIds' in options ? options.agentIds : givenAgents.map((agent) => agent.id);

// get any policy ids from upgradable agents
const policyIdsToGet = new Set(
Expand All @@ -125,25 +123,33 @@ export async function sendUpgradeAgentsActions(
acc[policy.id] = policy.is_managed;
return acc;
}, {});
const isManagedAgent = (agent: Agent) => agent.policy_id && managedPolicies[agent.policy_id];

// results from getAgents with options.kuery '' (or even 'active:false') may include managed agents
// filter them out unless options.force
const agentsToCheckUpgradeable =
'kuery' in options && !options.force
? givenAgents.filter((agent: Agent) => !isManagedAgent(agent))
: givenAgents;

// Filter out agents currently unenrolling, unenrolled, or not upgradeable b/c of version check
const kibanaVersion = appContextService.getKibanaVersion();
const agentResults = await Promise.allSettled(
givenAgents.map(async (agent) => {
const upgradeableResults = await Promise.allSettled(
agentsToCheckUpgradeable.map(async (agent) => {
// Filter out agents currently unenrolling, unenrolled, or not upgradeable b/c of version check
const isAllowed = options.force || isAgentUpgradeable(agent, kibanaVersion);
if (!isAllowed) {
throw new IngestManagerError(`${agent.id} is not upgradeable`);
}

if (!options.force && agent.policy_id && managedPolicies[agent.policy_id]) {
if (!options.force && isManagedAgent(agent)) {
throw new IngestManagerError(`Cannot upgrade agent in managed policy ${agent.policy_id}`);
}
return agent;
})
);

// Filter to agents that do not already use the new agent policy ID
const agentsToUpdate = agentResults.reduce<Agent[]>((agents, result, index) => {
// Filter & record errors from results
const agentsToUpdate = upgradeableResults.reduce<Agent[]>((agents, result, index) => {
if (result.status === 'fulfilled') {
agents.push(result.value);
} else {
Expand Down Expand Up @@ -182,6 +188,10 @@ export async function sendUpgradeAgentsActions(
},
}))
);

const givenOrder =
'agentIds' in options ? options.agentIds : agentsToCheckUpgradeable.map((agent) => agent.id);

const orderedOut = givenOrder.map((agentId) => {
const hasError = agentId in outgoingErrors;
const result: BulkActionResult = {
Expand Down
4 changes: 0 additions & 4 deletions x-pack/plugins/translations/translations/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -8716,12 +8716,8 @@
"xpack.fleet.upgradeAgents.cancelButtonLabel": "キャンセル",
"xpack.fleet.upgradeAgents.confirmMultipleButtonLabel": "{count}個のエージェントをアップグレード",
"xpack.fleet.upgradeAgents.confirmSingleButtonLabel": "エージェントをアップグレード",
"xpack.fleet.upgradeAgents.deleteMultipleTitle": "{count} 個のエージェントをアップグレードしますか?",
"xpack.fleet.upgradeAgents.deleteSingleTitle": "エージェントをアップグレードしますか?",
"xpack.fleet.upgradeAgents.experimentalLabel": "実験的",
"xpack.fleet.upgradeAgents.experimentalLabelTooltip": "アップグレードエージェントは今後のリリースで変更または削除される可能性があり、SLA のサポート対象になりません。",
"xpack.fleet.upgradeAgents.successMultiNotificationTitle": "エージェントをアップグレード中",
"xpack.fleet.upgradeAgents.successSingleNotificationTitle": "エージェントをアップグレード中",
"xpack.fleet.upgradeAgents.upgradeMultipleDescription": "このアクションにより、複数のエージェントがバージョン{version}にアップグレードされます。このアップグレードは元に戻すことができません。",
"xpack.fleet.upgradeAgents.upgradeSingleDescription": "このアクションにより、「{hostName}」で実行中のエージェントがバージョン{version}にアップグレードされます。このアップグレードは元に戻すことができません。",
"xpack.globalSearch.find.invalidLicenseError": "GlobalSearch API は、ライセンス状態が無効であるため、無効になっています。{errorMessage}",
Expand Down
4 changes: 0 additions & 4 deletions x-pack/plugins/translations/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -8803,13 +8803,9 @@
"xpack.fleet.upgradeAgents.cancelButtonLabel": "取消",
"xpack.fleet.upgradeAgents.confirmMultipleButtonLabel": "升级 {count} 个代理",
"xpack.fleet.upgradeAgents.confirmSingleButtonLabel": "升级代理",
"xpack.fleet.upgradeAgents.deleteMultipleTitle": "升级 {count} 个代理?",
"xpack.fleet.upgradeAgents.deleteSingleTitle": "升级代理?",
"xpack.fleet.upgradeAgents.experimentalLabel": "实验性",
"xpack.fleet.upgradeAgents.experimentalLabelTooltip": "在未来的版本中可能会更改或移除升级代理,其不受支持 SLA 的约束。",
"xpack.fleet.upgradeAgents.fatalErrorNotificationTitle": "升级{count, plural, other {代理}}时出错",
"xpack.fleet.upgradeAgents.successMultiNotificationTitle": "正在升级代理",
"xpack.fleet.upgradeAgents.successSingleNotificationTitle": "正在升级代理",
"xpack.fleet.upgradeAgents.upgradeMultipleDescription": "此操作将多个代理升级到版本 {version}。您无法撤消此升级。",
"xpack.fleet.upgradeAgents.upgradeSingleDescription": "此操作会将运行在“{hostName}”上的代理升级到版本 {version}。您无法撤消此升级。",
"xpack.globalSearch.find.invalidLicenseError": "GlobalSearch API 已禁用,因为许可状态无效:{errorMessage}",
Expand Down

0 comments on commit 1d45885

Please sign in to comment.