mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Standardize TeamsDropdown component usage (#3135)
This commit is contained in:
parent
7464e72ba8
commit
e750eb9745
25 changed files with 355 additions and 632 deletions
1
changes/fix-2996-schedule-dropdown
Normal file
1
changes/fix-2996-schedule-dropdown
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fix manage schedule page teams dropdown cutting off in some cases
|
||||
|
|
@ -143,7 +143,7 @@ describe(
|
|||
cy.findByRole("button", { name: /done/i }).click();
|
||||
|
||||
// See the "Manage" enroll secret” button on team Oranges only
|
||||
cy.findByText(/all teams/i).should("exist");
|
||||
cy.findAllByText(/apples/i).should("exist");
|
||||
cy.findByText(/manage enroll secret/i).should("not.exist");
|
||||
|
||||
cy.visit("/hosts/manage/?team_id=1");
|
||||
|
|
|
|||
|
|
@ -102,12 +102,7 @@ describe("Teams flow", () => {
|
|||
cy.visit("/schedule/manage");
|
||||
|
||||
cy.wait(2000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||
cy.findByText(/all teams/i).click();
|
||||
cy.findByText(/valor/i).click();
|
||||
|
||||
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||
cy.findByText(/query all window crashes/i).should("not.exist");
|
||||
cy.findByText(/inherited query/i).click();
|
||||
cy.findByText(/valor/i).should("exist");
|
||||
cy.findByText(/query all window crashes/i).should("exist");
|
||||
|
||||
// Edit Team
|
||||
|
|
|
|||
|
|
@ -1,20 +1,39 @@
|
|||
import React, { useContext } from "react";
|
||||
import React, { useContext, useMemo } from "react";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import { ITeam } from "interfaces/team";
|
||||
import {
|
||||
generateTeamFilterDropdownOptions,
|
||||
getValidatedTeamId,
|
||||
} from "fleet/helpers";
|
||||
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
|
||||
const generateDropdownOptions = (
|
||||
teams: ITeam[] | undefined,
|
||||
includeAll: boolean | undefined
|
||||
) => {
|
||||
if (!teams) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const options = teams.map((team) => ({
|
||||
disabled: false,
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
}));
|
||||
|
||||
if (includeAll) {
|
||||
options.unshift({
|
||||
disabled: false,
|
||||
label: "All teams",
|
||||
value: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
interface ITeamsDropdownProps {
|
||||
isLoading: boolean;
|
||||
teams: ITeam[];
|
||||
currentTeamId: number;
|
||||
hideAllTeamsOption?: boolean;
|
||||
currentUserTeams: ITeam[];
|
||||
selectedTeamId: number;
|
||||
includeAll?: boolean;
|
||||
onChange: (newSelectedValue: number) => void;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
|
|
@ -23,47 +42,41 @@ interface ITeamsDropdownProps {
|
|||
const baseClass = "component__team-dropdown";
|
||||
|
||||
const TeamsDropdown = ({
|
||||
isLoading,
|
||||
teams,
|
||||
currentTeamId,
|
||||
hideAllTeamsOption = false,
|
||||
currentUserTeams,
|
||||
selectedTeamId,
|
||||
includeAll,
|
||||
onChange,
|
||||
onOpen,
|
||||
onClose,
|
||||
}: ITeamsDropdownProps) => {
|
||||
const { currentUser, isPremiumTier, isOnGlobalTeam } = useContext(AppContext);
|
||||
}: ITeamsDropdownProps): JSX.Element => {
|
||||
const { isOnGlobalTeam } = useContext(AppContext);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
} else if (!isPremiumTier) {
|
||||
return <h1>Hosts</h1>;
|
||||
}
|
||||
const teamOptions = useMemo(
|
||||
() =>
|
||||
generateDropdownOptions(currentUserTeams, isOnGlobalTeam || includeAll),
|
||||
[currentUserTeams, isOnGlobalTeam]
|
||||
);
|
||||
|
||||
const teamOptions = generateTeamFilterDropdownOptions(
|
||||
teams,
|
||||
currentUser,
|
||||
isOnGlobalTeam as boolean,
|
||||
hideAllTeamsOption
|
||||
);
|
||||
const selectedTeamId = getValidatedTeamId(
|
||||
teams,
|
||||
currentTeamId,
|
||||
currentUser,
|
||||
isOnGlobalTeam as boolean
|
||||
);
|
||||
const selectedValue = teamOptions.find(
|
||||
(option) => selectedTeamId === option.value
|
||||
)
|
||||
? selectedTeamId
|
||||
: teamOptions[0]?.value;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropdown
|
||||
value={selectedTeamId}
|
||||
placeholder="All teams"
|
||||
className={baseClass}
|
||||
options={teamOptions}
|
||||
searchable={false}
|
||||
onChange={onChange}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
{teamOptions.length && (
|
||||
<Dropdown
|
||||
value={selectedValue}
|
||||
placeholder="All teams"
|
||||
className={baseClass}
|
||||
options={teamOptions}
|
||||
searchable={false}
|
||||
onChange={onChange}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,10 +38,11 @@
|
|||
}
|
||||
|
||||
.Select-arrow-zone {
|
||||
height: 32px;
|
||||
padding-left: $pad-small;
|
||||
|
||||
.Select-arrow {
|
||||
top: 1px !important;
|
||||
top: 2px !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +66,7 @@
|
|||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
top: 2px;
|
||||
|
||||
&.is-focused {
|
||||
border: 0 !important;
|
||||
|
|
@ -80,4 +81,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import PATHS from "router/paths";
|
||||
import URL_PREFIX from "router/url_prefix";
|
||||
import permissionUtils from "utilities/permissions";
|
||||
import { getSortedTeamOptions } from "fleet/helpers";
|
||||
|
||||
export default (currentUser) => {
|
||||
const logo = [
|
||||
|
|
@ -67,6 +68,7 @@ export default (currentUser) => {
|
|||
const userAdminTeams = currentUser.teams.filter(
|
||||
(thisTeam) => thisTeam.role === "admin"
|
||||
);
|
||||
const sortedTeams = getSortedTeamOptions(userAdminTeams);
|
||||
const adminNavItems = [
|
||||
{
|
||||
icon: "settings",
|
||||
|
|
@ -77,7 +79,7 @@ export default (currentUser) => {
|
|||
pathname:
|
||||
currentUser.global_role === "admin"
|
||||
? PATHS.ADMIN_SETTINGS
|
||||
: `${PATHS.ADMIN_TEAMS}/${userAdminTeams[0].id}/members`,
|
||||
: `${PATHS.ADMIN_TEAMS}/${sortedTeams[0].value}/members`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -675,7 +675,7 @@ export const syntaxHighlight = (json: any): string => {
|
|||
/* eslint-enable no-useless-escape */
|
||||
};
|
||||
|
||||
const getSortedTeamOptions = memoize((teams: ITeam[]) =>
|
||||
export const getSortedTeamOptions = memoize((teams: ITeam[]) =>
|
||||
teams
|
||||
.map((team) => {
|
||||
return {
|
||||
|
|
@ -687,34 +687,6 @@ const getSortedTeamOptions = memoize((teams: ITeam[]) =>
|
|||
.sort((a, b) => sortUtils.caseInsensitiveAsc(a.label, b.label))
|
||||
);
|
||||
|
||||
export const generateTeamFilterDropdownOptions = (
|
||||
teams: ITeam[],
|
||||
currentUser: IUser | null,
|
||||
isOnGlobalTeam: boolean,
|
||||
hideAllTeamsOption: boolean
|
||||
) => {
|
||||
let currentUserTeams: ITeam[] = [];
|
||||
if (isOnGlobalTeam) {
|
||||
currentUserTeams = teams;
|
||||
} else if (currentUser && currentUser.teams) {
|
||||
currentUserTeams = currentUser.teams;
|
||||
}
|
||||
|
||||
const allTeamOption = [
|
||||
{
|
||||
disabled: false,
|
||||
label: "All teams",
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const sortedCurrentUserTeamOptions = getSortedTeamOptions(currentUserTeams);
|
||||
|
||||
return !hideAllTeamsOption
|
||||
? allTeamOption.concat(sortedCurrentUserTeamOptions)
|
||||
: sortedCurrentUserTeamOptions;
|
||||
};
|
||||
|
||||
export const getValidatedTeamId = (
|
||||
teams: ITeam[],
|
||||
teamId: number,
|
||||
|
|
@ -766,6 +738,5 @@ export default {
|
|||
setupData,
|
||||
frontendFormattedConfig,
|
||||
syntaxHighlight,
|
||||
generateTeamFilterDropdownOptions,
|
||||
getValidatedTeamId,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import paths from "router/paths";
|
||||
import { Link } from "react-router";
|
||||
import { AppContext } from "context/app";
|
||||
import { find } from "lodash";
|
||||
|
||||
import hostSummaryAPI from "services/entities/host_summary";
|
||||
import teamsAPI from "services/entities/teams";
|
||||
import { IHostSummary, IHostSummaryPlatforms } from "interfaces/host_summary";
|
||||
import { ISoftware } from "interfaces/software";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { getSortedTeamOptions } from "fleet/helpers";
|
||||
import sortUtils from "utilities/sort";
|
||||
|
||||
import TeamsDropdown from "components/TeamsDropdown";
|
||||
import Button from "components/buttons/Button";
|
||||
import InfoCard from "./components/InfoCard";
|
||||
import HostsStatus from "./cards/HostsStatus";
|
||||
import HostsSummary from "./cards/HostsSummary";
|
||||
|
|
@ -20,7 +19,6 @@ import ActivityFeed from "./cards/ActivityFeed";
|
|||
import Software from "./cards/Software";
|
||||
import LearnFleet from "./cards/LearnFleet";
|
||||
import WelcomeHost from "./cards/WelcomeHost";
|
||||
import LinkArrow from "../../../assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png";
|
||||
|
||||
interface ITeamsResponse {
|
||||
teams: ITeam[];
|
||||
|
|
@ -41,6 +39,7 @@ const Homepage = (): JSX.Element => {
|
|||
currentTeam,
|
||||
isPremiumTier,
|
||||
isPreviewMode,
|
||||
isOnGlobalTeam,
|
||||
setCurrentTeam,
|
||||
} = useContext(AppContext);
|
||||
|
||||
|
|
@ -60,7 +59,17 @@ const Homepage = (): JSX.Element => {
|
|||
ITeam[]
|
||||
>(["teams"], () => teamsAPI.loadAll(), {
|
||||
enabled: !!isPremiumTier,
|
||||
select: (data: ITeamsResponse) => data.teams,
|
||||
select: (data: ITeamsResponse) =>
|
||||
data.teams.sort((a, b) => sortUtils.caseInsensitiveAsc(a.name, b.name)),
|
||||
onSuccess: (responseTeams) => {
|
||||
if (!isOnGlobalTeam) {
|
||||
const sortedTeams = getSortedTeamOptions(responseTeams);
|
||||
const firstTeamOption = responseTeams.find(
|
||||
(responseTeam) => responseTeam.id === sortedTeams[0].value
|
||||
);
|
||||
setCurrentTeam(firstTeamOption);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleTeamSelect = (teamId: number) => {
|
||||
|
|
@ -94,68 +103,72 @@ const Homepage = (): JSX.Element => {
|
|||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
{isPremiumTier ? (
|
||||
<TeamsDropdown
|
||||
currentTeamId={currentTeam?.id || 0}
|
||||
isLoading={isLoadingTeams}
|
||||
teams={teams || []}
|
||||
onChange={(newSelectedValue: number) =>
|
||||
handleTeamSelect(newSelectedValue)
|
||||
}
|
||||
<div className={`${baseClass}__wrapper body-wrap`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__text`}>
|
||||
<div className={`${baseClass}__title`}>
|
||||
{isPremiumTier && teams && teams.length > 1 && (
|
||||
<TeamsDropdown
|
||||
selectedTeamId={currentTeam?.id || 0}
|
||||
currentUserTeams={teams || []}
|
||||
onChange={(newSelectedValue: number) =>
|
||||
handleTeamSelect(newSelectedValue)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isPremiumTier && teams && teams.length === 1 && (
|
||||
<h1>{teams[0].name}</h1>
|
||||
)}
|
||||
{!isPremiumTier && <h1>{config?.org_name}</h1>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__section one-column`}>
|
||||
<InfoCard
|
||||
title="Hosts"
|
||||
action={{
|
||||
type: "link",
|
||||
to:
|
||||
MANAGE_HOSTS +
|
||||
TAGGED_TEMPLATES.hostsByTeamRoute(currentTeam?.id),
|
||||
text: "View all hosts",
|
||||
}}
|
||||
total_host_count={totalCount}
|
||||
>
|
||||
<HostsSummary
|
||||
currentTeamId={currentTeam?.id}
|
||||
macCount={macCount}
|
||||
windowsCount={windowsCount}
|
||||
/>
|
||||
) : (
|
||||
<h1 className={`${baseClass}__title`}>
|
||||
<span>{config?.org_name}</span>
|
||||
</h1>
|
||||
)}
|
||||
</InfoCard>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__section one-column`}>
|
||||
<InfoCard
|
||||
title="Hosts"
|
||||
action={{
|
||||
type: "link",
|
||||
to:
|
||||
MANAGE_HOSTS + TAGGED_TEMPLATES.hostsByTeamRoute(currentTeam?.id),
|
||||
text: "View all hosts",
|
||||
}}
|
||||
total_host_count={totalCount}
|
||||
<div className={`${baseClass}__section one-column`}>
|
||||
<InfoCard title="">
|
||||
<HostsStatus
|
||||
onlineCount={onlineCount}
|
||||
offlineCount={offlineCount}
|
||||
newCount={newCount}
|
||||
/>
|
||||
</InfoCard>
|
||||
</div>
|
||||
{isPreviewMode && (
|
||||
<div className={`${baseClass}__section two-column`}>
|
||||
<InfoCard title="Welcome to Fleet">
|
||||
<WelcomeHost />
|
||||
</InfoCard>
|
||||
<InfoCard title="Learn how to use Fleet">
|
||||
<LearnFleet />
|
||||
</InfoCard>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`
|
||||
${baseClass}__section
|
||||
${currentTeam ? "one" : "two"}-column
|
||||
`}
|
||||
>
|
||||
<HostsSummary
|
||||
currentTeamId={currentTeam?.id}
|
||||
macCount={macCount}
|
||||
windowsCount={windowsCount}
|
||||
/>
|
||||
</InfoCard>
|
||||
</div>
|
||||
<div className={`${baseClass}__section one-column`}>
|
||||
<InfoCard title="">
|
||||
<HostsStatus
|
||||
onlineCount={onlineCount}
|
||||
offlineCount={offlineCount}
|
||||
newCount={newCount}
|
||||
/>
|
||||
</InfoCard>
|
||||
</div>
|
||||
{isPreviewMode && (
|
||||
<div className={`${baseClass}__section two-column`}>
|
||||
<InfoCard title="Welcome to Fleet">
|
||||
<WelcomeHost />
|
||||
</InfoCard>
|
||||
<InfoCard title="Learn how to use Fleet">
|
||||
<LearnFleet />
|
||||
</InfoCard>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`
|
||||
${baseClass}__section
|
||||
${currentTeam ? "one" : "two"}-column
|
||||
`}
|
||||
>
|
||||
{!currentTeam && (
|
||||
<InfoCard
|
||||
title="Software"
|
||||
action={{
|
||||
|
|
@ -169,12 +182,12 @@ const Homepage = (): JSX.Element => {
|
|||
setIsSoftwareModalOpen={setIsSoftwareModalOpen}
|
||||
/>
|
||||
</InfoCard>
|
||||
)}
|
||||
{!isPreviewMode && !currentTeam && (
|
||||
<InfoCard title="Activity">
|
||||
<ActivityFeed />
|
||||
</InfoCard>
|
||||
)}
|
||||
{!isPreviewMode && !currentTeam && isOnGlobalTeam && (
|
||||
<InfoCard title="Activity">
|
||||
<ActivityFeed />
|
||||
</InfoCard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
.homepage {
|
||||
min-height: 0;
|
||||
padding: $pad-xxlarge;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
background-color: $ui-off-white;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
.homepage__wrapper {
|
||||
background-color: $ui-off-white;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $small;
|
||||
font-weight: $regular;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
&__header-wrap {
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -22,15 +22,17 @@
|
|||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
||||
.form-field--dropdown {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.Select-control {
|
||||
background-color: $ui-off-white !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: $large;
|
||||
|
||||
.fleeticon {
|
||||
color: $core-fleet-blue;
|
||||
margin-right: 15px;
|
||||
|
|
@ -57,7 +59,7 @@
|
|||
&.two-column {
|
||||
row-gap: $pad-medium;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (min-width: 990px) {
|
||||
&.two-column {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__header-wrap {
|
||||
.form-field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__block {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
.settings-wrapper {
|
||||
h1 {
|
||||
margin-bottom: $pad-small;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 38px;
|
||||
position: relative;
|
||||
|
||||
// fake padding for h1 while sticky
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: $pad-xxlarge;
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ const TeamDetailsWrapper = ({
|
|||
location: { pathname },
|
||||
params: routeParams,
|
||||
}: ITeamDetailsPageProps): JSX.Element => {
|
||||
const { isGlobalAdmin, setCurrentTeam } = useContext(AppContext);
|
||||
const { isGlobalAdmin, isOnGlobalTeam, setCurrentTeam } = useContext(
|
||||
AppContext
|
||||
);
|
||||
|
||||
const isLoadingTeams = useSelector(
|
||||
(state: IRootState) => state.entities.teams.loading
|
||||
|
|
@ -213,8 +215,9 @@ const TeamDetailsWrapper = ({
|
|||
setTeamMenuIsOpen(false);
|
||||
};
|
||||
|
||||
const teamDetailsClasses = classnames(baseClass, {
|
||||
const teamWrapperClasses = classnames(baseClass, {
|
||||
"team-select-open": teamMenuIsOpen,
|
||||
"team-settings": !isOnGlobalTeam,
|
||||
});
|
||||
|
||||
if (isLoadingTeams || team === undefined) {
|
||||
|
|
@ -232,7 +235,7 @@ const TeamDetailsWrapper = ({
|
|||
const adminTeams = isGlobalAdmin ? teams : userAdminTeams;
|
||||
|
||||
return (
|
||||
<div className={teamDetailsClasses}>
|
||||
<div className={teamWrapperClasses}>
|
||||
<TabsWrapper>
|
||||
<>
|
||||
{isGlobalAdmin && (
|
||||
|
|
@ -248,10 +251,8 @@ const TeamDetailsWrapper = ({
|
|||
<h1>{team.name}</h1>
|
||||
) : (
|
||||
<TeamsDropdown
|
||||
currentTeamId={toNumber(routeParams.team_id)}
|
||||
isLoading={isLoadingTeams}
|
||||
teams={adminTeams || []}
|
||||
hideAllTeamsOption
|
||||
selectedTeamId={toNumber(routeParams.team_id)}
|
||||
currentUserTeams={adminTeams || []}
|
||||
onChange={(newSelectedValue: number) =>
|
||||
handleTeamSelect(newSelectedValue)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
.team-details {
|
||||
padding: 25px $pad-xlarge 50px; // different to pad sticky subnav properly
|
||||
|
||||
&.team-settings {
|
||||
padding: $pad-xxlarge;
|
||||
}
|
||||
|
||||
&__loading-spinner {
|
||||
// NOTE: this value was chosen just cause it looked right. Might want to
|
||||
// come back and change it later to be more exact.
|
||||
|
|
@ -23,7 +27,6 @@
|
|||
}
|
||||
|
||||
&__team-header {
|
||||
margin-top: $pad-large;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $pad-medium;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { IStatusLabels } from "interfaces/status_labels";
|
|||
import { ITeam } from "interfaces/team";
|
||||
import { useDeepEffect } from "utilities/hooks"; // @ts-ignore
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
import sortUtils from "utilities/sort";
|
||||
import {
|
||||
PLATFORM_LABEL_DISPLAY_NAMES,
|
||||
PolicyResponse,
|
||||
|
|
@ -312,7 +313,13 @@ const ManageHostsPage = ({
|
|||
() => teamsAPI.loadAll(),
|
||||
{
|
||||
enabled: !!isPremiumTier,
|
||||
select: (data: ITeamsResponse) => data.teams,
|
||||
select: (data: ITeamsResponse) =>
|
||||
data.teams.sort((a, b) => sortUtils.caseInsensitiveAsc(a.name, b.name)),
|
||||
onSuccess: (responseTeams: ITeam[]) => {
|
||||
if (responseTeams.length === 1) {
|
||||
setCurrentTeam(responseTeams[0]);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -1066,9 +1073,8 @@ const ManageHostsPage = ({
|
|||
|
||||
const renderTeamsFilterDropdown = () => (
|
||||
<TeamsDropdown
|
||||
teams={teams || []}
|
||||
isLoading={isLoadingTeams}
|
||||
currentTeamId={
|
||||
currentUserTeams={teams || []}
|
||||
selectedTeamId={
|
||||
(policyId && policy?.team_id) || (currentTeam?.id as number)
|
||||
}
|
||||
onChange={(newSelectedValue: number) =>
|
||||
|
|
@ -1333,7 +1339,16 @@ const ManageHostsPage = ({
|
|||
return (
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__text`}>
|
||||
{renderTeamsFilterDropdown()}
|
||||
<div className={`${baseClass}__title`}>
|
||||
{!isPremiumTier && <h1>Hosts</h1>}
|
||||
{isPremiumTier &&
|
||||
teams &&
|
||||
teams.length > 1 &&
|
||||
renderTeamsFilterDropdown()}
|
||||
{isPremiumTier && teams && teams.length === 1 && (
|
||||
<h1>{teams[0].name}</h1>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1597,10 +1612,13 @@ const ManageHostsPage = ({
|
|||
</div>
|
||||
</div>
|
||||
{renderActiveFilterBlock()}
|
||||
<div className={`${baseClass}__info-banners`}>
|
||||
{renderNoEnrollSecretBanner()}
|
||||
{renderSoftwareVulnerabilities()}
|
||||
</div>
|
||||
{renderNoEnrollSecretBanner() ||
|
||||
(renderSoftwareVulnerabilities() && (
|
||||
<div className={`${baseClass}__info-banners`}>
|
||||
{renderNoEnrollSecretBanner()}
|
||||
{renderSoftwareVulnerabilities()}
|
||||
</div>
|
||||
))}
|
||||
{config && (!isPremiumTier || teams) && renderTable(selectedTeam)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
.manage-hosts {
|
||||
.header-wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $pad-medium;
|
||||
height: 38px;
|
||||
|
||||
.button-wrap {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ import TableDataError from "components/TableDataError";
|
|||
import Button from "components/buttons/Button";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
import IconToolTip from "components/IconToolTip";
|
||||
import TeamsDropdown from "components/TeamsDropdown";
|
||||
import PoliciesListWrapper from "./components/PoliciesListWrapper";
|
||||
import RemovePoliciesModal from "./components/RemovePoliciesModal";
|
||||
import TeamsDropdown from "./components/TeamsDropdown";
|
||||
|
||||
const baseClass = "manage-policies-page";
|
||||
|
||||
|
|
@ -77,16 +77,16 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
isTeamMaintainer(user, teamId) ||
|
||||
isTeamAdmin(user, teamId);
|
||||
|
||||
const { data: teams } = useQuery<{ teams: ITeam[] }, Error, ITeam[]>(
|
||||
["teams"],
|
||||
() => teamsAPI.loadAll({}),
|
||||
{
|
||||
enabled: !!isPremiumTier,
|
||||
select: (data) => data.teams,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
const { data: teams, isLoading: isLoadingTeams } = useQuery<
|
||||
{ teams: ITeam[] },
|
||||
Error,
|
||||
ITeam[]
|
||||
>(["teams"], () => teamsAPI.loadAll({}), {
|
||||
enabled: !!isPremiumTier,
|
||||
select: (data) => data.teams,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const { data: fleetQueries } = useQuery(
|
||||
["fleetQueries"],
|
||||
|
|
@ -110,8 +110,8 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
const [isLoadingTeamPolicies, setIsLoadingTeamPolicies] = useState(true);
|
||||
const [isTeamPoliciesError, setIsTeamPoliciesError] = useState(false);
|
||||
const [userTeams, setUserTeams] = useState<ITeam[] | never[] | null>(null);
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(
|
||||
parseInt(location?.query?.team_id, 10) || null
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<number>(
|
||||
parseInt(location?.query?.team_id, 10) || 0
|
||||
);
|
||||
const [selectedPolicyIds, setSelectedPolicyIds] = useState<
|
||||
number[] | never[]
|
||||
|
|
@ -166,7 +166,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
[getGlobalPolicies, getTeamPolicies]
|
||||
);
|
||||
|
||||
const handleChangeSelectedTeam = (id: number) => {
|
||||
const handleTeamSelect = (id: number) => {
|
||||
const { MANAGE_POLICIES } = PATHS;
|
||||
const path = id ? `${MANAGE_POLICIES}?team_id=${id}` : MANAGE_POLICIES;
|
||||
router.replace(path);
|
||||
|
|
@ -249,7 +249,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
if (isOnGlobalTeam) {
|
||||
// For global users, default to zero (i.e. all teams).
|
||||
if (teamId !== 0) {
|
||||
handleChangeSelectedTeam(0);
|
||||
handleTeamSelect(0);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -258,7 +258,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
// API request will not be triggered.
|
||||
teamId = userTeams[0]?.id || null;
|
||||
if (teamId) {
|
||||
handleChangeSelectedTeam(teamId);
|
||||
handleTeamSelect(teamId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -329,14 +329,20 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
<div className={`${baseClass}__title`}>
|
||||
{isFreeTier && <h1>Policies</h1>}
|
||||
{isPremiumTier &&
|
||||
userTeams !== null &&
|
||||
selectedTeamId !== null && (
|
||||
userTeams &&
|
||||
userTeams.length > 1 &&
|
||||
selectedTeamId >= 0 && (
|
||||
<TeamsDropdown
|
||||
currentUserTeams={userTeams}
|
||||
onChange={handleChangeSelectedTeam}
|
||||
selectedTeam={selectedTeamId}
|
||||
selectedTeamId={selectedTeamId}
|
||||
currentUserTeams={userTeams || []}
|
||||
onChange={(newSelectedValue: number) =>
|
||||
handleTeamSelect(newSelectedValue)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isPremiumTier && userTeams && userTeams.length === 1 && (
|
||||
<h1>{userTeams[0].name}</h1>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -351,20 +357,22 @@ const ManagePolicyPage = (managePoliciesPageProps: {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__description`}>
|
||||
{isPremiumTier && !!selectedTeamId && (
|
||||
<p>
|
||||
Add additional policies for <b>all hosts assigned to this team</b>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
{showDefaultDescription && (
|
||||
<p>
|
||||
Add policies for <b>all of your hosts</b> to see which pass your
|
||||
organization’s standards.{" "}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{!isLoadingTeams && (
|
||||
<div className={`${baseClass}__description`}>
|
||||
{isPremiumTier && !!selectedTeamId && (
|
||||
<p>
|
||||
Add additional policies for{" "}
|
||||
<b>all hosts assigned to this team</b>.
|
||||
</p>
|
||||
)}
|
||||
{showDefaultDescription && (
|
||||
<p>
|
||||
Add policies for <b>all of your hosts</b> to see which pass your
|
||||
organization’s standards.{" "}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!!updateInterval && showInfoBanner && (
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 38px;
|
||||
margin-bottom: $pad-small;
|
||||
}
|
||||
|
||||
&__header {
|
||||
|
|
@ -130,7 +129,7 @@
|
|||
top: 0;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
@include button-variant(
|
||||
$core-vibrant-blue,
|
||||
$core-vibrant-blue-over,
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
import React, { useContext, useMemo } from "react";
|
||||
import { isEmpty } from "lodash";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import { ITeam } from "interfaces/team";
|
||||
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
|
||||
const generateDropdownOptions = (
|
||||
teams: ITeam[] | undefined,
|
||||
includeAll: boolean | undefined
|
||||
) => {
|
||||
if (!teams) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const options = teams.map((team) => ({
|
||||
disabled: false,
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
}));
|
||||
|
||||
if (includeAll) {
|
||||
options.unshift({
|
||||
disabled: false,
|
||||
label: "All teams",
|
||||
value: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const TeamsDropdown = (dropdownProps: {
|
||||
currentUserTeams: ITeam[];
|
||||
onChange: (id: number) => void;
|
||||
selectedTeam: number;
|
||||
}): JSX.Element => {
|
||||
const { currentUserTeams, onChange, selectedTeam } = dropdownProps;
|
||||
const { isOnGlobalTeam } = useContext(AppContext);
|
||||
|
||||
const dropdownOptions = useMemo(
|
||||
() => generateDropdownOptions(currentUserTeams, isOnGlobalTeam),
|
||||
[currentUserTeams, isOnGlobalTeam]
|
||||
);
|
||||
|
||||
const selectedValue = dropdownOptions.find(
|
||||
(option) => selectedTeam === option.value
|
||||
)
|
||||
? selectedTeam
|
||||
: dropdownOptions[0]?.value;
|
||||
|
||||
return isEmpty(currentUserTeams) ? (
|
||||
<h1>Policies</h1>
|
||||
) : (
|
||||
<div>
|
||||
<Dropdown
|
||||
value={selectedValue}
|
||||
placeholder={"All teams"}
|
||||
className="teams-dropdown"
|
||||
options={dropdownOptions}
|
||||
searchable={false}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default TeamsDropdown;
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
.teams-dropdown {
|
||||
border: 0 !important;
|
||||
position: relative;
|
||||
|
||||
:hover {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
&.is-focused {
|
||||
.Select-control {
|
||||
border: 0 !important;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.Select-menu-outer {
|
||||
position: absolute;
|
||||
width: 330px;
|
||||
min-width: 125px;
|
||||
left: -12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.Select-control {
|
||||
background-color: #fff;
|
||||
border: 0 !important;
|
||||
border-radius: none;
|
||||
position: none;
|
||||
width: max-content; // move select arrow
|
||||
height: 20px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover .Select-value-label {
|
||||
color: $core-vibrant-blue !important;
|
||||
}
|
||||
|
||||
.Select-arrow-zone {
|
||||
padding-left: $pad-small;
|
||||
|
||||
.Select-arrow {
|
||||
top: 1px !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.Select-multi-value-wrapper {
|
||||
width: max-content; // move select arrow
|
||||
height: 20px;
|
||||
margin-bottom: $pad-xsmall;
|
||||
|
||||
.Select-input {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.Select-value {
|
||||
position: relative; // move select arrow
|
||||
display: inline-block; // move select arrow
|
||||
line-height: 28px;
|
||||
padding: 0;
|
||||
border: 0 !important;
|
||||
background-color: #fff !important;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
|
||||
&.is-focused {
|
||||
border: 0 !important;
|
||||
}
|
||||
:hover {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.Select-value-label {
|
||||
font-size: $large !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./TeamsDropdown";
|
||||
|
|
@ -209,11 +209,6 @@ const ManageQueriesPage = (): JSX.Element => {
|
|||
<h1 className={`${baseClass}__title`}>
|
||||
<span>Queries</span>
|
||||
</h1>
|
||||
<div className={`${baseClass}__description`}>
|
||||
<p>
|
||||
Manage queries to ask specific questions about your devices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isOnlyObserver && !!fleetQueries?.length && (
|
||||
|
|
@ -228,6 +223,9 @@ const ManageQueriesPage = (): JSX.Element => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__description`}>
|
||||
<p>Manage queries to ask specific questions about your devices.</p>
|
||||
</div>
|
||||
<div>
|
||||
{isTableDataLoading && !fleetQueriesError && <Spinner />}
|
||||
{!isTableDataLoading && fleetQueriesError ? (
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
.manage-queries-page {
|
||||
&__header-wrap {
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
|
|
@ -37,8 +41,7 @@
|
|||
}
|
||||
|
||||
&__description {
|
||||
margin: 0 0 $pad-medium;
|
||||
padding-top: $pad-xsmall;
|
||||
margin: 0 0 $pad-xxlarge;
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { useQuery } from "react-query";
|
|||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppContext } from "context/app";
|
||||
import { push } from "react-router-redux";
|
||||
import { find } from "lodash";
|
||||
|
||||
// @ts-ignore
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
|
|
@ -20,12 +19,12 @@ import fleetQueriesAPI from "services/entities/queries";
|
|||
import teamsAPI from "services/entities/teams";
|
||||
// @ts-ignore
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
import permissionUtils from "utilities/permissions";
|
||||
import sortUtils from "utilities/sort";
|
||||
|
||||
import paths from "router/paths";
|
||||
import Button from "components/buttons/Button";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import TeamsDropdown from "components/TeamsDropdown";
|
||||
import IconToolTip from "components/IconToolTip";
|
||||
import TableDataError from "components/TableDataError";
|
||||
import ScheduleListWrapper from "./components/ScheduleListWrapper";
|
||||
|
|
@ -117,33 +116,46 @@ interface IFormData {
|
|||
team_id?: number;
|
||||
}
|
||||
|
||||
interface ITeamOptions {
|
||||
disabled: boolean;
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
const ManageSchedulePage = ({
|
||||
params: { team_id },
|
||||
location,
|
||||
}: ITeamSchedulesPageProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { MANAGE_PACKS, MANAGE_SCHEDULE, MANAGE_TEAM_SCHEDULE } = paths;
|
||||
const handleAdvanced = () => dispatch(push(MANAGE_PACKS));
|
||||
|
||||
const {
|
||||
currentUser,
|
||||
isOnGlobalTeam,
|
||||
isPremiumTier,
|
||||
isAnyTeamMaintainerOrTeamAdmin,
|
||||
} = useContext(AppContext);
|
||||
const { currentUser, isOnGlobalTeam, isPremiumTier, isFreeTier } = useContext(
|
||||
AppContext
|
||||
);
|
||||
|
||||
const { data: teams } = useQuery(["teams"], () => teamsAPI.loadAll({}), {
|
||||
enabled: !!isPremiumTier,
|
||||
select: (data) => data.teams,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const filterAndSortTeamOptions = (allTeams: ITeam[], userTeams: ITeam[]) => {
|
||||
const filteredSortedTeams = allTeams
|
||||
.sort((teamA: ITeam, teamB: ITeam) =>
|
||||
sortUtils.caseInsensitiveAsc(teamA.name, teamB.name)
|
||||
)
|
||||
.filter((team: ITeam) => {
|
||||
const userTeam = userTeams.find(
|
||||
(thisUserTeam) => thisUserTeam.id === team.id
|
||||
);
|
||||
return userTeam?.role !== "observer" ? team : null;
|
||||
});
|
||||
|
||||
return filteredSortedTeams;
|
||||
};
|
||||
|
||||
const { data: teams, isLoading: isLoadingTeams } = useQuery(
|
||||
["teams"],
|
||||
() => teamsAPI.loadAll({}),
|
||||
{
|
||||
enabled: !!isPremiumTier,
|
||||
select: (data) => {
|
||||
return currentUser?.teams
|
||||
? filterAndSortTeamOptions(data.teams, currentUser.teams)
|
||||
: data.teams;
|
||||
},
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: fleetQueries } = useQuery(
|
||||
["fleetQueries"],
|
||||
|
|
@ -155,51 +167,18 @@ const ManageSchedulePage = ({
|
|||
}
|
||||
);
|
||||
|
||||
let teamId = parseInt(team_id, 10);
|
||||
const teamId = team_id ? parseInt(team_id, 10) : 0;
|
||||
|
||||
// isTeamMaintainerOrTeamAdmin set locally and not in AppContext
|
||||
const isTeamMaintainerOrTeamAdmin = (() => {
|
||||
return !!permissionUtils.isTeamMaintainerOrTeamAdmin(currentUser, teamId);
|
||||
})();
|
||||
|
||||
const onChangeSelectedTeam = (selectedTeamId: number) => {
|
||||
if (isNaN(selectedTeamId)) {
|
||||
dispatch(push(MANAGE_SCHEDULE));
|
||||
} else {
|
||||
const handleTeamSelect = (selectedTeamId: number) => {
|
||||
if (selectedTeamId) {
|
||||
dispatch(push(MANAGE_TEAM_SCHEDULE(selectedTeamId)));
|
||||
} else {
|
||||
dispatch(push(MANAGE_SCHEDULE));
|
||||
}
|
||||
};
|
||||
|
||||
const loadFirstMaintainerOrAdminTeam = () => {
|
||||
if (currentUser) {
|
||||
const adminOrMaintainerTeam = currentUser.teams.find((team) => {
|
||||
return team.role === "admin" || team.role === "maintainer"
|
||||
? team.id
|
||||
: null;
|
||||
});
|
||||
if (adminOrMaintainerTeam) {
|
||||
teamId = adminOrMaintainerTeam.id;
|
||||
onChangeSelectedTeam(teamId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOnGlobalTeam && !isTeamMaintainerOrTeamAdmin && !teamId) {
|
||||
loadFirstMaintainerOrAdminTeam();
|
||||
}
|
||||
|
||||
if (!isOnGlobalTeam && !isTeamMaintainerOrTeamAdmin && teamId) {
|
||||
if (currentUser) {
|
||||
const canLoadTeam = currentUser.teams.find((team) => {
|
||||
return (
|
||||
(team.role === "admin" || team.role === "maintainer") &&
|
||||
team.id === teamId
|
||||
);
|
||||
});
|
||||
if (!canLoadTeam) {
|
||||
loadFirstMaintainerOrAdminTeam();
|
||||
}
|
||||
}
|
||||
if (!isOnGlobalTeam && !teamId && teams) {
|
||||
handleTeamSelect(teams[0].id);
|
||||
}
|
||||
|
||||
// TODO: move team scheduled queries and global scheduled queries into services entities, remove redux
|
||||
|
|
@ -233,7 +212,7 @@ const ManageSchedulePage = ({
|
|||
const inheritedQueryOrQueries =
|
||||
allTeamsScheduledQueriesList.length === 1 ? "query" : "queries";
|
||||
|
||||
const selectedTeam = isNaN(teamId) ? "global" : teamId;
|
||||
const selectedTeam = !teamId ? "global" : teamId;
|
||||
|
||||
const selectedTeamData =
|
||||
teams?.find((team: ITeam) => selectedTeam === team.id) || undefined;
|
||||
|
|
@ -271,55 +250,6 @@ const ManageSchedulePage = ({
|
|||
setShowRemoveScheduledQueryModal(!showRemoveScheduledQueryModal);
|
||||
}, [showRemoveScheduledQueryModal, setShowRemoveScheduledQueryModal]);
|
||||
|
||||
const generateTeamOptionsDropdownItems = (): ITeamOptions[] => {
|
||||
const teamOptions: ITeamOptions[] = [];
|
||||
|
||||
if (isAnyTeamMaintainerOrTeamAdmin && currentUser) {
|
||||
currentUser.teams.forEach((team) => {
|
||||
if (team.role === "admin" || team.role === "maintainer") {
|
||||
teamOptions.push({
|
||||
disabled: false,
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (isOnGlobalTeam && teams) {
|
||||
teamOptions.push({
|
||||
disabled: false,
|
||||
label: "All teams",
|
||||
value: "global",
|
||||
});
|
||||
|
||||
teams.forEach((team: ITeam) => {
|
||||
teamOptions.push({
|
||||
disabled: false,
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return teamOptions;
|
||||
};
|
||||
|
||||
const renderTitleOrDropdown = (): JSX.Element => {
|
||||
const dropDownOptions = generateTeamOptionsDropdownItems();
|
||||
return dropDownOptions.length === 1 ? (
|
||||
<h1>{dropDownOptions[0].label}</h1>
|
||||
) : (
|
||||
<Dropdown
|
||||
value={selectedTeam}
|
||||
className={`${baseClass}__team-dropdown`}
|
||||
options={dropDownOptions}
|
||||
searchable={false}
|
||||
onChange={(newSelectedValue: number) =>
|
||||
onChangeSelectedTeam(newSelectedValue)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const onRemoveScheduledQueryClick = (
|
||||
selectedTableQueryIds: number[]
|
||||
): void => {
|
||||
|
|
@ -435,57 +365,33 @@ const ManageSchedulePage = ({
|
|||
[dispatch, teamId, toggleScheduleEditorModal]
|
||||
);
|
||||
|
||||
if (selectedTeam === "global" && isTeamMaintainerOrTeamAdmin) {
|
||||
const teamMaintainerTeams = generateTeamOptionsDropdownItems();
|
||||
if (teamMaintainerTeams.length) {
|
||||
dispatch(
|
||||
push(MANAGE_TEAM_SCHEDULE(Number(teamMaintainerTeams[0].value)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__wrapper body-wrap`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
{!isPremiumTier ? (
|
||||
<div className={`${baseClass}__text`}>
|
||||
<h1 className={`${baseClass}__title`}>
|
||||
<span>Schedule</span>
|
||||
</h1>
|
||||
<div className={`${baseClass}__description`}>
|
||||
<p>
|
||||
Schedule recurring queries for your hosts. Fleet’s query
|
||||
schedule lets you add queries which are executed at regular
|
||||
intervals.
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__text`}>
|
||||
<div className={`${baseClass}__title`}>
|
||||
{isFreeTier && <h1>Schedule</h1>}
|
||||
{isPremiumTier && teams && teams.length > 1 && (
|
||||
<TeamsDropdown
|
||||
selectedTeamId={teamId}
|
||||
currentUserTeams={teams || []}
|
||||
onChange={(newSelectedValue: number) =>
|
||||
handleTeamSelect(newSelectedValue)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isPremiumTier && teams && teams.length === 1 && (
|
||||
<h1>{teams[0].name}</h1>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{renderTitleOrDropdown()}
|
||||
<div className={`${baseClass}__description`}>
|
||||
{isNaN(teamId) ? (
|
||||
<p>
|
||||
Schedule queries to run at regular intervals across{" "}
|
||||
<b>all of your hosts</b>.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Schedule additional queries for all hosts assigned to this
|
||||
team.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Hide CTA Buttons if no schedule or schedule error */}
|
||||
{allScheduledQueriesList.length !== 0 &&
|
||||
allScheduledQueriesError.length !== 0 && (
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
{!isTeamMaintainerOrTeamAdmin && (
|
||||
{isOnGlobalTeam && (
|
||||
<Button
|
||||
variant="inverse"
|
||||
onClick={handleAdvanced}
|
||||
|
|
@ -504,17 +410,35 @@ const ManageSchedulePage = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{renderTable(
|
||||
onRemoveScheduledQueryClick,
|
||||
onEditScheduledQueryClick,
|
||||
allScheduledQueriesList,
|
||||
allScheduledQueriesError,
|
||||
toggleScheduleEditorModal,
|
||||
isOnGlobalTeam || false,
|
||||
selectedTeamData
|
||||
<div className={`${baseClass}__description`}>
|
||||
{!isLoadingTeams && (
|
||||
<div>
|
||||
{!teamId ? (
|
||||
<p>
|
||||
Schedule queries to run at regular intervals across{" "}
|
||||
<strong>all of your hosts.</strong>
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Schedule queries for{" "}
|
||||
<strong>all hosts assigned to this team.</strong>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!isLoadingTeams &&
|
||||
renderTable(
|
||||
onRemoveScheduledQueryClick,
|
||||
onEditScheduledQueryClick,
|
||||
allScheduledQueriesList,
|
||||
allScheduledQueriesError,
|
||||
toggleScheduleEditorModal,
|
||||
isOnGlobalTeam || false,
|
||||
selectedTeamData
|
||||
)}
|
||||
</div>
|
||||
{/* must use ternary for NaN */}
|
||||
{teamId && allTeamsScheduledQueriesList.length > 0 ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
&__description {
|
||||
margin: 0;
|
||||
padding-top: $pad-xsmall;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
|
|
@ -86,95 +86,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__team-dropdown {
|
||||
border: 0 !important;
|
||||
position: relative;
|
||||
|
||||
:hover {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
&.is-focused {
|
||||
.Select-control {
|
||||
border: 0 !important;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.Select-menu-outer {
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.Select.is-open {
|
||||
.Select-value-label {
|
||||
color: $core-vibrant-blue !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
.Select-control {
|
||||
background-color: #fff;
|
||||
border: 0 !important;
|
||||
border-radius: none;
|
||||
position: none;
|
||||
width: max-content; // move select arrow
|
||||
height: 20px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover .Select-value-label {
|
||||
color: $core-vibrant-blue !important;
|
||||
}
|
||||
|
||||
.Select-arrow-zone {
|
||||
padding-left: $pad-small;
|
||||
|
||||
.Select-arrow {
|
||||
top: 1px !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
.Select-multi-value-wrapper {
|
||||
width: max-content; // move select arrow
|
||||
height: 20px;
|
||||
margin-bottom: $pad-xsmall;
|
||||
|
||||
.Select-input {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.Select-value {
|
||||
position: relative; // move select arrow
|
||||
display: inline-block; // move select arrow
|
||||
line-height: 28px;
|
||||
padding: 0;
|
||||
border: 0 !important;
|
||||
background-color: #fff !important;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
|
||||
&.is-focused {
|
||||
border: 0 !important;
|
||||
}
|
||||
:hover {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.Select-value-label {
|
||||
font-size: $large !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__inherited-queries-button {
|
||||
margin: $pad-medium 0 0 0;
|
||||
color: $core-vibrant-blue;
|
||||
|
|
|
|||
|
|
@ -49,10 +49,9 @@ a {
|
|||
}
|
||||
|
||||
.body-wrap {
|
||||
padding: $pad-xxlarge 30px 0;
|
||||
padding: $pad-xxlarge;
|
||||
border-radius: 3px;
|
||||
background-color: $core-white;
|
||||
border: solid 1px $core-white;
|
||||
min-width: 798px;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue