@prorobotech/openapi-k8s-toolkit 1.2.0-alpha.7 → 1.2.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8803,6 +8803,7 @@ const useListWatch = ({
8803
8803
  }) => {
8804
8804
  const resId = `${query.apiGroup ?? ""}|${query.apiVersion}|${query.plural}|${query.namespace ?? ""}|${query.fieldSelector ?? ""}|${query.labelSelector ?? ""}`;
8805
8805
  const resIdRef = useRef(resId);
8806
+ const [debugTick, bumpTick] = useReducer((x) => x + 1, 0);
8806
8807
  const [state, dispatch] = useReducer(reducer$1, { order: [], byKey: {} });
8807
8808
  const [contToken, setContToken] = useState();
8808
8809
  const [hasMore, setHasMore] = useState(false);
@@ -9050,6 +9051,7 @@ const useListWatch = ({
9050
9051
  }
9051
9052
  if (frame.type === "INITIAL") {
9052
9053
  dispatch({ type: "RESET", items: frame.items });
9054
+ bumpTick();
9053
9055
  setContToken(frame.continue);
9054
9056
  setHasMore(Boolean(frame.continue));
9055
9057
  setErrorSafe(void 0);
@@ -9065,6 +9067,7 @@ const useListWatch = ({
9065
9067
  }
9066
9068
  if (frame.type === "PAGE") {
9067
9069
  dispatch({ type: "APPEND_PAGE", items: frame.items });
9070
+ bumpTick();
9068
9071
  setContToken(frame.continue);
9069
9072
  setHasMore(Boolean(frame.continue));
9070
9073
  fetchingRef.current = false;
@@ -9087,9 +9090,11 @@ const useListWatch = ({
9087
9090
  }
9088
9091
  if (!pausedRef.current) {
9089
9092
  if (frame.type === "ADDED" || frame.type === "MODIFIED") {
9093
+ bumpTick();
9090
9094
  dispatch({ type: "UPSERT", item: frame.item });
9091
9095
  }
9092
9096
  if (!ignoreRemoveRef.current && frame.type === "DELETED") {
9097
+ bumpTick();
9093
9098
  dispatch({ type: "REMOVE", key: eventKey$1(frame.item) });
9094
9099
  }
9095
9100
  }
@@ -9230,7 +9235,8 @@ const useListWatch = ({
9230
9235
  drainAll,
9231
9236
  reconnect,
9232
9237
  setUrl,
9233
- setQuery
9238
+ setQuery,
9239
+ debugTick
9234
9240
  };
9235
9241
  };
9236
9242
 
@@ -9321,7 +9327,7 @@ const useK8sSmartResourceWithoutKinds = ({
9321
9327
  const watchEnabled = Boolean(
9322
9328
  cluster && cluster.length > 0 && isEnabled && canList && canWatch && !verbsLoading && !verbsIsError
9323
9329
  );
9324
- const { state, status, hasInitial, lastError } = useListWatch({
9330
+ const { state, status, hasInitial, lastError, debugTick } = useListWatch({
9325
9331
  wsUrl: `/api/clusters/${cluster}/openapi-bff-ws/listThenWatch/listWatchWs`,
9326
9332
  paused: false,
9327
9333
  ignoreRemove: false,
@@ -9353,7 +9359,7 @@ const useK8sSmartResourceWithoutKinds = ({
9353
9359
  else if (used === "list" && restIsError) error = restError;
9354
9360
  const isError = Boolean(error);
9355
9361
  const data = used === "watch" ? watchData : used === "list" ? restData : void 0;
9356
- return { data, isLoading, isError, error, _meta: { used } };
9362
+ return { data, isLoading, isError, error, _meta: { used }, debugTick };
9357
9363
  };
9358
9364
 
9359
9365
  const hasItemsArray = (value) => {
@@ -9440,22 +9446,98 @@ const useManyK8sSmartResource = (paramsList) => {
9440
9446
  return results;
9441
9447
  };
9442
9448
 
9449
+ const checkIfApiInstanceNamespaceScoped = async ({
9450
+ plural,
9451
+ apiGroup,
9452
+ apiVersion,
9453
+ cluster
9454
+ }) => {
9455
+ const payload = {
9456
+ plural,
9457
+ apiGroup,
9458
+ apiVersion,
9459
+ cluster
9460
+ };
9461
+ const { data } = await axios.post(
9462
+ `/api/clusters/${cluster}/openapi-bff/scopes/checkScopes/checkIfApiNamespaceScoped`,
9463
+ payload
9464
+ );
9465
+ return data;
9466
+ };
9467
+ const checkIfBuiltInInstanceNamespaceScoped = async ({
9468
+ plural,
9469
+ cluster
9470
+ }) => {
9471
+ const payload = {
9472
+ plural,
9473
+ cluster
9474
+ };
9475
+ const { data } = await axios.post(
9476
+ `/api/clusters/${cluster}/openapi-bff/scopes/checkScopes/checkIfBuiltInNamespaceScoped`,
9477
+ payload
9478
+ );
9479
+ return data;
9480
+ };
9481
+
9443
9482
  const useSmartResourceParams = ({ cluster, namespace }) => {
9444
9483
  const [searchParams] = useSearchParams();
9445
- return useMemo(() => {
9484
+ const rawEntries = useMemo(() => {
9446
9485
  const raw = searchParams.get("resources");
9447
9486
  if (!raw) return [];
9448
9487
  return raw.split(",").map((entry) => {
9449
- const [apiGroup = "", apiVersion = "", plural] = entry.split("/");
9488
+ const [apiGroup = "", apiVersion = "", plural = ""] = entry.split("/");
9489
+ const normalizedGroup = apiGroup === "builtin" || apiGroup === "" ? void 0 : apiGroup;
9450
9490
  return {
9451
9491
  cluster,
9452
- namespace,
9453
- apiGroup: apiGroup === "builtin" ? void 0 : apiGroup,
9454
- apiVersion,
9455
- plural
9492
+ plural,
9493
+ apiGroup: normalizedGroup,
9494
+ apiVersion
9495
+ };
9496
+ }).filter((e) => Boolean(e.plural));
9497
+ }, [searchParams, cluster]);
9498
+ const scopeQueries = useQueries({
9499
+ queries: rawEntries.map((e) => {
9500
+ const isApi = Boolean(e.apiGroup);
9501
+ return {
9502
+ queryKey: ["resource-scope", e.cluster, isApi ? e.apiGroup : "builtin", e.apiVersion ?? "", e.plural],
9503
+ enabled: Boolean(e.cluster && e.plural && (!isApi || e.apiVersion)),
9504
+ queryFn: () => {
9505
+ if (isApi) {
9506
+ return checkIfApiInstanceNamespaceScoped({
9507
+ plural: e.plural,
9508
+ apiGroup: e.apiGroup,
9509
+ apiVersion: e.apiVersion || "",
9510
+ cluster: e.cluster
9511
+ });
9512
+ }
9513
+ return checkIfBuiltInInstanceNamespaceScoped({
9514
+ plural: e.plural,
9515
+ cluster: e.cluster
9516
+ });
9517
+ },
9518
+ staleTime: 5 * 60 * 1e3
9519
+ };
9520
+ })
9521
+ });
9522
+ const scopesLoading = scopeQueries.some((q) => q.isLoading);
9523
+ const scopesError = scopeQueries.find((q) => q.error)?.error;
9524
+ const paramsList = useMemo(() => {
9525
+ return rawEntries.map((e, i) => {
9526
+ const isClusterWide = scopeQueries[i]?.data?.isNamespaceScoped === false;
9527
+ return {
9528
+ cluster: e.cluster,
9529
+ plural: e.plural,
9530
+ apiGroup: e.apiGroup,
9531
+ apiVersion: e.apiVersion || "",
9532
+ namespace: isClusterWide ? void 0 : namespace
9456
9533
  };
9457
9534
  });
9458
- }, [searchParams, cluster, namespace]);
9535
+ }, [rawEntries, scopeQueries, namespace]);
9536
+ return {
9537
+ paramsList,
9538
+ scopesLoading,
9539
+ scopesError
9540
+ };
9459
9541
  };
9460
9542
 
9461
9543
  const prepareTemplate = ({
@@ -34100,7 +34182,6 @@ const normalizeCoreUnit = (u) => {
34100
34182
  const key = String(u).trim().toLowerCase();
34101
34183
  const canon = CORE_ALIASES[key];
34102
34184
  if (!canon) {
34103
- console.error(`Unknown core unit: "${u}"`);
34104
34185
  return "core";
34105
34186
  }
34106
34187
  return canon;
@@ -47072,9 +47153,9 @@ const getEnrichedColumns = ({
47072
47153
  }
47073
47154
  return columns.map((el, colIndex) => {
47074
47155
  const possibleAdditionalPrinterColumnsCustomSortersAndFiltersType = additionalPrinterColumnsCustomSortersAndFilters?.find(({ key }) => key === el.key)?.type;
47075
- const isSortersAndFitlersDisabled = possibleAdditionalPrinterColumnsCustomSortersAndFiltersType === "disabled";
47076
- const isSortersAndFitlersCPU = possibleAdditionalPrinterColumnsCustomSortersAndFiltersType === "cpu";
47077
- const isSortersAndFitlersMemory = possibleAdditionalPrinterColumnsCustomSortersAndFiltersType === "memory";
47156
+ const isSortersAndFiltersDisabled = possibleAdditionalPrinterColumnsCustomSortersAndFiltersType === "disabled";
47157
+ const isSortersAndFiltersCPU = possibleAdditionalPrinterColumnsCustomSortersAndFiltersType === "cpu";
47158
+ const isSortersAndFiltersMemory = possibleAdditionalPrinterColumnsCustomSortersAndFiltersType === "memory";
47078
47159
  const possibleUndefinedValue = additionalPrinterColumnsUndefinedValues?.find(({ key }) => key === el.key)?.value;
47079
47160
  const possibleTrimLength = additionalPrinterColumnsTrimLengths?.find(({ key }) => key === el.key)?.value;
47080
47161
  const possibleColWidth = additionalPrinterColumnsColWidths?.find(({ key }) => key === el.key)?.value;
@@ -47088,36 +47169,25 @@ const getEnrichedColumns = ({
47088
47169
  if (!cell) return "";
47089
47170
  return (cell.innerText || cell.textContent || "").trim().toLowerCase();
47090
47171
  };
47091
- const getEntry = (record) => {
47092
- const { dataIndex } = el;
47093
- return Array.isArray(dataIndex) ? lodashExports.get(record, dataIndex) : record[dataIndex];
47094
- };
47095
- const getTextForNumericUnitSort = (record) => {
47096
- if (useFactorySearch) {
47097
- const textFromDom = getCellTextFromDOM(record);
47098
- if (textFromDom) return textFromDom;
47099
- }
47100
- const raw = getEntry(record);
47101
- if (raw == null) return null;
47102
- return String(raw);
47103
- };
47104
47172
  const getMemoryInBytes = (record) => {
47105
- const text = getTextForNumericUnitSort(record);
47106
- if (!text) return NaN;
47173
+ const text = getCellTextFromDOM(record);
47174
+ if (!text) return 0;
47107
47175
  const parsed = parseValueWithUnit(text);
47108
- if (!parsed) return NaN;
47176
+ if (!parsed) return 0;
47109
47177
  if (parsed.unit) {
47110
- return toBytes(parsed.value, parsed.unit);
47178
+ const bytes = toBytes(parsed.value, parsed.unit);
47179
+ return bytes >= 0 ? bytes : 0;
47111
47180
  }
47112
47181
  return parsed.value;
47113
47182
  };
47114
47183
  const getCpuInCores = (record) => {
47115
- const text = getTextForNumericUnitSort(record);
47116
- if (!text) return NaN;
47184
+ const text = getCellTextFromDOM(record);
47185
+ if (!text) return 0;
47117
47186
  const parsed = parseCoresWithUnit(text);
47118
- if (!parsed) return NaN;
47187
+ if (!parsed) return 0;
47119
47188
  if (parsed.unit) {
47120
- return toCores(parsed.value, parsed.unit);
47189
+ const cores = toCores(parsed.value, parsed.unit);
47190
+ return cores >= 0 ? cores : 0;
47121
47191
  }
47122
47192
  return parsed.value;
47123
47193
  };
@@ -47149,7 +47219,7 @@ const getEnrichedColumns = ({
47149
47219
  };
47150
47220
  },
47151
47221
  filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => {
47152
- if (isSortersAndFitlersDisabled || isSortersAndFitlersMemory || isSortersAndFitlersCPU) {
47222
+ if (isSortersAndFiltersDisabled || isSortersAndFiltersMemory || isSortersAndFiltersCPU) {
47153
47223
  return null;
47154
47224
  }
47155
47225
  return /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -47164,13 +47234,13 @@ const getEnrichedColumns = ({
47164
47234
  );
47165
47235
  },
47166
47236
  filterIcon: (filtered) => {
47167
- if (isSortersAndFitlersDisabled || isSortersAndFitlersMemory || isSortersAndFitlersCPU) {
47237
+ if (isSortersAndFiltersDisabled || isSortersAndFiltersMemory || isSortersAndFiltersCPU) {
47168
47238
  return null;
47169
47239
  }
47170
47240
  return /* @__PURE__ */ jsxRuntimeExports.jsx(SearchOutlined, { style: { color: filtered ? "#1677ff" : void 0 } });
47171
47241
  },
47172
47242
  onFilter: (value, record) => {
47173
- if (isSortersAndFitlersDisabled || isSortersAndFitlersMemory || isSortersAndFitlersCPU) {
47243
+ if (isSortersAndFiltersDisabled || isSortersAndFiltersMemory || isSortersAndFiltersCPU) {
47174
47244
  return false;
47175
47245
  }
47176
47246
  if (useFactorySearch) {
@@ -47197,22 +47267,22 @@ const getEnrichedColumns = ({
47197
47267
  }
47198
47268
  return false;
47199
47269
  },
47200
- sorter: isSortersAndFitlersDisabled ? false : (a, b) => {
47201
- if (useFactorySearch) {
47202
- const aText = getCellTextFromDOM(a);
47203
- const bText = getCellTextFromDOM(b);
47204
- return aText.localeCompare(bText);
47205
- }
47206
- if (isSortersAndFitlersMemory) {
47270
+ sorter: isSortersAndFiltersDisabled ? false : (a, b) => {
47271
+ if (isSortersAndFiltersMemory) {
47207
47272
  const aBytes = getMemoryInBytes(a);
47208
47273
  const bBytes = getMemoryInBytes(b);
47209
47274
  return safeNumericCompare(aBytes, bBytes);
47210
47275
  }
47211
- if (isSortersAndFitlersCPU) {
47276
+ if (isSortersAndFiltersCPU) {
47212
47277
  const aCores = getCpuInCores(a);
47213
47278
  const bCores = getCpuInCores(b);
47214
47279
  return safeNumericCompare(aCores, bCores);
47215
47280
  }
47281
+ if (useFactorySearch) {
47282
+ const aText = getCellTextFromDOM(a);
47283
+ const bText = getCellTextFromDOM(b);
47284
+ return aText.localeCompare(bText);
47285
+ }
47216
47286
  const { dataIndex } = el;
47217
47287
  const aEntry = Array.isArray(dataIndex) ? lodashExports.get(a, dataIndex) : a[dataIndex];
47218
47288
  const bEntry = Array.isArray(dataIndex) ? lodashExports.get(b, dataIndex) : b[dataIndex];
@@ -54722,39 +54792,6 @@ const getSwagger = async ({ cluster }) => {
54722
54792
  return axios.get(`/api/clusters/${cluster}/openapi-bff/swagger/swagger/${cluster}`);
54723
54793
  };
54724
54794
 
54725
- const checkIfApiInstanceNamespaceScoped = async ({
54726
- plural,
54727
- apiGroup,
54728
- apiVersion,
54729
- cluster
54730
- }) => {
54731
- const payload = {
54732
- plural,
54733
- apiGroup,
54734
- apiVersion,
54735
- cluster
54736
- };
54737
- const { data } = await axios.post(
54738
- `/api/clusters/${cluster}/openapi-bff/scopes/checkScopes/checkIfApiNamespaceScoped`,
54739
- payload
54740
- );
54741
- return data;
54742
- };
54743
- const checkIfBuiltInInstanceNamespaceScoped = async ({
54744
- plural,
54745
- cluster
54746
- }) => {
54747
- const payload = {
54748
- plural,
54749
- cluster
54750
- };
54751
- const { data } = await axios.post(
54752
- `/api/clusters/${cluster}/openapi-bff/scopes/checkScopes/checkIfBuiltInNamespaceScoped`,
54753
- payload
54754
- );
54755
- return data;
54756
- };
54757
-
54758
54795
  const useClusterList = ({ refetchInterval }) => {
54759
54796
  return useQuery({
54760
54797
  queryKey: ["useClusterList"],
@@ -55010,5 +55047,26 @@ const useInfiniteSentinel = (sentinelRef, hasMore, onNeedMore) => {
55010
55047
  }, [sentinelRef, hasMore, onNeedMore]);
55011
55048
  };
55012
55049
 
55013
- export { BackToDefaultIcon, BlackholeForm, BlackholeFormProvider, ContentCard$1 as ContentCard, CursorDefaultDiv, CursorPointerTag, CursorPointerTagMinContent, CustomSelect$4 as CustomSelect, DeleteIcon, DeleteModal, DeleteModalMany, DownIcon, DynamicComponents, DynamicRenderer, DynamicRendererWithProviders, EarthIcon, EditIcon, EnrichedTable, EnrichedTableProvider, Events, FlexGrow, LockedIcon, LookingGlassIcon, ManageableBreadcrumbs, ManageableBreadcrumbsProvider, ManageableSidebar, ManageableSidebarProvider, MarketPlace, MarketplaceCard, MinusIcon, NodeTerminal, PaddingContainer, PauseCircleIcon, PlusIcon, PodLogs, PodLogsMonaco, PodTerminal, ProjectInfoCard, ResourceLink, ResumeCircleIcon, Search, Spacer$1 as Spacer, SuccessIcon, TreeWithSearch, UncontrolledSelect, UnlockedIcon, UpIcon, YamlEditorSingleton$1 as YamlEditorSingleton, checkIfApiInstanceNamespaceScoped, checkIfBuiltInInstanceNamespaceScoped, checkPermission, convertBytes, convertCompute, convertCores, convertStorage, createContextFactory, createNewEntry, deepMerge, deleteEntry, feedbackIcons, filterIfApiInstanceNamespaceScoped, filterIfBuiltInInstanceNamespaceScoped, filterSelectOptions, floorToDecimal, formatBytesAuto, formatCoresAuto, getAllPathsFromObj, getApiResourceSingle, getApiResourceTypes, getApiResourceTypesByApiGroup, getApiResources, getBuiltinResourceSingle, getBuiltinResourceTypes, getBuiltinResources, getBuiltinTreeData, getClusterList, getCrdData, getCrdResourceSingle, getCrdResources, getDirectUnknownResource, getEnrichedColumns, getEnrichedColumnsWithControls, getGroupsByCategory, getKinds, getLinkToApiForm, getLinkToBuiltinForm, getLinkToForm, getNamespaceLink, getObjectFormItemsDraft, getPrefixSubarrays, getResourceLink, getSortedKinds, getSortedKindsAll, getStringByName, getSwagger, getUppercase, groupsToTreeData, hslFromString, includesArray, isFlatObject, isMultilineFromYaml, isMultilineString, kindByGvr, namespacedByGvr, normalizeValuesForQuotasToNumber, parseCoresWithUnit, parseQuotaValue, parseQuotaValueCpu, parseQuotaValueMemoryAndStorage, parseValueWithUnit, pluralByKind, prepareDataForManageableBreadcrumbs, prepareDataForManageableSidebar, prepareTemplate, prepareUrlsToFetchForDynamicRenderer, toBytes, toCores, updateEntry, useApiResourceSingle, useApiResourceTypesByGroup, useApiResources, useApisResourceTypes, useBuiltinResourceSingle, useBuiltinResourceTypes, useBuiltinResources, useClusterList, useCrdData, useCrdResourceSingle, useCrdResources, useDirectUnknownResource, useInfiniteSentinel, useK8sSmartResource, useK8sVerbs, useListWatch, useManyK8sSmartResource, usePermissions, useSmartResourceParams };
55050
+ const useResourceScope = ({ plural, cluster, apiGroup, apiVersion }) => {
55051
+ const computedResourceType = apiGroup ? "api" : "builtin";
55052
+ const enabled = Boolean(cluster) && Boolean(plural) && (computedResourceType === "builtin" || Boolean(apiVersion));
55053
+ return useQuery({
55054
+ queryKey: ["resource-scope", computedResourceType, cluster, plural, apiGroup, apiVersion],
55055
+ enabled,
55056
+ queryFn: async () => {
55057
+ if (computedResourceType === "builtin") {
55058
+ return checkIfBuiltInInstanceNamespaceScoped({ plural, cluster });
55059
+ }
55060
+ return checkIfApiInstanceNamespaceScoped({
55061
+ plural,
55062
+ apiGroup: apiGroup || "",
55063
+ apiVersion: apiVersion || "",
55064
+ cluster
55065
+ });
55066
+ },
55067
+ staleTime: 5 * 60 * 1e3
55068
+ });
55069
+ };
55070
+
55071
+ export { BackToDefaultIcon, BlackholeForm, BlackholeFormProvider, ContentCard$1 as ContentCard, CursorDefaultDiv, CursorPointerTag, CursorPointerTagMinContent, CustomSelect$4 as CustomSelect, DeleteIcon, DeleteModal, DeleteModalMany, DownIcon, DynamicComponents, DynamicRenderer, DynamicRendererWithProviders, EarthIcon, EditIcon, EnrichedTable, EnrichedTableProvider, Events, FlexGrow, LockedIcon, LookingGlassIcon, ManageableBreadcrumbs, ManageableBreadcrumbsProvider, ManageableSidebar, ManageableSidebarProvider, MarketPlace, MarketplaceCard, MinusIcon, NodeTerminal, PaddingContainer, PauseCircleIcon, PlusIcon, PodLogs, PodLogsMonaco, PodTerminal, ProjectInfoCard, ResourceLink, ResumeCircleIcon, Search, Spacer$1 as Spacer, SuccessIcon, TreeWithSearch, UncontrolledSelect, UnlockedIcon, UpIcon, YamlEditorSingleton$1 as YamlEditorSingleton, checkIfApiInstanceNamespaceScoped, checkIfBuiltInInstanceNamespaceScoped, checkPermission, convertBytes, convertCompute, convertCores, convertStorage, createContextFactory, createNewEntry, deepMerge, deleteEntry, feedbackIcons, filterIfApiInstanceNamespaceScoped, filterIfBuiltInInstanceNamespaceScoped, filterSelectOptions, floorToDecimal, formatBytesAuto, formatCoresAuto, getAllPathsFromObj, getApiResourceSingle, getApiResourceTypes, getApiResourceTypesByApiGroup, getApiResources, getBuiltinResourceSingle, getBuiltinResourceTypes, getBuiltinResources, getBuiltinTreeData, getClusterList, getCrdData, getCrdResourceSingle, getCrdResources, getDirectUnknownResource, getEnrichedColumns, getEnrichedColumnsWithControls, getGroupsByCategory, getKinds, getLinkToApiForm, getLinkToBuiltinForm, getLinkToForm, getNamespaceLink, getObjectFormItemsDraft, getPrefixSubarrays, getResourceLink, getSortedKinds, getSortedKindsAll, getStringByName, getSwagger, getUppercase, groupsToTreeData, hslFromString, includesArray, isFlatObject, isMultilineFromYaml, isMultilineString, kindByGvr, namespacedByGvr, normalizeValuesForQuotasToNumber, parseCoresWithUnit, parseQuotaValue, parseQuotaValueCpu, parseQuotaValueMemoryAndStorage, parseValueWithUnit, pluralByKind, prepareDataForManageableBreadcrumbs, prepareDataForManageableSidebar, prepareTemplate, prepareUrlsToFetchForDynamicRenderer, toBytes, toCores, updateEntry, useApiResourceSingle, useApiResourceTypesByGroup, useApiResources, useApisResourceTypes, useBuiltinResourceSingle, useBuiltinResourceTypes, useBuiltinResources, useClusterList, useCrdData, useCrdResourceSingle, useCrdResources, useDirectUnknownResource, useInfiniteSentinel, useK8sSmartResource, useK8sVerbs, useListWatch, useManyK8sSmartResource, usePermissions, useResourceScope, useSmartResourceParams };
55014
55072
  //# sourceMappingURL=openapi-k8s-toolkit.es.js.map