diff --git a/insider/insider_worker/http_client/client.py b/insider/insider_worker/http_client/client.py
index b3e10167f..469ebf504 100644
--- a/insider/insider_worker/http_client/client.py
+++ b/insider/insider_worker/http_client/client.py
@@ -9,6 +9,9 @@ def retry_if_connection_error(exception):
if isinstance(exception, requests.HTTPError):
if exception.response.status_code in (503,):
return True
+ # retry too many requests
+ elif exception.response.status_code in (429,):
+ return True
return False
diff --git a/metroculus/metroculus_worker/processor.py b/metroculus/metroculus_worker/processor.py
index baa4e01b1..3c3fa238f 100644
--- a/metroculus/metroculus_worker/processor.py
+++ b/metroculus/metroculus_worker/processor.py
@@ -537,12 +537,14 @@ def get_gcp_metrics(cloud_account_id, cloud_resource_ids, resource_ids_map,
"cpu": "compute.googleapis.com/instance/cpu/utilization",
"network_in_io": "compute.googleapis.com/instance/network/received_bytes_count",
"network_out_io": "compute.googleapis.com/instance/network/sent_bytes_count",
+ "ram_percent": "agent.googleapis.com/memory/percent_used",
"ram_size": "compute.googleapis.com/instance/memory/balloon/ram_size",
"ram": "compute.googleapis.com/instance/memory/balloon/ram_used",
"disk_read_io": "compute.googleapis.com/instance/disk/read_ops_count",
"disk_write_io": "compute.googleapis.com/instance/disk/write_ops_count",
}
ram_sizes = {}
+ ram_percents = set()
for metric_name, cloud_metric_name in metric_cloud_names_map.items():
response = adapter.get_metric(
cloud_metric_name,
@@ -579,17 +581,28 @@ def get_gcp_metrics(cloud_account_id, cloud_resource_ids, resource_ids_map,
'disk_write_io']:
# change values per min to values per second
value = value / 60
+ # RAM value in % is returned on instances with Ops agent
+ elif metric_name == "ram_percent":
+ if record.metric.labels.get('state') != 'used':
+ continue
+ key = (resource_id, date)
+ ram_percents.add(key)
# to determine RAM value in % instead of absolute values,
- # we need to know values of 2 metrics - ram_used and ram_size - for the same time
- # so we store ram_size in a map and pop values from it later when processing ram_used.
- # we rely on the fact that the metrics API returns metrics in the same order
- # as they were requested.
+ # on instances without Ops agent, we need to know values
+ # of 2 metrics - ram_used and ram_size - for the same time
+ # so we store ram_size in a map and pop values from it
+ # later when processing ram_used.
+ # we rely on the fact that the metrics API returns metrics
+ # in the same order as they were requested.
elif metric_name == "ram_size":
key = (resource_id, date)
ram_sizes[key] = value
continue
elif metric_name == "ram":
key = (resource_id, date)
+ if key in ram_percents:
+ # not calculate ram value, as agent's value exists
+ continue
ram_size = ram_sizes.pop(key, None)
if ram_size is None:
LOG.warn(
@@ -609,7 +622,7 @@ def get_gcp_metrics(cloud_account_id, cloud_resource_ids, resource_ids_map,
'cloud_account_id': cloud_account_id,
'resource_id': resource_id,
'date': date,
- 'metric': metric_name,
+ 'metric': 'ram' if 'ram' in metric_name else metric_name,
'value': value
})
return result
diff --git a/ngui/ui/src/components/ApolloApiErrorAlert/ApolloApiErrorAlert.tsx b/ngui/ui/src/components/ApolloApiErrorAlert/ApolloApiErrorAlert.tsx
new file mode 100644
index 000000000..5d95e4a84
--- /dev/null
+++ b/ngui/ui/src/components/ApolloApiErrorAlert/ApolloApiErrorAlert.tsx
@@ -0,0 +1,43 @@
+import { useEffect, useState } from "react";
+import { useQuery } from "@apollo/client";
+import ApiErrorMessage from "components/ApiErrorMessage";
+import SnackbarAlert from "components/SnackbarAlert";
+import { GET_ERROR } from "graphql/api/common";
+
+// TODO: implement ERROR_HANDLER_TYPE_ALERT analogy for Apollo queries. https://www.apollographql.com/docs/react/v2/data/error-handling/
+const ApolloApiErrorAlert = () => {
+ const { data = {} } = useQuery(GET_ERROR);
+
+ const { error: { error_code: errorCode, reason: errorReason, url, params } = {} } = data;
+
+ const [open, setOpen] = useState(false);
+
+ useEffect(() => {
+ setOpen(!!errorCode);
+ }, [errorCode]);
+
+ const handleClose = (event, reason) => {
+ if (reason === "clickaway") {
+ return;
+ }
+ setOpen(false);
+ };
+
+ const errorMessage = errorCode && ;
+
+ return (
+ errorMessage !== null && (
+
+ )
+ );
+};
+
+export default ApolloApiErrorAlert;
diff --git a/ngui/ui/src/components/ApolloApiErrorAlert/index.ts b/ngui/ui/src/components/ApolloApiErrorAlert/index.ts
new file mode 100644
index 000000000..dc802c7e5
--- /dev/null
+++ b/ngui/ui/src/components/ApolloApiErrorAlert/index.ts
@@ -0,0 +1,3 @@
+import ApolloApiErrorAlert from "./ApolloApiErrorAlert";
+
+export default ApolloApiErrorAlert;
diff --git a/ngui/ui/src/components/ApolloProvider/ApolloProvider.tsx b/ngui/ui/src/components/ApolloProvider/ApolloProvider.tsx
index d9e54e51e..3a7fe5ce5 100644
--- a/ngui/ui/src/components/ApolloProvider/ApolloProvider.tsx
+++ b/ngui/ui/src/components/ApolloProvider/ApolloProvider.tsx
@@ -1,14 +1,26 @@
-import { ApolloClient, ApolloProvider, InMemoryCache, split, HttpLink } from "@apollo/client";
+import { ApolloClient, ApolloProvider, InMemoryCache, split, HttpLink, from, type DefaultContext } from "@apollo/client";
+import { onError, type ErrorResponse } from "@apollo/client/link/error";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { getMainDefinition } from "@apollo/client/utilities";
+import { type GraphQLError } from "graphql";
import { createClient } from "graphql-ws";
import { GET_TOKEN } from "api/auth/actionTypes";
+import { GET_ERROR } from "graphql/api/common";
import { useApiData } from "hooks/useApiData";
import { getEnvironmentVariable } from "utils/env";
const httpBase = getEnvironmentVariable("VITE_APOLLO_HTTP_BASE");
const wsBase = getEnvironmentVariable("VITE_APOLLO_WS_BASE");
+const writeErrorToCache = (cache: DefaultContext, graphQLError: GraphQLError) => {
+ const { extensions: { response: { url, body: { error } = {} } = {} } = {} } = graphQLError;
+
+ cache.writeQuery({
+ query: GET_ERROR,
+ data: { error: { __typename: "Error", ...error, url } }
+ });
+};
+
const ApolloClientProvider = ({ children }) => {
const {
apiData: { token }
@@ -27,11 +39,23 @@ const ApolloClientProvider = ({ children }) => {
})
);
- /*
- @param A function that's called for each operation to execute
- @param The Link to use for an operation if the function returns a "truthy" value
- @param The Link to use for an operation if the function returns a "falsy" value
- */
+ const errorLink = onError(({ graphQLErrors, networkError, operation }: ErrorResponse) => {
+ if (graphQLErrors) {
+ graphQLErrors.forEach(({ message, path }) => console.log(`[GraphQL error]: Message: ${message}, Path: ${path}`));
+
+ const { cache } = operation.getContext();
+ writeErrorToCache(cache, graphQLErrors[0]);
+ }
+
+ /* Just log network errors for now.
+ We rely on custom error codes that are returned in graphQLErrors.
+ It might be usefult to cache networkError errors to display alerts as well.
+ */
+ if (networkError) {
+ console.error(`[Network error]: ${networkError}`);
+ }
+ });
+
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
@@ -42,7 +66,7 @@ const ApolloClientProvider = ({ children }) => {
);
const client = new ApolloClient({
- link: splitLink,
+ link: from([errorLink, splitLink]),
cache: new InMemoryCache()
});
diff --git a/ngui/ui/src/components/EditModelVersionAliasForm/FormElements/EditModelVersionAliasFormAliasesField.tsx b/ngui/ui/src/components/EditModelVersionAliasForm/FormElements/EditModelVersionAliasFormAliasesField.tsx
index 30ba6151e..997b4ff9b 100644
--- a/ngui/ui/src/components/EditModelVersionAliasForm/FormElements/EditModelVersionAliasFormAliasesField.tsx
+++ b/ngui/ui/src/components/EditModelVersionAliasForm/FormElements/EditModelVersionAliasFormAliasesField.tsx
@@ -10,6 +10,7 @@ import SlicedText from "components/SlicedText";
import { ModelVersion } from "services/MlModelsService";
import { DEFAULT_MAX_INPUT_LENGTH } from "utils/constants";
import { SPACING_1 } from "utils/layouts";
+import { notOnlyWhiteSpaces } from "utils/validation";
export const FIELD_NAME = "aliases";
@@ -54,7 +55,13 @@ const ConflictingAliasesWarning = ({ modelVersion, aliasesFieldName, aliasToVers
));
};
-export const isAliasValid = (alias: string) => alias.length <= DEFAULT_MAX_INPUT_LENGTH;
+export const isAliasValid = (alias: string) => {
+ const isLengthValid = alias.length <= DEFAULT_MAX_INPUT_LENGTH;
+
+ const containsNotOnlyWhiteSpaces = notOnlyWhiteSpaces(alias) === true;
+
+ return isLengthValid && containsNotOnlyWhiteSpaces;
+};
const EditModelVersionAliasFormAliasesField = ({
name = FIELD_NAME,
diff --git a/ngui/ui/src/components/EditModelVersionTagsForm/FormElements/EditModelVersionTagsTagsFieldArray.tsx b/ngui/ui/src/components/EditModelVersionTagsForm/FormElements/EditModelVersionTagsTagsFieldArray.tsx
index 97b055235..8417958fe 100644
--- a/ngui/ui/src/components/EditModelVersionTagsForm/FormElements/EditModelVersionTagsTagsFieldArray.tsx
+++ b/ngui/ui/src/components/EditModelVersionTagsForm/FormElements/EditModelVersionTagsTagsFieldArray.tsx
@@ -8,6 +8,7 @@ import IconButton from "components/IconButton";
import Input from "components/Input";
import { DEFAULT_MAX_INPUT_LENGTH } from "utils/constants";
import { SPACING_1 } from "utils/layouts";
+import { notOnlyWhiteSpaces } from "utils/validation";
export const ARRAY_FIELD_NAME = "tags";
export const KEY_FIELD_NAME = "key";
@@ -52,7 +53,8 @@ const KeyInput = ({ index }: { index: number }) => {
const isPropertyUnique = tagsWithSameKey.length === 1;
return isPropertyUnique || intl.formatMessage({ id: "thisFieldShouldBeUnique" });
- }
+ },
+ notOnlyWhiteSpaces
}
})}
dataTestId={`tag_name_${index}`}
@@ -88,6 +90,9 @@ const ValueInput = ({ index }: { index: number }) => {
max: DEFAULT_MAX_INPUT_LENGTH
}
)
+ },
+ validate: {
+ notOnlyWhiteSpaces
}
}}
render={({ field }) => (
diff --git a/ngui/ui/src/components/MlModel/MlModel.tsx b/ngui/ui/src/components/MlModel/MlModel.tsx
index 76db9794e..d30e67c44 100644
--- a/ngui/ui/src/components/MlModel/MlModel.tsx
+++ b/ngui/ui/src/components/MlModel/MlModel.tsx
@@ -140,6 +140,7 @@ const Version = ({ versions = [], isLoading = false }: VersionProps) => {
),
id: "aliases",
+ enableSorting: false,
accessorFn: ({ aliases }) => aliases.join(", "),
cell: ({ row: { original } }) => {
const { aliases } = original;
@@ -201,6 +202,7 @@ const Version = ({ versions = [], isLoading = false }: VersionProps) => {
style: {
minWidth: "200px"
},
+ enableSorting: false,
accessorFn: (originalRow) =>
Object.entries(originalRow.tags ?? {})
.map(([key, val]) => `${key}: ${val}`)
diff --git a/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormDescriptionField.tsx b/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormDescriptionField.tsx
index 86cb76737..d87fa1577 100644
--- a/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormDescriptionField.tsx
+++ b/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormDescriptionField.tsx
@@ -3,6 +3,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import Input from "components/Input";
import InputLoader from "components/InputLoader";
import { DEFAULT_MAX_TEXTAREA_LENGTH } from "utils/constants";
+import { notOnlyWhiteSpaces } from "utils/validation";
export const FIELD_NAME = "description";
@@ -33,6 +34,9 @@ const MlModelFormDescriptionField = ({ name = FIELD_NAME, isLoading = false }) =
{ id: "maxLength" },
{ inputName: intl.formatMessage({ id: "description" }), max: DEFAULT_MAX_TEXTAREA_LENGTH }
)
+ },
+ validate: {
+ notOnlyWhiteSpaces
}
})}
/>
diff --git a/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormKeyField.tsx b/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormKeyField.tsx
index 7d0d05abb..c5d88df30 100644
--- a/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormKeyField.tsx
+++ b/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormKeyField.tsx
@@ -3,6 +3,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import Input from "components/Input";
import InputLoader from "components/InputLoader";
import { DEFAULT_MAX_INPUT_LENGTH } from "utils/constants";
+import { notOnlyWhiteSpaces } from "utils/validation";
export const FIELD_NAME = "key";
@@ -34,6 +35,9 @@ const MlModelFormKeyField = ({ name = FIELD_NAME, isLoading = false }) => {
{ id: "maxLength" },
{ inputName: intl.formatMessage({ id: "key" }), max: DEFAULT_MAX_INPUT_LENGTH }
)
+ },
+ validate: {
+ notOnlyWhiteSpaces
}
})}
/>
diff --git a/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormNameField.tsx b/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormNameField.tsx
index 993c0879c..87010c2ac 100644
--- a/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormNameField.tsx
+++ b/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormNameField.tsx
@@ -3,6 +3,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import Input from "components/Input";
import InputLoader from "components/InputLoader";
import { DEFAULT_MAX_INPUT_LENGTH } from "utils/constants";
+import { notOnlyWhiteSpaces } from "utils/validation";
export const FIELD_NAME = "name";
@@ -35,6 +36,9 @@ const MlModelFormNameField = ({ name = FIELD_NAME, isLoading = false }) => {
{ id: "maxLength" },
{ inputName: intl.formatMessage({ id: "name" }), max: DEFAULT_MAX_INPUT_LENGTH }
)
+ },
+ validate: {
+ notOnlyWhiteSpaces
}
})}
/>
diff --git a/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormTagsFieldArray.tsx b/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormTagsFieldArray.tsx
index e7a044724..65baf0355 100644
--- a/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormTagsFieldArray.tsx
+++ b/ngui/ui/src/components/MlModelForm/FormElements/MlModelFormTagsFieldArray.tsx
@@ -9,6 +9,7 @@ import Input from "components/Input";
import InputLoader from "components/InputLoader";
import { DEFAULT_MAX_INPUT_LENGTH } from "utils/constants";
import { SPACING_1 } from "utils/layouts";
+import { notOnlyWhiteSpaces } from "utils/validation";
export const ARRAY_FIELD_NAME = "tags";
export const KEY_FIELD_NAME = "key";
@@ -52,7 +53,8 @@ const KeyInput = ({ index }: { index: number }) => {
const isPropertyUnique = tagsWithSameKey.length === 1;
return isPropertyUnique || intl.formatMessage({ id: "thisFieldShouldBeUnique" });
- }
+ },
+ notOnlyWhiteSpaces
}
})}
dataTestId={`tag_name_${index}`}
@@ -88,6 +90,9 @@ const ValueInput = ({ index }: { index: number }) => {
max: DEFAULT_MAX_INPUT_LENGTH
}
)
+ },
+ validate: {
+ notOnlyWhiteSpaces
}
}}
render={({ field }) => (
diff --git a/ngui/ui/src/components/MlModels/MlModels.tsx b/ngui/ui/src/components/MlModels/MlModels.tsx
index 1b41e5a56..efa4d5798 100644
--- a/ngui/ui/src/components/MlModels/MlModels.tsx
+++ b/ngui/ui/src/components/MlModels/MlModels.tsx
@@ -105,8 +105,13 @@ const ModelsTable = ({ models }: ModelsTableProps) => {
),
- accessorKey: "description",
- cell: ({ cell }) => {cell.getValue()}
+ id: "description",
+ accessorFn: (originalRow) => originalRow.description ?? "",
+ cell: ({ cell }) => {
+ const description = cell.getValue();
+
+ return description ? {description} : CELL_EMPTY_VALUE;
+ }
},
tags({
id: "tags",
diff --git a/ngui/ui/src/graphql/api/common.ts b/ngui/ui/src/graphql/api/common.ts
new file mode 100644
index 000000000..05185fed9
--- /dev/null
+++ b/ngui/ui/src/graphql/api/common.ts
@@ -0,0 +1,9 @@
+import { gql } from "@apollo/client";
+
+const GET_ERROR = gql`
+ query GetError {
+ error @client
+ }
+`;
+
+export { GET_ERROR };
diff --git a/ngui/ui/src/index.tsx b/ngui/ui/src/index.tsx
index 9b30b2e48..211bfd491 100644
--- a/ngui/ui/src/index.tsx
+++ b/ngui/ui/src/index.tsx
@@ -11,6 +11,7 @@ import { PersistGate } from "redux-persist/integration/react";
import ActivityListener from "components/ActivityListener";
import ApiErrorAlert from "components/ApiErrorAlert";
import ApiSuccessAlert from "components/ApiSuccessAlert";
+import ApolloApiErrorAlert from "components/ApolloApiErrorAlert";
import ApolloProvider from "components/ApolloProvider";
import App from "components/App";
import SideModalManager from "components/SideModalManager";
@@ -42,6 +43,7 @@ root.render(
+