Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ngui/ui/src/api/restapi/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,3 +423,6 @@ export const DELETE_LAYOUT = "DELETE_LAYOUT";
export const RESTORE_PASSWORD = "RESTORE_PASSWORD";

export const VERIFY_EMAIL = "VERIFY_EMAIL";

export const SET_ORG_DATASOURCES_TAGS = "SET_ORG_DATASOURCES_TAGS";
export const GET_ORG_DATASOURCES_TAGS = "GET_ORG_DATASOURCES_TAGS";
10 changes: 9 additions & 1 deletion ngui/ui/src/api/restapi/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ import {
SET_ML_ARTIFACT,
SET_ML_DATASET_LABELS,
SET_ML_TASK_TAGS,
CREATE_ML_LEADERBOARD
CREATE_ML_LEADERBOARD,
SET_ORG_DATASOURCES_TAGS
} from "./actionTypes";

export const RESTAPI = "restapi";
Expand Down Expand Up @@ -149,6 +150,13 @@ const reducer = (state = {}, action) => {
pool: action.payload
}
};
case SET_ORG_DATASOURCES_TAGS:
return {
...state,
[action.label]: {
dataSourcesTags: action.payload
}
};
case DELETE_POOL:
// note: that code does nothing in current UI flow: GET_POOL storage at the moment of pool deletion is _that pool_.
// but if we place "remove button" to any other place (for example, at pools table) — filter will work
Expand Down
47 changes: 30 additions & 17 deletions ngui/ui/src/components/CloudAccountsTable/CloudAccountsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { DataSourceTagCell } from "@theme/shared/components/DataSourceTagCell/DataSourceTagCell";
import CaptionedCell from "components/CaptionedCell";
import Circle from "components/Circle";
import CloudLabel from "components/CloudLabel";
Expand Down Expand Up @@ -79,7 +80,6 @@ const NameCell = ({

const CloudAccountsTable = ({ cloudAccounts = [], isLoading = false }) => {
const navigate = useNavigate();

const theme = useTheme();

const { classes } = useStyles();
Expand All @@ -102,6 +102,14 @@ const CloudAccountsTable = ({ cloudAccounts = [], isLoading = false }) => {
accessorKey: "type",
cell: ({ cell }) => <CloudType type={cell.getValue()} />
},
{
header: intl.formatMessage({ id: "entitled" }),
id: "tags",
accessorKey: "id",
cell: ({ cell }) => <DataSourceTagCell dataSourceId={cell.getValue()} tagKey="entitlement" />,
enableSorting: false,
emptyValue: <FormattedMessage id="notEntitled" />
},
{
header: intl.formatMessage({ id: "resourcesChargedThisMonth" }),
id: "details.resources",
Expand Down Expand Up @@ -156,22 +164,27 @@ const CloudAccountsTable = ({ cloudAccounts = [], isLoading = false }) => {
return isLoading ? (
<TableLoader columnsCounter={columns.length} showHeader />
) : (
<Table
dataTestIds={{
container: "table_accs"
}}
data={data}
columns={columns}
localization={{
emptyMessageId: "noDataSources"
}}
pageSize={50}
withExpanded
actionBar={{
show: true,
definition: actionBarDefinition
}}
/>
<>
<Table
dataTestIds={{
container: "table_accs"
}}
data={data}
columns={columns}
localization={{
emptyMessageId: "noDataSources"
}}
pageSize={50}
withExpanded
actionBar={{
show: true,
definition: actionBarDefinition
}}
/>
<Typography variant="caption" color="text.primary">
<FormattedMessage id="entitledColumnDisclaimer" />
</Typography>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Stack } from "@mui/material";
import ActionBar from "components/ActionBar";
import CloudAccountsTable from "components/CloudAccountsTable";
import CloudExpensesChart from "components/CloudExpensesChart";
import InlineSeverityAlert from "components/InlineSeverityAlert";
import PageContentWrapper from "components/PageContentWrapper";
import SummaryGrid from "components/SummaryGrid";
import { useIsAllowed } from "hooks/useAllowedActions";
import { getSumByNestedObjectKey, isEmptyArray } from "utils/arrays";
import { SUMMARY_VALUE_COMPONENT_TYPES, SUMMARY_CARD_TYPES, AWS_CNR } from "utils/constants";
import { SPACING_2 } from "utils/layouts";
import { getPercentageChangeModule, round } from "utils/math";

type SummaryProps = {
totalExpenses: number;
totalForecast: number;
lastMonthCost: number;
isLoading?: boolean;
};

const actionBarDefinition = {
title: {
messageId: "dataSourcesTitle"
}
};

const Summary = ({ totalExpenses, totalForecast, lastMonthCost, isLoading = false }: SummaryProps) => {
const getSummaryData = () =>
lastMonthCost
? [
{
key: "totalExpensesMonthToDate",
type: SUMMARY_CARD_TYPES.EXTENDED,
valueComponentType: SUMMARY_VALUE_COMPONENT_TYPES.FormattedMoney,
valueComponentProps: {
value: totalExpenses
},
captionMessageId: "totalExpensesMonthToDate",
relativeValueComponentType: SUMMARY_VALUE_COMPONENT_TYPES.FormattedNumber,
relativeValueComponentProps: {
value: getPercentageChangeModule(totalExpenses, lastMonthCost) / 100,
format: "percentage"
},
relativeValueCaptionMessageId:
totalExpenses > lastMonthCost ? "moreThanForPreviousMonth" : "lessThanForPreviousMonth",
dataTestIds: {
cardTestId: "card_total_exp"
},
color: totalExpenses > lastMonthCost ? "error" : "success",
isLoading
},
{
key: "forecastForThisMonth",
type: SUMMARY_CARD_TYPES.EXTENDED,
valueComponentType: SUMMARY_VALUE_COMPONENT_TYPES.FormattedMoney,
valueComponentProps: {
value: totalForecast
},
captionMessageId: "forecastForThisMonth",
relativeValueComponentType: SUMMARY_VALUE_COMPONENT_TYPES.FormattedNumber,
relativeValueComponentProps: {
value: getPercentageChangeModule(totalForecast, lastMonthCost) / 100,
format: "percentage"
},
relativeValueCaptionMessageId:
totalForecast > lastMonthCost ? "moreThanForPreviousMonth" : "lessThanForPreviousMonth",
dataTestIds: {
cardTestId: "card_forecast"
},
color: totalForecast > lastMonthCost ? "error" : "success",
isLoading
}
]
: [
{
key: "totalExpensesMonthToDate",
valueComponentType: SUMMARY_VALUE_COMPONENT_TYPES.FormattedMoney,
valueComponentProps: {
value: totalExpenses
},
captionMessageId: "totalExpensesMonthToDate",
dataTestIds: {
cardTestId: "card_total_exp"
},
isLoading
},
{
key: "forecastForThisMonth",
valueComponentType: SUMMARY_VALUE_COMPONENT_TYPES.FormattedMoney,
valueComponentProps: {
value: totalForecast
},
captionMessageId: "forecastForThisMonth",
dataTestIds: {
cardTestId: "card_forecast"
},
isLoading
}
];

return <SummaryGrid summaryData={getSummaryData()} />;
};

const AwsLinkedAccountsWarning = () => {
const isManageCloudCredentialsAllowed = useIsAllowed({ requiredActions: ["MANAGE_CLOUD_CREDENTIALS"] });

return (
<InlineSeverityAlert
messageId="onlyAwsLinkedAccountsConnectedWarning"
messageValues={{
hasPermissionsToConnectDataSources: isManageCloudCredentialsAllowed
}}
severity="warning"
/>
);
};

const CloudAccountsOverview = ({ cloudAccounts = [], organizationLimit, isLoading = false }) => {
if (isEmptyArray(cloudAccounts)) {
return <></>;
}
const totalExpenses = round(getSumByNestedObjectKey(cloudAccounts, "details", "cost"), 2);
const totalForecast = round(getSumByNestedObjectKey(cloudAccounts, "details", "forecast"), 2);
const lastMonthCost = round(getSumByNestedObjectKey(cloudAccounts, "details", "last_month_cost"), 2);

const awsDataSources = cloudAccounts.filter(({ type }) => type === AWS_CNR);
const onlyAwsLinkedAccountsConnected = !isEmptyArray(awsDataSources) && awsDataSources.every(({ config }) => config?.linked);

return (
<>
<ActionBar data={actionBarDefinition} />
<PageContentWrapper>
<Stack spacing={SPACING_2}>
<div>
<Summary
totalExpenses={totalExpenses}
totalForecast={totalForecast}
lastMonthCost={lastMonthCost}
isLoading={isLoading}
/>
</div>
<div>
{(organizationLimit === 0 && totalForecast === 0) || totalExpenses === 0 ? null : (
<CloudExpensesChart
cloudAccounts={cloudAccounts}
limit={organizationLimit}
forecast={totalForecast}
isLoading={isLoading}
/>
)}
</div>
{!isLoading && onlyAwsLinkedAccountsConnected && (
<div>
<AwsLinkedAccountsWarning />
</div>
)}
<div>
<CloudAccountsTable cloudAccounts={cloudAccounts} isLoading={isLoading} />
</div>
</Stack>
</PageContentWrapper>
</>
);
};

export default CloudAccountsOverview;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import CloudAccountsOverview from "./CloudAccountsOverview";
import CloudAccountsOverviewMocked from "./CloudAccountsOverviewMocked";

export default CloudAccountsOverview;
export { CloudAccountsOverviewMocked };
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { useDataSourceTagsByOrg } from "@theme/shared/hooks/useDataSourceTagsByOrg";
import { getPool } from "api";
import { GET_POOL } from "api/restapi/actionTypes";
import CloudAccountsOverview from "components/CloudAccountsOverview";
import { useAllDataSources } from "hooks/coreData/useAllDataSources";
import { useApiData } from "hooks/useApiData";
import { useApiState } from "hooks/useApiState";
import { useOrganizationInfo } from "hooks/useOrganizationInfo";

const GetCloudAccountsContainer = () => {
const { organizationId } = useOrganizationInfo();
const { isLoading: isTagsLoading } = useDataSourceTagsByOrg(organizationId);
const dataSources = useAllDataSources();

const { organizationPoolId } = useOrganizationInfo();

const dispatch = useDispatch();

const {
apiData: { pool: { limit: organizationLimit = 0 } = {} }
} = useApiData(GET_POOL);

const { isLoading: isGetPoolLoading, shouldInvoke: shouldInvokeGetPool } = useApiState(GET_POOL, {
poolId: organizationPoolId
});

useEffect(() => {
if (organizationPoolId && shouldInvokeGetPool) {
dispatch(getPool(organizationPoolId));
}
}, [shouldInvokeGetPool, dispatch, organizationPoolId]);

return (
<CloudAccountsOverview
isLoading={isGetPoolLoading || isTagsLoading}
cloudAccounts={dataSources}
organizationLimit={organizationLimit}
/>
);
};

export default GetCloudAccountsContainer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import GetCloudAccountsContainer from "./GetCloudAccountsContainer";

export default GetCloudAccountsContainer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Theme } from "@mui/material";
import { makeStyles } from "tss-react/mui";
import { MPT_SPACING_2 } from "@theme/utils/layouts";
const useStyles = makeStyles()((theme: Theme) => ({
icon: {
fontSize: MPT_SPACING_2,
"& > svg": {
paddingTop: "2px",
width: "0.8rem",
height: "0.8rem"
}
},
labelSuccess: {
color: theme.palette.success.main
},
tagValue: {
color: theme.palette.primary.gray4
}
}));

export default useStyles;
Loading
Loading