@backstage/plugin-techdocs 0.12.8 → 0.12.12

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.
package/dist/index.esm.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createApiRef, createRouteRef, useRouteRef, useApi, configApiRef, createPlugin, createApiFactory, discoveryApiRef, identityApiRef, createRoutableExtension, createComponentExtension } from '@backstage/core-plugin-api';
2
2
  import { ResponseError, NotFoundError } from '@backstage/errors';
3
3
  import { EventSourcePolyfill } from 'event-source-polyfill';
4
- import React, { useEffect, useState, Suspense, useReducer, useRef, useMemo, useContext, createContext, useCallback } from 'react';
4
+ import React, { useEffect, useState, useReducer, useRef, useMemo, createContext, useContext, useCallback } from 'react';
5
5
  import { makeStyles, ListItemText, ListItem, Divider, Card, CardMedia, CardContent, CardActions, Grid, TextField, InputAdornment, IconButton, CircularProgress, createStyles, Button as Button$1, Drawer, Typography, useTheme } from '@material-ui/core';
6
- import { Link, SubvalueCell, Table, EmptyState, Button, WarningPanel, CodeSnippet, PageWithHeader, Content, ContentHeader, SupportButton, ItemCardGrid, ItemCardHeader, Progress, ErrorPage, HeaderLabel, Header, Page, HeaderTabs, MissingAnnotationEmptyState } from '@backstage/core-components';
6
+ import { Link, SubvalueCell, Table, EmptyState, Button, WarningPanel, CodeSnippet, PageWithHeader, Content, ContentHeader, SupportButton, ItemCardGrid, ItemCardHeader, Progress, LogViewer, ErrorPage, HeaderLabel, Header, Page, HeaderTabs, MissingAnnotationEmptyState } from '@backstage/core-components';
7
7
  import TextTruncate from 'react-text-truncate';
8
8
  import { FilteredEntityLayout, FilterContainer, EntityListContainer } from '@backstage/plugin-catalog';
9
9
  import { favoriteEntityIcon, favoriteEntityTooltip, EntityRefLinks, getEntityRelations, formatEntityRefTitle, useEntityListProvider, useStarredEntities, CATALOG_FILTER_EXISTS, EntityListProvider, UserListPicker, EntityOwnerPicker, EntityTagPicker, EntityRefLink, catalogApiRef, useOwnUser, isOwnerOf, useEntity } from '@backstage/plugin-catalog-react';
@@ -27,12 +27,10 @@ import Close from '@material-ui/icons/Close';
27
27
  import CodeIcon from '@material-ui/icons/Code';
28
28
 
29
29
  const techdocsStorageApiRef = createApiRef({
30
- id: "plugin.techdocs.storageservice",
31
- description: "Used to make requests towards the techdocs storage"
30
+ id: "plugin.techdocs.storageservice"
32
31
  });
33
32
  const techdocsApiRef = createApiRef({
34
- id: "plugin.techdocs.service",
35
- description: "Used to make requests towards techdocs API"
33
+ id: "plugin.techdocs.service"
36
34
  });
37
35
 
