Standardize TeamsDropdown component usage (#3135)

This commit is contained in:
Luke Heath 2021-12-01 17:37:33 -06:00 committed by GitHub
parent 7464e72ba8
commit e750eb9745
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 355 additions and 632 deletions

View file

@ -0,0 +1 @@
* Fix manage schedule page teams dropdown cutting off in some cases

View file

@ -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");

View file

@ -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

View file

@ -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>
);
};

View file

@ -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 @@
}
}
}
}
}

View file

@ -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`,
},
},
];

View file

@ -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,
};

View file

@ -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>
);

View file

@ -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));

View file

@ -3,6 +3,12 @@
display: flex;
flex-direction: column;
&__header-wrap {
.form-field {
margin-bottom: 0;
}
}
&__block {
display: flex;
flex-direction: row;

View file

@ -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;

View file

@ -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)
}

View file

@ -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;

View file

@ -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>
)}

View file

@ -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;

View file

@ -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
organizations 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
organizations standards.{" "}
</p>
)}
</div>
)}
{!!updateInterval && showInfoBanner && (
<InfoBanner className={`${baseClass}__sandbox-info`}>
<p>

View file

@ -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,

View file

@ -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;

View file

@ -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;
}
}
}
}
}

View file

@ -1 +0,0 @@
export { default } from "./TeamsDropdown";

View file

@ -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 ? (

View file

@ -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;

View file

@ -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. Fleets 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 ? (
<>

View file

@ -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;

View file

@ -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;
}