38
36
  class TechDocsClient {
@@ -50,12 +48,12 @@ class TechDocsClient {
50
48
  return (_a = this.configApi.getOptionalString("techdocs.requestUrl")) != null ? _a : await this.discoveryApi.getBaseUrl("techdocs");
51
49
  }
52
50
  async getTechDocsMetadata(entityId) {
53
- const {kind, namespace, name} = entityId;
51
+ const { kind, namespace, name } = entityId;
54
52
  const apiOrigin = await this.getApiOrigin();
55
53
  const requestUrl = `${apiOrigin}/metadata/techdocs/${namespace}/${kind}/${name}`;
56
54
  const token = await this.identityApi.getIdToken();
57
55
  const request = await fetch(`${requestUrl}`, {
58
- headers: token ? {Authorization: `Bearer ${token}`} : {}
56
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
59
57
  });
60
58
  if (!request.ok) {
61
59
  throw await ResponseError.fromResponse(request);
@@ -63,12 +61,12 @@ class TechDocsClient {
63
61
  return await request.json();
64
62
  }
65
63
  async getEntityMetadata(entityId) {
66
- const {kind, namespace, name} = entityId;
64
+ const { kind, namespace, name } = entityId;
67
65
  const apiOrigin = await this.getApiOrigin();
68
66
  const requestUrl = `${apiOrigin}/metadata/entity/${namespace}/${kind}/${name}`;
69
67
  const token = await this.identityApi.getIdToken();
70
68
  const request = await fetch(`${requestUrl}`, {
71
- headers: token ? {Authorization: `Bearer ${token}`} : {}
69
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
72
70
  });
73
71
  if (!request.ok) {
74
72
  throw await ResponseError.fromResponse(request);
@@ -98,12 +96,12 @@ class TechDocsStorageClient {
98
96
  return this.configApi.getString("techdocs.builder");
99
97
  }
100
98
  async getEntityDocs(entityId, path) {
101
- const {kind, namespace, name} = entityId;
99
+ const { kind, namespace, name } = entityId;
102
100
  const storageUrl = await this.getStorageUrl();
103
101
  const url = `${storageUrl}/${namespace}/${kind}/${name}/${path}`;
104
102
  const token = await this.identityApi.getIdToken();
105
103
  const request = await fetch(`${url.endsWith("/") ? url : `${url}/`}index.html`, {
106
- headers: token ? {Authorization: `Bearer ${token}`} : {}
104
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
107
105
  });
108
106
  let errorMessage = "";
109
107
  switch (request.status) {
@@ -121,14 +119,14 @@ class TechDocsStorageClient {
121
119
  }
122
120
  async syncEntityDocs(entityId, logHandler = () => {
123
121
  }) {
124
- const {kind, namespace, name} = entityId;
122
+ const { kind, namespace, name } = entityId;
125
123
  const apiOrigin = await this.getApiOrigin();
126
124
  const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`;
127
125
  const token = await this.identityApi.getIdToken();
128
126
  return new Promise((resolve, reject) => {
129
127
  const source = new EventSourcePolyfill(url, {
130
128
  withCredentials: true,
131
- headers: token ? {Authorization: `Bearer ${token}`} : {}
129
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
132
130
  });
133
131
  source.addEventListener("log", (e) => {
134
132
  if (e.data) {
@@ -138,7 +136,7 @@ class TechDocsStorageClient {
138
136
  source.addEventListener("finish", (e) => {
139
137
  let updated = false;
140
138
  if (e.data) {
141
- ({updated} = JSON.parse(e.data));
139
+ ({ updated } = JSON.parse(e.data));
142
140
  }
143
141
  resolve(updated ? "updated" : "cached");
144
142
  });
@@ -156,9 +154,10 @@ class TechDocsStorageClient {
156
154
  });
157
155
  }
158
156
  async getBaseUrl(oldBaseUrl, entityId, path) {
159
- const {kind, namespace, name} = entityId;
157
+ const { kind, namespace, name } = entityId;
160
158
  const apiOrigin = await this.getApiOrigin();
161
- return new URL(oldBaseUrl, `${apiOrigin}/static/docs/${namespace}/${kind}/${name}/${path}`).toString();
159
+ const newBaseUrl = `${apiOrigin}/static/docs/${namespace}/${kind}/${name}/${path}`;
160
+ return new URL(oldBaseUrl, newBaseUrl.endsWith("/") ? newBaseUrl : `${newBaseUrl}/`).toString();
162
161
  }
163
162
  }
164
163
 
@@ -183,7 +182,7 @@ const DocsResultListItem = ({
183
182
  var _a;
184
183
  return /* @__PURE__ */ React.createElement(ListItemText, {
185
184
  className: classes.itemText,
186
- primaryTypographyProps: {variant: "h6"},
185
+ primaryTypographyProps: { variant: "h6" },
187
186
  primary: title ? title : `${result.title} | ${(_a = result.entityTitle) != null ? _a : result.name} docs`,
188
187
  secondary: /* @__PURE__ */ React.createElement(TextTruncate, {
189
188
  line: lineClamp,
@@ -193,10 +192,10 @@ const DocsResultListItem = ({
193
192
  })
194
193
  });
195
194
  };
196
- const LinkWrapper = ({children}) => asLink ? /* @__PURE__ */ React.createElement(Link, {
195
+ const LinkWrapper = ({ children }) => asLink ? /* @__PURE__ */ React.createElement(Link, {
197
196
  to: result.location
198
197
  }, children) : /* @__PURE__ */ React.createElement(React.Fragment, null, children);
199
- const ListItemWrapper = ({children}) => asListItem ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ListItem, {
198
+ const ListItemWrapper = ({ children }) => asListItem ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ListItem, {
200
199
  alignItems: "flex-start",
201
200
  className: classes.flexContainer
202
201
  }, children), /* @__PURE__ */ React.createElement(Divider, {
@@ -228,10 +227,10 @@ function createCopyDocsUrlAction(copyToClipboard) {
228
227
  };
229
228
  }
230
229
  function createStarEntityAction(isStarredEntity, toggleStarredEntity) {
231
- return ({entity}) => {
230
+ return ({ entity }) => {
232
231
  const isStarred = isStarredEntity(entity);
233
232
  return {
234
- cellStyle: {paddingLeft: "1em"},
233
+ cellStyle: { paddingLeft: "1em" },
235
234
  icon: () => favoriteEntityIcon(isStarred),
236
235
  tooltip: favoriteEntityTooltip(isStarred),
237
236
  onClick: () => toggleStarredEntity(entity)
@@ -265,7 +264,7 @@ function createOwnerColumn() {
265
264
  return {
266
265
  title: "Owner",
267
266
  field: "resolved.ownedByRelationsTitle",
268
- render: ({resolved}) => /* @__PURE__ */ React.createElement(EntityRefLinks, {
267
+ render: ({ resolved }) => /* @__PURE__ */ React.createElement(EntityRefLinks, {
269
268
  entityRefs: resolved.ownedByRelations,
270
269
  defaultKind: "group"
271
270
  })
@@ -285,6 +284,10 @@ var columnFactories = /*#__PURE__*/Object.freeze({
285
284
  createTypeColumn: createTypeColumn
286
285
  });
287
286
 
287
+ function toLowerMaybe(str, config) {
288
+ return config.getOptionalBoolean("techdocs.legacyUseCaseSensitiveTripletPaths") ? str : str.toLocaleLowerCase("en-US");
289
+ }
290
+
288
291
  const DocsTable$1 = ({
289
292
  entities,
290
293
  title,
@@ -294,7 +297,7 @@ const DocsTable$1 = ({
294
297
  }) => {
295
298
  const [, copyToClipboard] = useCopyToClipboard();
296
299
  const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef);
297
- const toLowerMaybe = useApi(configApiRef).getOptionalBoolean("techdocs.legacyUseCaseSensitiveTripletPaths") ? (str) => str : (str) => str.toLocaleLowerCase("en-US");
300
+ const config = useApi(configApiRef);
298
301
  if (!entities)
299
302
  return null;
300
303
  const documents = entities.map((entity) => {
@@ -304,12 +307,12 @@ const DocsTable$1 = ({
304
307
  entity,
305
308
  resolved: {
306
309
  docsUrl: getRouteToReaderPageFor({
307
- namespace: toLowerMaybe((_a = entity.metadata.namespace) != null ? _a : "default"),
308
- kind: toLowerMaybe(entity.kind),
309
- name: toLowerMaybe(entity.metadata.name)
310
+ namespace: toLowerMaybe((_a = entity.metadata.namespace) != null ? _a : "default", config),
311
+ kind: toLowerMaybe(entity.kind, config),
312
+ name: toLowerMaybe(entity.metadata.name, config)
310
313
  }),
311
314
  ownedByRelations,
312
- ownedByRelationsTitle: ownedByRelations.map((r) => formatEntityRefTitle(r, {defaultKind: "group"})).join(", ")
315
+ ownedByRelationsTitle: ownedByRelations.map((r) => formatEntityRefTitle(r, { defaultKind: "group" })).join(", ")
313
316
  }
314
317
  };
315
318
  });
@@ -355,8 +358,8 @@ const EntityListDocsTable = ({
355
358
  actions
356
359
  }) => {
357
360
  var _a, _b;
358
- const {loading, error, entities, filters} = useEntityListProvider();
359
- const {isStarredEntity, toggleStarredEntity} = useStarredEntities();
361
+ const { loading, error, entities, filters } = useEntityListProvider();
362
+ const { isStarredEntity, toggleStarredEntity } = useStarredEntities();
360
363
  const [, copyToClipboard] = useCopyToClipboard();
361
364
  const title = capitalize((_b = (_a = filters.user) == null ? void 0 : _a.value) != null ? _b : "all");
362
365
  const defaultActions = [
@@ -383,7 +386,7 @@ const EntityListDocsTable = ({
383
386
  EntityListDocsTable.columns = columnFactories;
384
387
  EntityListDocsTable.actions = actionFactories;
385
388
 
386
- const TechDocsPageWrapper = ({children}) => {
389
+ const TechDocsPageWrapper = ({ children }) => {
387
390
  var _a;
388
391
  const configApi = useApi(configApiRef);
389
392
  const generatedSubtitle = `Documentation available in ${(_a = configApi.getOptionalString("organization.name")) != null ? _a : "Backstage"}`;
@@ -402,7 +405,7 @@ class TechDocsFilter {
402
405
  }
403
406
  }
404
407
  const TechDocsPicker = () => {
405
- const {updateFilters} = useEntityListProvider();
408
+ const { updateFilters } = useEntityListProvider();
406
409
  useEffect(() => {
407
410
  updateFilters({
408
411
  techdocs: new TechDocsFilter()
@@ -430,7 +433,7 @@ const DocsCardGrid$1 = ({
430
433
  entities
431
434
  }) => {
432
435
  const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef);
433
- const toLowerMaybe = useApi(configApiRef).getOptionalBoolean("techdocs.legacyUseCaseSensitiveTripletPaths") ? (str) => str : (str) => str.toLocaleLowerCase("en-US");
436
+ const config = useApi(configApiRef);
434
437
  if (!entities)
435
438
  return null;
436
439
  return /* @__PURE__ */ React.createElement(ItemCardGrid, {
@@ -443,9 +446,9 @@ const DocsCardGrid$1 = ({
443
446
  title: (_a = entity.metadata.title) != null ? _a : entity.metadata.name
444
447
  })), /* @__PURE__ */ React.createElement(CardContent, null, entity.metadata.description), /* @__PURE__ */ React.createElement(CardActions, null, /* @__PURE__ */ React.createElement(Button, {
445
448
  to: getRouteToReaderPageFor({
446
- namespace: toLowerMaybe((_b = entity.metadata.namespace) != null ? _b : "default"),
447
- kind: toLowerMaybe(entity.kind),
448
- name: toLowerMaybe(entity.metadata.name)
449
+ namespace: toLowerMaybe((_b = entity.metadata.namespace) != null ? _b : "default", config),
450
+ kind: toLowerMaybe(entity.kind, config),
451
+ name: toLowerMaybe(entity.metadata.name, config)
449
452
  }),
450
453
  color: "primary",
451
454
  "data-testid": "read_docs"
@@ -459,7 +462,7 @@ var DocsCardGrid$2 = /*#__PURE__*/Object.freeze({
459
462
  });
460
463
 
461
464
  const EntityListDocsGrid = () => {
462
- const {loading, error, entities} = useEntityListProvider();
465
+ const { loading, error, entities } = useEntityListProvider();
463
466
  if (error) {
464
467
  return /* @__PURE__ */ React.createElement(WarningPanel, {
465
468
  severity: "error",
@@ -491,7 +494,7 @@ const techdocsPlugin = createPlugin({
491
494
  discoveryApi: discoveryApiRef,
492
495
  identityApi: identityApiRef
493
496
  },
494
- factory: ({configApi, discoveryApi, identityApi}) => new TechDocsStorageClient({
497
+ factory: ({ configApi, discoveryApi, identityApi }) => new TechDocsStorageClient({
495
498
  configApi,
496
499
  discoveryApi,
497
500
  identityApi
@@ -504,7 +507,7 @@ const techdocsPlugin = createPlugin({
504
507
  discoveryApi: discoveryApiRef,
505
508
  identityApi: identityApiRef
506
509
  },
507
- factory: ({configApi, discoveryApi, identityApi}) => new TechDocsClient({
510
+ factory: ({ configApi, discoveryApi, identityApi }) => new TechDocsClient({
508
511
  configApi,
509
512
  discoveryApi,
510
513
  identityApi
@@ -577,7 +580,7 @@ const addBaseUrl = ({
577
580
  const newValue = await techdocsStorageApi.getBaseUrl(elemAttribute, entityId, path);
578
581
  if (isSvgNeedingInlining(attributeName, elemAttribute, apiOrigin)) {
579
582
  try {
580
- const svg = await fetch(newValue, {credentials: "include"});
583
+ const svg = await fetch(newValue, { credentials: "include" });
581
584
  const svgContent = await svg.text();
582
585
  elem.setAttribute(attributeName, `data:image/svg+xml;base64,${btoa(svgContent)}`);
583
586
  } catch (e) {
@@ -748,11 +751,37 @@ const safeLinksHook = (node) => {
748
751
  }
749
752
  return node;
750
753
  };
751
- const sanitizeDOM = () => {
754
+ const filterIframeHook = (allowedIframeHosts) => (node) => {
755
+ if (node.nodeName === "IFRAME") {
756
+ const src = node.getAttribute("src");
757
+ if (!src) {
758
+ node.remove();
759
+ return node;
760
+ }
761
+ try {
762
+ const srcUrl = new URL(src);
763
+ const isMatch = allowedIframeHosts.some((host) => srcUrl.host === host);
764
+ if (!isMatch) {
765
+ node.remove();
766
+ }
767
+ } catch (error) {
768
+ console.warn(`Invalid iframe src, ${error}`);
769
+ node.remove();
770
+ }
771
+ }
772
+ return node;
773
+ };
774
+ const sanitizeDOM = (config) => {
775
+ const allowedIframeHosts = (config == null ? void 0 : config.getOptionalStringArray("allowedIframeHosts")) || [];
752
776
  return (dom) => {
753
777
  DOMPurify.addHook("afterSanitizeAttributes", safeLinksHook);
778
+ const addTags = ["link"];
779
+ if (allowedIframeHosts.length > 0) {
780
+ DOMPurify.addHook("beforeSanitizeElements", filterIframeHook(allowedIframeHosts));
781
+ addTags.push("iframe");
782
+ }
754
783
  return DOMPurify.sanitize(dom.innerHTML, {
755
- ADD_TAGS: ["link"],
784
+ ADD_TAGS: addTags,
756
785
  FORBID_TAGS: ["style"],
757
786
  WHOLE_DOCUMENT: true,
758
787
  RETURN_DOM: true
@@ -760,7 +789,7 @@ const sanitizeDOM = () => {
760
789
  };
761
790
  };
762
791
 
763
- const injectCss = ({css}) => {
792
+ const injectCss = ({ css }) => {
764
793
  return (dom) => {
765
794
  dom.getElementsByTagName("head")[0].insertAdjacentHTML("beforeend", `<style>${css}</style>`);
766
795
  return dom;
@@ -804,7 +833,7 @@ const TechDocsSearchBar = ({
804
833
  const {
805
834
  term,
806
835
  setTerm,
807
- result: {loading, value: searchVal}
836
+ result: { loading, value: searchVal }
808
837
  } = useSearch();
809
838
  const [options, setOptions] = useState([]);
810
839
  useEffect(() => {
@@ -827,7 +856,7 @@ const TechDocsSearchBar = ({
827
856
  };
828
857
  const handleSelection = (_, selection) => {
829
858
  if (selection == null ? void 0 : selection.document) {
830
- const {location} = selection.document;
859
+ const { location } = selection.document;
831
860
  navigate(location);
832
861
  }
833
862
  };
@@ -853,7 +882,7 @@ const TechDocsSearchBar = ({
853
882
  noOptionsText: "No results found",
854
883
  value: null,
855
884
  options,
856
- renderOption: ({document}) => /* @__PURE__ */ React.createElement(DocsResultListItem, {
885
+ renderOption: ({ document }) => /* @__PURE__ */ React.createElement(DocsResultListItem, {
857
886
  result: document,
858
887
  lineClamp: 3,
859
888
  asListItem: false,
@@ -899,7 +928,6 @@ const TechDocsSearch = (props) => {
899
928
  }));
900
929
  };
901
930
 
902
- const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
903
931
  const useDrawerStyles = makeStyles((theme) => createStyles({
904
932
  paper: {
905
933
  width: "100%",
@@ -914,6 +942,9 @@ const useDrawerStyles = makeStyles((theme) => createStyles({
914
942
  root: {
915
943
  height: "100%",
916
944
  overflow: "hidden"
945
+ },
946
+ logs: {
947
+ background: theme.palette.background.default
917
948
  }
918
949
  }));
919
950
  const TechDocsBuildLogsDrawerContent = ({
@@ -921,6 +952,7 @@ const TechDocsBuildLogsDrawerContent = ({
921
952
  onClose
922
953
  }) => {
923
954
  const classes = useDrawerStyles();
955
+ const logText = buildLog.length === 0 ? "Waiting for logs..." : buildLog.join("\n");
924
956
  return /* @__PURE__ */ React.createElement(Grid, {
925
957
  container: true,
926
958
  direction: "column",
@@ -941,24 +973,19 @@ const TechDocsBuildLogsDrawerContent = ({
941
973
  title: "Close the drawer",
942
974
  onClick: onClose,
943
975
  color: "inherit"
944
- }, /* @__PURE__ */ React.createElement(Close, null))), /* @__PURE__ */ React.createElement(Suspense, {
945
- fallback: /* @__PURE__ */ React.createElement(Progress, null)
946
- }, /* @__PURE__ */ React.createElement(LazyLog, {
947
- text: buildLog.length === 0 ? "Waiting for logs..." : buildLog.join("\n"),
948
- extraLines: 1,
949
- follow: true,
950
- selectableLines: true,
951
- enableSearch: true
952
- })));
976
+ }, /* @__PURE__ */ React.createElement(Close, null))), /* @__PURE__ */ React.createElement(LogViewer, {
977
+ text: logText,
978
+ classes: { root: classes.logs }
979
+ }));
953
980
  };
954
- const TechDocsBuildLogs = ({buildLog}) => {
981
+ const TechDocsBuildLogs = ({ buildLog }) => {
955
982
  const classes = useDrawerStyles();
956
983
  const [open, setOpen] = useState(false);
957
984
  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Button$1, {
958
985
  color: "inherit",
959
986
  onClick: () => setOpen(true)
960
987
  }, "Show Build Logs"), /* @__PURE__ */ React.createElement(Drawer, {
961
- classes: {paper: classes.paper},
988
+ classes: { paper: classes.paper },
962
989
  anchor: "right",
963
990
  open,
964
991
  onClose: () => setOpen(false)
@@ -968,7 +995,7 @@ const TechDocsBuildLogs = ({buildLog}) => {
968
995
  })));
969
996
  };
970
997
 
971
- const TechDocsNotFound = ({errorMessage}) => {
998
+ const TechDocsNotFound = ({ errorMessage }) => {
972
999
  const techdocsBuilder = useApi(configApiRef).getOptionalString("techdocs.builder");
973
1000
  let additionalInfo = "";
974
1001
  if (techdocsBuilder !== "local") {
@@ -1039,7 +1066,7 @@ const TechDocsStateIndicator = () => {
1039
1066
  action: /* @__PURE__ */ React.createElement(TechDocsBuildLogs, {
1040
1067
  buildLog
1041
1068
  }),
1042
- classes: {message: classes.message}
1069
+ classes: { message: classes.message }
1043
1070
  }, "Building a newer version of this documentation failed.", " ", syncErrorMessage);
1044
1071
  }
1045
1072
  if (state === "CONTENT_NOT_FOUND") {
@@ -1049,7 +1076,7 @@ const TechDocsStateIndicator = () => {
1049
1076
  action: /* @__PURE__ */ React.createElement(TechDocsBuildLogs, {
1050
1077
  buildLog
1051
1078
  }),
1052
- classes: {message: classes.message}
1079
+ classes: { message: classes.message }
1053
1080
  }, "Building a newer version of this documentation failed.", " ", syncErrorMessage), /* @__PURE__ */ React.createElement(TechDocsNotFound, {
1054
1081
  errorMessage: contentErrorMessage
1055
1082
  }));
@@ -1089,7 +1116,7 @@ function calculateDisplayState({
1089
1116
  return "CONTENT_FRESH";
1090
1117
  }
1091
1118
  function reducer(oldState, action) {
1092
- const newState = {...oldState};
1119
+ const newState = { ...oldState };
1093
1120
  switch (action.type) {
1094
1121
  case "sync":
1095
1122
  if (action.state === "CHECKING") {
@@ -1131,14 +1158,14 @@ function useReaderState(kind, namespace, name, path) {
1131
1158
  buildLog: []
1132
1159
  });
1133
1160
  const techdocsStorageApi = useApi(techdocsStorageApiRef);
1134
- const {retry: contentReload} = useAsyncRetry(async () => {
1135
- dispatch({type: "contentLoading"});
1161
+ const { retry: contentReload } = useAsyncRetry(async () => {
1162
+ dispatch({ type: "contentLoading" });
1136
1163
  try {
1137
- const entityDocs = await techdocsStorageApi.getEntityDocs({kind, namespace, name}, path);
1138
- dispatch({type: "content", content: entityDocs, path});
1164
+ const entityDocs = await techdocsStorageApi.getEntityDocs({ kind, namespace, name }, path);
1165
+ dispatch({ type: "content", content: entityDocs, path });
1139
1166
  return entityDocs;
1140
1167
  } catch (e) {
1141
- dispatch({type: "content", contentError: e, path});
1168
+ dispatch({ type: "content", contentError: e, path });
1142
1169
  }
1143
1170
  return void 0;
1144
1171
  }, [techdocsStorageApi, kind, namespace, name, path]);
@@ -1147,11 +1174,11 @@ function useReaderState(kind, namespace, name, path) {
1147
1174
  reload: () => {
1148
1175
  }
1149
1176
  });
1150
- contentRef.current = {content: state.content, reload: contentReload};
1177
+ contentRef.current = { content: state.content, reload: contentReload };
1151
1178
  useAsync(async () => {
1152
- dispatch({type: "sync", state: "CHECKING"});
1179
+ dispatch({ type: "sync", state: "CHECKING" });
1153
1180
  const buildingTimeout = setTimeout(() => {
1154
- dispatch({type: "sync", state: "BUILDING"});
1181
+ dispatch({ type: "sync", state: "BUILDING" });
1155
1182
  }, 1e3);
1156
1183
  try {
1157
1184
  const result = await techdocsStorageApi.syncEntityDocs({
@@ -1159,19 +1186,19 @@ function useReaderState(kind, namespace, name, path) {
1159
1186
  namespace,
1160
1187
  name
1161
1188
  }, (log) => {
1162
- dispatch({type: "buildLog", log});
1189
+ dispatch({ type: "buildLog", log });
1163
1190
  });
1164
1191
  switch (result) {
1165
1192
  case "updated":
1166
1193
  if (!contentRef.current.content) {
1167
1194
  contentRef.current.reload();
1168
- dispatch({type: "sync", state: "BUILD_READY_RELOAD"});
1195
+ dispatch({ type: "sync", state: "BUILD_READY_RELOAD" });
1169
1196
  } else {
1170
- dispatch({type: "sync", state: "BUILD_READY"});
1197
+ dispatch({ type: "sync", state: "BUILD_READY" });
1171
1198
  }
1172
1199
  break;
1173
1200
  case "cached":
1174
- dispatch({type: "sync", state: "UP_TO_DATE"});
1201
+ dispatch({ type: "sync", state: "UP_TO_DATE" });
1175
1202
  break;
1176
1203
  default:
1177
1204
  dispatch({
@@ -1182,7 +1209,7 @@ function useReaderState(kind, namespace, name, path) {
1182
1209
  break;
1183
1210
  }
1184
1211
  } catch (e) {
1185
- dispatch({type: "sync", state: "ERROR", syncError: e});
1212
+ dispatch({ type: "sync", state: "ERROR", syncError: e });
1186
1213
  } finally {
1187
1214
  clearTimeout(buildingTimeout);
1188
1215
  }
@@ -1219,8 +1246,8 @@ const TechDocsReaderProvider = ({
1219
1246
  children,
1220
1247
  entityRef
1221
1248
  }) => {
1222
- const {"*": path} = useParams();
1223
- const {kind, namespace, name} = entityRef;
1249
+ const { "*": path } = useParams();
1250
+ const { kind, namespace, name } = entityRef;
1224
1251
  const value = useReaderState(kind, namespace, name, path);
1225
1252
  return /* @__PURE__ */ React.createElement(TechDocsReaderContext.Provider, {
1226
1253
  value
@@ -1237,8 +1264,9 @@ const useTechDocsReaderDom = (entityRef) => {
1237
1264
  const theme = useTheme();
1238
1265
  const techdocsStorageApi = useApi(techdocsStorageApiRef);
1239
1266
  const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
1240
- const {namespace = "", kind = "", name = ""} = entityRef;
1241
- const {state, path, content: rawPage} = useTechDocsReader();
1267
+ const techdocsSanitizer = useApi(configApiRef);
1268
+ const { namespace = "", kind = "", name = "" } = entityRef;
1269
+ const { state, path, content: rawPage } = useTechDocsReader();
1242
1270
  const [sidebars, setSidebars] = useState();
1243
1271
  const [dom, setDom] = useState(null);
1244
1272
  const updateSidebarPosition = useCallback(() => {
@@ -1260,7 +1288,7 @@ const useTechDocsReaderDom = (entityRef) => {
1260
1288
  };
1261
1289
  }, [updateSidebarPosition, state]);
1262
1290
  const preRender = useCallback((rawContent, contentPath) => transform(rawContent, [
1263
- sanitizeDOM(),
1291
+ sanitizeDOM(techdocsSanitizer.getOptionalConfig("techdocs.sanitizer")),
1264
1292
  addBaseUrl({
1265
1293
  techdocsStorageApi,
1266
1294
  entityId: {
@@ -1395,6 +1423,7 @@ const useTechDocsReaderDom = (entityRef) => {
1395
1423
  namespace,
1396
1424
  scmIntegrationsApi,
1397
1425
  techdocsStorageApi,
1426
+ techdocsSanitizer,
1398
1427
  theme.palette.action.disabledBackground,
1399
1428
  theme.palette.background.default,
1400
1429
  theme.palette.background.paper,
@@ -1444,7 +1473,7 @@ const useTechDocsReaderDom = (entityRef) => {
1444
1473
  if (!shouldReplaceContent) {
1445
1474
  return;
1446
1475
  }
1447
- window.scroll({top: 0});
1476
+ window.scroll({ top: 0 });
1448
1477
  const postTransformedDomElement = await postRender(preTransformedDomElement);
1449
1478
  setDom(postTransformedDomElement);
1450
1479
  });
@@ -1472,7 +1501,7 @@ const TheReader = ({
1472
1501
  if (!dom || !shadowDomRef.current)
1473
1502
  return;
1474
1503
  const shadowDiv = shadowDomRef.current;
1475
- const shadowRoot = shadowDiv.shadowRoot || shadowDiv.attachShadow({mode: "open"});
1504
+ const shadowRoot = shadowDiv.shadowRoot || shadowDiv.attachShadow({ mode: "open" });
1476
1505
  Array.from(shadowRoot.children).forEach((child) => shadowRoot.removeChild(child));
1477
1506
  shadowRoot.appendChild(dom);
1478
1507
  onReadyRef.current();
@@ -1505,9 +1534,9 @@ const TechDocsPageHeader = ({
1505
1534
  entityMetadata,
1506
1535
  techDocsMetadata
1507
1536
  }) => {
1508
- const {name} = entityRef;
1509
- const {site_name: siteName, site_description: siteDescription} = techDocsMetadata || {};
1510
- const {locationMetadata, spec} = entityMetadata || {};
1537
+ const { name } = entityRef;
1538
+ const { site_name: siteName, site_description: siteDescription } = techDocsMetadata || {};
1539
+ const { locationMetadata, spec } = entityMetadata || {};
1511
1540
  const lifecycle = spec == null ? void 0 : spec.lifecycle;
1512
1541
  const ownedByRelations = entityMetadata ? getEntityRelations(entityMetadata, RELATION_OWNED_BY) : [];
1513
1542
  const docsRootLink = useRouteRef(rootRouteRef)();
@@ -1535,7 +1564,7 @@ const TechDocsPageHeader = ({
1535
1564
  target: "_blank",
1536
1565
  rel: "noopener noreferrer"
1537
1566
  }, /* @__PURE__ */ React.createElement(CodeIcon, {
1538
- style: {marginTop: "-25px", fill: "#fff"}
1567
+ style: { marginTop: "-25px", fill: "#fff" }
1539
1568
  }))
1540
1569
  }) : null);
1541
1570
  return /* @__PURE__ */ React.createElement(Header, {
@@ -1549,16 +1578,16 @@ const TechDocsPageHeader = ({
1549
1578
 
1550
1579
  const LegacyTechDocsPage = () => {
1551
1580
  const [documentReady, setDocumentReady] = useState(false);
1552
- const {namespace, kind, name} = useParams();
1581
+ const { namespace, kind, name } = useParams();
1553
1582
  const techdocsApi = useApi(techdocsApiRef);
1554
- const {value: techdocsMetadataValue} = useAsync(() => {
1583
+ const { value: techdocsMetadataValue } = useAsync(() => {
1555
1584
  if (documentReady) {
1556
- return techdocsApi.getTechDocsMetadata({kind, namespace, name});
1585
+ return techdocsApi.getTechDocsMetadata({ kind, namespace, name });
1557
1586
  }
1558
1587
  return Promise.resolve(void 0);
1559
1588
  }, [kind, namespace, name, techdocsApi, documentReady]);
1560
- const {value: entityMetadataValue, error: entityMetadataError} = useAsync(() => {
1561
- return techdocsApi.getEntityMetadata({kind, namespace, name});
1589
+ const { value: entityMetadataValue, error: entityMetadataError } = useAsync(() => {
1590
+ return techdocsApi.getEntityMetadata({ kind, namespace, name });
1562
1591
  }, [kind, namespace, name, techdocsApi]);
1563
1592
  const onReady = useCallback(() => {
1564
1593
  setDocumentReady(true);
@@ -1590,19 +1619,19 @@ const LegacyTechDocsPage = () => {
1590
1619
  })));
1591
1620
  };
1592
1621
 
1593
- const TechDocsPage = ({children}) => {
1622
+ const TechDocsPage = ({ children }) => {
1594
1623
  const outlet = useOutlet();
1595
1624
  const [documentReady, setDocumentReady] = useState(false);
1596
- const {namespace, kind, name} = useParams();
1625
+ const { namespace, kind, name } = useParams();
1597
1626
  const techdocsApi = useApi(techdocsApiRef);
1598
- const {value: techdocsMetadataValue} = useAsync(() => {
1627
+ const { value: techdocsMetadataValue } = useAsync(() => {
1599
1628
  if (documentReady) {
1600
- return techdocsApi.getTechDocsMetadata({kind, namespace, name});
1629
+ return techdocsApi.getTechDocsMetadata({ kind, namespace, name });
1601
1630
  }
1602
1631
  return Promise.resolve(void 0);
1603
1632
  }, [kind, namespace, name, techdocsApi, documentReady]);
1604
- const {value: entityMetadataValue, error: entityMetadataError} = useAsync(() => {
1605
- return techdocsApi.getEntityMetadata({kind, namespace, name});
1633
+ const { value: entityMetadataValue, error: entityMetadataError } = useAsync(() => {
1634
+ return techdocsApi.getEntityMetadata({ kind, namespace, name });
1606
1635
  }, [kind, namespace, name, techdocsApi]);
1607
1636
  const onReady = useCallback(() => {
1608
1637
  setDocumentReady(true);
@@ -1619,7 +1648,7 @@ const TechDocsPage = ({children}) => {
1619
1648
  }, children instanceof Function ? children({
1620
1649
  techdocsMetadataValue,
1621
1650
  entityMetadataValue,
1622
- entityRef: {kind, namespace, name},
1651
+ entityRef: { kind, namespace, name },
1623
1652
  onReady
1624
1653
  }) : children);
1625
1654
  };
@@ -1645,7 +1674,7 @@ const CustomPanel = ({
1645
1674
  }
1646
1675
  });
1647
1676
  const classes = useStyles();
1648
- const {value: user} = useOwnUser();
1677
+ const { value: user } = useOwnUser();
1649
1678
  const Panel = panels[config.panelType];
1650
1679
  const shownEntities = entities.filter((entity) => {
1651
1680
  if (config.filterPredicate === "ownedByUser") {
@@ -1710,7 +1739,7 @@ const TechDocsCustomHome = ({
1710
1739
  return /* @__PURE__ */ React.createElement(TechDocsPageWrapper, null, /* @__PURE__ */ React.createElement(HeaderTabs, {
1711
1740
  selectedIndex: selectedTab,
1712
1741
  onChange: (index) => setSelectedTab(index),
1713
- tabs: tabsConfig.map(({label}, index) => ({
1742
+ tabs: tabsConfig.map(({ label }, index) => ({
1714
1743
  id: index.toString(),
1715
1744
  label
1716
1745
  }))
@@ -1769,14 +1798,15 @@ var TechDocsIndexPage$1 = /*#__PURE__*/Object.freeze({
1769
1798
  TechDocsIndexPage: TechDocsIndexPage
1770
1799
  });
1771
1800
 
1772
- const EntityPageDocs = ({entity}) => {
1801
+ const EntityPageDocs = ({ entity }) => {
1773
1802
  var _a;
1803
+ const config = useApi(configApiRef);
1774
1804
  return /* @__PURE__ */ React.createElement(Reader, {
1775
1805
  withSearch: false,
1776
1806
  entityRef: {
1777
- kind: entity.kind,
1778
- namespace: (_a = entity.metadata.namespace) != null ? _a : "default",
1779
- name: entity.metadata.name
1807
+ namespace: toLowerMaybe((_a = entity.metadata.namespace) != null ? _a : "default", config),
1808
+ kind: toLowerMaybe(entity.kind, config),
1809
+ name: toLowerMaybe(entity.metadata.name, config)
1780
1810
  }
1781
1811
  });
1782
1812
  };
@@ -1797,7 +1827,7 @@ const Router = () => {
1797
1827
  };
1798
1828
  const EmbeddedDocsRouter = (_props) => {
1799
1829
  var _a;
1800
- const {entity} = useEntity();
1830
+ const { entity } = useEntity();
1801
1831
  const projectId = (_a = entity.metadata.annotations) == null ? void 0 : _a[TECHDOCS_ANNOTATION];
1802
1832
  if (!projectId) {
1803
1833
  return /* @__PURE__ */ React.createElement(MissingAnnotationEmptyState, {