@cccsaurora/howler-ui 2.18.0-dev.841 → 2.19.0-cases.1040

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.
Files changed (171) hide show
  1. package/api/action/index.d.ts +5 -4
  2. package/api/action/index.js +8 -8
  3. package/api/analytic/index.d.ts +3 -2
  4. package/api/analytic/index.js +4 -4
  5. package/api/analytic/owner.d.ts +2 -1
  6. package/api/analytic/owner.js +2 -2
  7. package/api/analytic/rules.d.ts +2 -1
  8. package/api/analytic/rules.js +2 -2
  9. package/api/dossier/index.d.ts +4 -3
  10. package/api/dossier/index.js +6 -6
  11. package/api/hit/index.d.ts +3 -2
  12. package/api/hit/index.js +4 -4
  13. package/api/hit/labels.d.ts +3 -2
  14. package/api/hit/labels.js +4 -4
  15. package/api/hit/overwrite.d.ts +2 -1
  16. package/api/hit/overwrite.js +2 -2
  17. package/api/hit/transition.d.ts +2 -1
  18. package/api/hit/transition.js +2 -2
  19. package/api/index.d.ts +5 -4
  20. package/api/index.js +8 -8
  21. package/api/overview/index.d.ts +4 -3
  22. package/api/overview/index.js +6 -6
  23. package/api/search/overview.d.ts +2 -2
  24. package/api/template/index.d.ts +4 -3
  25. package/api/template/index.js +6 -6
  26. package/api/user/index.d.ts +2 -1
  27. package/api/user/index.js +2 -2
  28. package/api/view/index.d.ts +4 -3
  29. package/api/view/index.js +6 -6
  30. package/components/app/App.d.ts +1 -0
  31. package/components/app/App.js +24 -6
  32. package/components/app/providers/FavouritesProvider.js +0 -4
  33. package/components/app/providers/ModalProvider.d.ts +1 -0
  34. package/components/app/providers/ModalProvider.js +5 -1
  35. package/components/app/providers/RecordProvider.d.ts +1 -1
  36. package/components/app/providers/RecordProvider.js +1 -1
  37. package/components/app/providers/SearchResponseProvider.d.ts +21 -0
  38. package/components/app/providers/SearchResponseProvider.js +81 -0
  39. package/components/app/providers/SearchResponseProvider.test.d.ts +1 -0
  40. package/components/app/providers/SearchResponseProvider.test.js +406 -0
  41. package/components/app/providers/ViewProvider.test.js +4 -1
  42. package/components/elements/addons/buttons/CustomButton.test.d.ts +1 -0
  43. package/components/elements/addons/buttons/CustomButton.test.js +106 -0
  44. package/components/elements/addons/layout/FlexOne.test.d.ts +1 -0
  45. package/components/elements/addons/layout/FlexOne.test.js +30 -0
  46. package/components/elements/addons/lists/TuiListBase.js +4 -0
  47. package/components/elements/addons/lists/TuiListElement.js +6 -3
  48. package/components/elements/addons/lists/index.d.ts +1 -0
  49. package/components/elements/addons/search/SearchPagination.d.ts +2 -1
  50. package/components/elements/addons/search/SearchPagination.js +4 -4
  51. package/components/elements/addons/search/SearchPagination.test.d.ts +1 -0
  52. package/components/elements/addons/search/SearchPagination.test.js +80 -0
  53. package/components/elements/addons/search/SearchTotal.test.d.ts +1 -0
  54. package/components/elements/addons/search/SearchTotal.test.js +47 -0
  55. package/components/elements/display/DocumentationButton.test.d.ts +1 -0
  56. package/components/elements/display/DocumentationButton.test.js +84 -0
  57. package/components/elements/display/DynamicTabs.test.d.ts +1 -0
  58. package/components/elements/display/DynamicTabs.test.js +93 -0
  59. package/components/elements/display/HowlerAvatar.js +2 -1
  60. package/components/elements/display/HowlerCard.test.d.ts +1 -0
  61. package/components/elements/display/HowlerCard.test.js +51 -0
  62. package/components/elements/display/Image.test.d.ts +1 -0
  63. package/components/elements/display/Image.test.js +79 -0
  64. package/components/elements/display/ItemManager.d.ts +2 -2
  65. package/components/elements/display/ItemManager.js +1 -1
  66. package/components/elements/display/Markdown.js +9 -2
  67. package/components/elements/display/QueryResultText.test.d.ts +1 -0
  68. package/components/elements/display/QueryResultText.test.js +48 -0
  69. package/components/elements/display/TextDivider.test.d.ts +1 -0
  70. package/components/elements/display/TextDivider.test.js +43 -0
  71. package/components/elements/display/TypingIndicator.test.d.ts +1 -0
  72. package/components/elements/display/TypingIndicator.test.js +33 -0
  73. package/components/elements/display/modals/ConfirmDeleteModal.d.ts +4 -0
  74. package/components/elements/display/modals/ConfirmDeleteModal.js +4 -2
  75. package/components/elements/hit/HitBanner.js +6 -3
  76. package/components/elements/hit/HitLinks.js +1 -1
  77. package/components/elements/hit/HitSummary.js +3 -2
  78. package/components/elements/hit/elements/AnalyticLink.js +1 -1
  79. package/components/elements/hit/elements/Assigned.d.ts +2 -0
  80. package/components/elements/hit/elements/Assigned.js +15 -13
  81. package/components/elements/record/RecordContextMenu.js +15 -3
  82. package/components/elements/record/RecordContextMenu.test.js +96 -0
  83. package/components/hooks/useMyApi.d.ts +1 -1
  84. package/components/hooks/useMyPreferences.js +2 -1
  85. package/components/routes/403.d.ts +3 -0
  86. package/components/routes/403.js +10 -0
  87. package/components/routes/403.test.d.ts +1 -0
  88. package/components/routes/403.test.js +27 -0
  89. package/components/routes/404.test.d.ts +1 -0
  90. package/components/routes/404.test.js +26 -0
  91. package/components/routes/ErrorBoundary.test.d.ts +1 -0
  92. package/components/routes/ErrorBoundary.test.js +43 -0
  93. package/components/routes/ErrorOccured.test.d.ts +1 -0
  94. package/components/routes/ErrorOccured.test.js +35 -0
  95. package/components/routes/Logout.test.d.ts +1 -0
  96. package/components/routes/Logout.test.js +59 -0
  97. package/components/routes/action/edit/ActionEditor.js +1 -1
  98. package/components/routes/action/useMyActionFunctions.js +30 -6
  99. package/components/routes/action/view/ActionDetails.js +15 -2
  100. package/components/routes/action/view/ActionSearch.js +32 -20
  101. package/components/routes/action/view/markdown/integrations.en.md.js +1 -1
  102. package/components/routes/action/view/markdown/integrations.fr.md.js +1 -1
  103. package/components/routes/analytics/AnalyticDetails.js +10 -6
  104. package/components/routes/analytics/AnalyticSearch.js +6 -6
  105. package/components/routes/cases/Cases.js +5 -6
  106. package/components/routes/cases/modals/ResolveModal.js +14 -13
  107. package/components/routes/cases/modals/ResolveModal.test.js +85 -25
  108. package/components/routes/dossiers/Dossiers.js +25 -19
  109. package/components/routes/help/ActionIntroductionDocumentation.js +1 -1
  110. package/components/routes/help/markdown/en/client.md.js +1 -1
  111. package/components/routes/help/markdown/en/retention.md.js +1 -1
  112. package/components/routes/help/markdown/fr/client.md.js +1 -1
  113. package/components/routes/help/markdown/fr/retention.md.js +1 -1
  114. package/components/routes/hits/search/shared/CustomSpan.js +7 -7
  115. package/components/routes/hits/search/shared/CustomSpan.test.d.ts +1 -0
  116. package/components/routes/hits/search/shared/CustomSpan.test.js +170 -0
  117. package/components/routes/home/AddNewCard.js +1 -1
  118. package/components/routes/home/AddNewCard.test.d.ts +1 -0
  119. package/components/routes/home/AddNewCard.test.js +86 -0
  120. package/components/routes/home/ViewCard.js +1 -1
  121. package/components/routes/home/ViewRefresh.test.d.ts +1 -0
  122. package/components/routes/home/ViewRefresh.test.js +79 -0
  123. package/components/routes/overviews/OverviewCard.test.d.ts +1 -0
  124. package/components/routes/overviews/OverviewCard.test.js +108 -0
  125. package/components/routes/overviews/OverviewViewer.js +16 -8
  126. package/components/routes/overviews/Overviews.js +25 -19
  127. package/components/routes/overviews/template/en.md.js +1 -1
  128. package/components/routes/overviews/template/fr.md.js +1 -1
  129. package/components/routes/settings/LocalSection.js +2 -1
  130. package/components/routes/settings/SettingsSection.test.d.ts +1 -0
  131. package/components/routes/settings/SettingsSection.test.js +19 -0
  132. package/components/routes/templates/TemplateCard.d.ts +3 -1
  133. package/components/routes/templates/TemplateCard.js +8 -4
  134. package/components/routes/templates/TemplateCard.test.d.ts +1 -0
  135. package/components/routes/templates/TemplateCard.test.js +115 -0
  136. package/components/routes/templates/TemplateViewer.js +62 -16
  137. package/components/routes/templates/Templates.js +35 -13
  138. package/components/routes/views/Views.js +21 -14
  139. package/locales/en/translation.json +25 -4
  140. package/locales/fr/translation.json +25 -4
  141. package/models/entities/generated/ApiType.d.ts +1 -1
  142. package/models/entities/generated/Hit.d.ts +1 -0
  143. package/models/entities/generated/Howler.d.ts +1 -0
  144. package/models/entities/generated/UserUser.d.ts +2 -0
  145. package/package.json +143 -148
  146. package/rest/FetchClient.test.d.ts +1 -0
  147. package/rest/FetchClient.test.js +81 -0
  148. package/setupTests.js +5 -0
  149. package/utils/Throttler.test.d.ts +1 -0
  150. package/utils/Throttler.test.js +135 -0
  151. package/utils/actionUtils.test.d.ts +1 -0
  152. package/utils/actionUtils.test.js +140 -0
  153. package/utils/constants.d.ts +1 -0
  154. package/utils/constants.js +1 -0
  155. package/utils/hitFunctions.test.d.ts +1 -0
  156. package/utils/hitFunctions.test.js +52 -0
  157. package/utils/localStorage.test.d.ts +1 -0
  158. package/utils/localStorage.test.js +96 -0
  159. package/utils/menuUtils.js +1 -1
  160. package/utils/menuUtils.test.d.ts +1 -0
  161. package/utils/menuUtils.test.js +198 -0
  162. package/utils/sessionStorage.test.d.ts +1 -0
  163. package/utils/sessionStorage.test.js +81 -0
  164. package/utils/stringUtils.test.d.ts +1 -0
  165. package/utils/stringUtils.test.js +159 -0
  166. package/utils/utils.test.d.ts +1 -0
  167. package/utils/utils.test.js +292 -0
  168. package/utils/viewUtils.test.d.ts +1 -0
  169. package/utils/viewUtils.test.js +45 -0
  170. package/utils/xsrf.test.d.ts +1 -0
  171. package/utils/xsrf.test.js +41 -0
@@ -1 +1 @@
1
- export default "# Cr\u00e9er un aper\u00e7u\n\nLes aper\u00e7us peuvent \u00eatre utilis\u00e9s pour modifier la fa\u00e7on dont les donn\u00e9es sont pr\u00e9sent\u00e9es sur les alertes qui correspondent aux param\u00e8tres de l'aper\u00e7u. Les aper\u00e7us sont, par conception, faciles \u00e0 cr\u00e9er et assez flexibles.\n\n## Premiers pas\n\nLes \u00e9l\u00e9ments de base des aper\u00e7us sont :\n\n1. Markdown\n2. Handlebars\n\nNous allons rapidement expliquer ces \u00e9l\u00e9ments.\n\n### Markdown\n\nCitation de l'excellent [markdownguide.org](https://www.markdownguide.org/getting-started/) :\n\n> Markdown est un langage de balisage l\u00e9ger que vous pouvez utiliser pour ajouter des \u00e9l\u00e9ments de formatage aux documents texte en texte brut. Cr\u00e9\u00e9 par [John Gruber](https://daringfireball.net/projects/markdown/) en 2004, Markdown est maintenant l'un des langages de balisage les plus populaires au monde.\n>\n> L'utilisation de Markdown est diff\u00e9rente de l'utilisation d'un \u00e9diteur [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG). Dans une application comme Microsoft Word, vous cliquez sur des boutons pour formater les mots et les phrases, et les changements sont visibles imm\u00e9diatement. Markdown n'est pas comme cela. Lorsque vous cr\u00e9ez un fichier format\u00e9 en Markdown, vous ajoutez une syntaxe Markdown au texte pour indiquer quels mots et phrases doivent appara\u00eetre diff\u00e9remment.\n>\n> Par exemple, pour d\u00e9signer un titre, vous ajoutez un signe di\u00e8se avant celui-ci (par ex., `# Titre Un`). Ou pour mettre une phrase en gras, vous ajoutez deux ast\u00e9risques avant et apr\u00e8s (par ex., `**ce texte est en gras**`). Il peut falloir un certain temps pour s'habituer \u00e0 voir la syntaxe Markdown dans votre texte, surtout si vous \u00eates habitu\u00e9 aux applications WYSIWYG.\n\n---\n\n### Handlebars\n\nCitation de [handlebarsjs.com](https://handlebarsjs.com/guide/) :\n\n> Handlebars est un langage de template simple.\n>\n> Il utilise un template et un objet d'entr\u00e9e pour g\u00e9n\u00e9rer du HTML ou d'autres formats de texte. Les templates Handlebars ressemblent \u00e0 du texte normal avec des expressions Handlebars int\u00e9gr\u00e9es.\n>\n>```html\n> <p>{{curly \"firstname\"}} {{curly \"lastname\"}}</p>\n>```\n>\n> Une expression handlebars est une double accolade, du contenu, suivi d'un ensemble d'accolades fermantes doubles. Lorsque le template est ex\u00e9cut\u00e9, ces expressions sont remplac\u00e9es par des valeurs d'un objet d'entr\u00e9e.\n\n---\n\nDans nos cas, nous utilisons handlebars pour remplacer des parties sp\u00e9cifiques du markdown par les valeurs incluses dans un r\u00e9sultat howler donn\u00e9. Par exemple :\n\n```markdown\nCette analytique est **{{curly \"howler.analytic\"}}**\n```\n\ndevient :\n\n> Cette analytique est **{{howler.analytic}}**.\n\nPour plus d'informations sur handlebars, consultez :\n\n- [Qu'est-ce que Handlebars ?](https://handlebarsjs.com/guide/#what-is-handlebars)\n- [Expressions Handlebars](https://handlebarsjs.com/guide/expressions.html)\n\n## Combiner Markdown et Handlebars\n\nVous pouvez utiliser handlebars pour le remplacement de template dans tout votre markdown. Voici un exemple de tableau utilisant handlebars et markdown :\n\n```markdown\n\n| IP Source | IP Destination |\n| --- | --- |\n| {{curly \"source.ip\"}} |{{curly \"destination.ip\"}} |\n```\n\ns'affiche comme :\n\n| IP Source | IP Destination |\n| --- | --- |\n| {{source.ip}} |{{destination.ip}} |\n\n## Handlebars avanc\u00e9s\n\nHowler int\u00e8gre un certain nombre de fonctions d'aide avec lesquelles vous pouvez travailler.\n\n### Expressions de contr\u00f4le\n\nPour utilisation comme sous-expressions, nous exposons un certain nombre de v\u00e9rifications conditionnelles :\n\n**\u00c9galit\u00e9 :**\n\n\u00c9tant donn\u00e9 que `howler.status` est {{howler.status}} :\n\n```markdown\n{{curly '#if (equals howler.status \"open\")'}}\nLe r\u00e9sultat est ouvert !\n{{curly \"/if\"}}\n{{curly '#if (equals howler.status \"resolved\")'}}\nLe r\u00e9sultat est r\u00e9solu !\n{{curly \"/if\"}}\n```\n\n{{#if (equals howler.status \"open\")}}\nLe r\u00e9sultat est ouvert !\n{{/if}}\n{{#if (equals howler.status \"resolved\")}}\nLe r\u00e9sultat est r\u00e9solu !\n{{/if}}\n\n**ET/OU/NON :**\n\n\u00c9tant donn\u00e9 que `howler.status` est {{howler.status}}, et `howler.escalation` est {{howler.escalation}} :\n\n```markdown\n{{curly '#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))'}}\nC'est correct !\n{{curly \"/if\"}}\n{{curly '#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))'}}\nC'est incorrect !\n{{curly \"/if\"}}\n```\n\n{{#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))}}\nC'est correct !\n{{/if}}\n{{#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))}}\nC'est incorrect !\n{{/if}}\n\n```markdown\n{{curly '#if (or howler.is_bundle (not howler.is_bundle))'}}\nS'affiche toujours !\n{{curly \"/if\"}}\n```\n\n{{#if (or howler.is_bundle (not howler.is_bundle))}}\nS'affiche toujours !\n{{/if}}\n\n---\n\n### Op\u00e9rations sur les cha\u00eenes\n\n**Concat\u00e9nation de cha\u00eenes :**\n\n```markdown\n{{curly 'join \"cha\u00eene une \" \"cha\u00eene deux\"'}}\n```\n\n{{join \"cha\u00eene une \" \"cha\u00eene deux\"}}\n\n**Majuscules/Minuscules :**\n\n```markdown\n{{curly 'upper \"mettre ceci en majuscules\"'}}\n{{curly 'lower \"METTRE CECI EN MINUSCULES\"'}}\n```\n\n{{upper \"mettre ceci en majuscules\"}}\n\n{{lower \"METTRE CECI EN MINUSCULES\"}}\n\n---\n\n### R\u00e9cup\u00e9ration de donn\u00e9es\n\nVous pouvez \u00e9galement faire des requ\u00eates fetch de base pour r\u00e9cup\u00e9rer et analyser des donn\u00e9es JSON de sources externes :\n\n```markdown\n{{curly 'fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"'}}\n```\n\n{{fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"}}\n\n## Liste compl\u00e8te des assistants\n"
1
+ export default "# Cr\u00e9er un aper\u00e7u\n\nLes aper\u00e7us peuvent \u00eatre utilis\u00e9s pour modifier la fa\u00e7on dont les donn\u00e9es sont pr\u00e9sent\u00e9es sur les alertes qui correspondent aux param\u00e8tres de l'aper\u00e7u. Les aper\u00e7us sont, par conception, faciles \u00e0 cr\u00e9er et assez flexibles.\n\n## Premiers pas\n\nLes \u00e9l\u00e9ments de base des aper\u00e7us sont :\n\n1. Markdown\n2. Handlebars\n\nNous allons rapidement expliquer ces \u00e9l\u00e9ments.\n\n### Markdown\n\nCitation de l'excellent [markdownguide.org](https://www.markdownguide.org/getting-started/) :\n\n> Markdown est un langage de balisage l\u00e9ger que vous pouvez utiliser pour ajouter des \u00e9l\u00e9ments de formatage aux documents texte en texte brut. Cr\u00e9\u00e9 par [John Gruber](https://daringfireball.net/projects/markdown/) en 2004, Markdown est maintenant l'un des langages de balisage les plus populaires au monde.\n>\n> L'utilisation de Markdown est diff\u00e9rente de l'utilisation d'un \u00e9diteur [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG). Dans une application comme Microsoft Word, vous cliquez sur des boutons pour formater les mots et les phrases, et les changements sont visibles imm\u00e9diatement. Markdown n'est pas comme cela. Lorsque vous cr\u00e9ez un fichier format\u00e9 en Markdown, vous ajoutez une syntaxe Markdown au texte pour indiquer quels mots et phrases doivent appara\u00eetre diff\u00e9remment.\n>\n> Par exemple, pour d\u00e9signer un titre, vous ajoutez un signe di\u00e8se avant celui-ci (par ex., `# Titre Un`). Ou pour mettre une phrase en gras, vous ajoutez deux ast\u00e9risques avant et apr\u00e8s (par ex., `**ce texte est en gras**`). Il peut falloir un certain temps pour s'habituer \u00e0 voir la syntaxe Markdown dans votre texte, surtout si vous \u00eates habitu\u00e9 aux applications WYSIWYG.\n\n---\n\n### Handlebars\n\nCitation de [handlebarsjs.com](https://handlebarsjs.com/guide/) :\n\n> Handlebars est un langage de template simple.\n>\n> Il utilise un template et un objet d'entr\u00e9e pour g\u00e9n\u00e9rer du HTML ou d'autres formats de texte. Les templates Handlebars ressemblent \u00e0 du texte normal avec des expressions Handlebars int\u00e9gr\u00e9es.\n>\n> ```html\n> <p>{{curly \"firstname\"}} {{curly \"lastname\"}}</p>\n> ```\n>\n> Une expression handlebars est une double accolade, du contenu, suivi d'un ensemble d'accolades fermantes doubles. Lorsque le template est ex\u00e9cut\u00e9, ces expressions sont remplac\u00e9es par des valeurs d'un objet d'entr\u00e9e.\n\n---\n\nDans nos cas, nous utilisons handlebars pour remplacer des parties sp\u00e9cifiques du markdown par les valeurs incluses dans un r\u00e9sultat howler donn\u00e9. Par exemple :\n\n```markdown\nCette analytique est **{{curly \"howler.analytic\"}}**\n```\n\ndevient :\n\n> Cette analytique est **{{howler.analytic}}**.\n\nPour plus d'informations sur handlebars, consultez :\n\n- [Qu'est-ce que Handlebars ?](https://handlebarsjs.com/guide/#what-is-handlebars)\n- [Expressions Handlebars](https://handlebarsjs.com/guide/expressions.html)\n\n## Combiner Markdown et Handlebars\n\nVous pouvez utiliser handlebars pour le remplacement de template dans tout votre markdown. Voici un exemple de tableau utilisant handlebars et markdown :\n\n```markdown\n| IP Source | IP Destination |\n| --------------------- | -------------------------- |\n| {{curly \"source.ip\"}} | {{curly \"destination.ip\"}} |\n```\n\ns'affiche comme :\n\n| IP Source | IP Destination |\n| ------------- | ------------------ |\n| {{source.ip}} | {{destination.ip}} |\n\n## Handlebars avanc\u00e9s\n\nHowler int\u00e8gre un certain nombre de fonctions d'aide avec lesquelles vous pouvez travailler.\n\n### Expressions de contr\u00f4le\n\nPour utilisation comme sous-expressions, nous exposons un certain nombre de v\u00e9rifications conditionnelles :\n\n**\u00c9galit\u00e9 :**\n\n\u00c9tant donn\u00e9 que `howler.status` est {{howler.status}} :\n\n```markdown\n{{curly '#if (equals howler.status \"open\")'}}\nLe r\u00e9sultat est ouvert !\n{{curly \"/if\"}}\n{{curly '#if (equals howler.status \"resolved\")'}}\nLe r\u00e9sultat est r\u00e9solu !\n{{curly \"/if\"}}\n```\n\n{{#if (equals howler.status \"open\")}}\nLe r\u00e9sultat est ouvert !\n{{/if}}\n{{#if (equals howler.status \"resolved\")}}\nLe r\u00e9sultat est r\u00e9solu !\n{{/if}}\n\n**ET/OU/NON :**\n\n\u00c9tant donn\u00e9 que `howler.status` est {{howler.status}}, et `howler.escalation` est {{howler.escalation}} :\n\n```markdown\n{{curly '#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))'}}\nC'est correct !\n{{curly \"/if\"}}\n{{curly '#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))'}}\nC'est incorrect !\n{{curly \"/if\"}}\n```\n\n{{#if (and (equals howler.status \"open\") (equals howler.escalation \"alert\"))}}\nC'est correct !\n{{/if}}\n{{#if (and (equals howler.status \"resolved\") (equals howler.escalation \"hit\"))}}\nC'est incorrect !\n{{/if}}\n\n```markdown\n{{curly '#if (or howler.is_bundle (not howler.is_bundle))'}}\nS'affiche toujours !\n{{curly \"/if\"}}\n```\n\n{{#if (or howler.is_bundle (not howler.is_bundle))}}\nS'affiche toujours !\n{{/if}}\n\n---\n\n### Op\u00e9rations sur les cha\u00eenes\n\n**Concat\u00e9nation de cha\u00eenes :**\n\n```markdown\n{{curly 'join \"cha\u00eene une \" \"cha\u00eene deux\"'}}\n```\n\n{{join \"cha\u00eene une \" \"cha\u00eene deux\"}}\n\n**Majuscules/Minuscules :**\n\n```markdown\n{{curly 'upper \"mettre ceci en majuscules\"'}}\n{{curly 'lower \"METTRE CECI EN MINUSCULES\"'}}\n```\n\n{{upper \"mettre ceci en majuscules\"}}\n\n{{lower \"METTRE CECI EN MINUSCULES\"}}\n\n---\n\n### R\u00e9cup\u00e9ration de donn\u00e9es\n\nVous pouvez \u00e9galement faire des requ\u00eates fetch de base pour r\u00e9cup\u00e9rer et analyser des donn\u00e9es JSON de sources externes :\n\n```markdown\n{{curly 'fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"'}}\n```\n\n{{fetch \"/api/v1/configs\" \"api_response.c12nDef.UNRESTRICTED\"}}\n\n## Liste compl\u00e8te des assistants\n"
@@ -17,11 +17,12 @@ const LocalSection = () => {
17
17
  const [compactJson, setCompactJson] = useMyLocalStorageItem(StorageKey.COMPACT_JSON, true);
18
18
  const [flattenJson, setFlattenJson] = useMyLocalStorageItem(StorageKey.FLATTEN_JSON, false);
19
19
  const [forceDrawer, setForceDrawer] = useMyLocalStorageItem(StorageKey.FORCE_DRAWER, false);
20
+ const [showHitSummaryGraph, setShowHitSummaryGraph] = useMyLocalStorageItem(StorageKey.SHOW_HIT_SUMMARY_GRAPH, true);
20
21
  const [hitLayout, setHitLayout] = useMyLocalStorageItem(StorageKey.HIT_LAYOUT, false);
21
22
  const [displayType, setDisplayType] = useMyLocalStorageItem(StorageKey.DISPLAY_TYPE, 'list');
22
23
  const [pageCount, setPageCount] = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25);
23
24
  const [searchWidth, setSearchWidth] = useMyLocalStorageItem(StorageKey.SEARCH_PANE_WIDTH, null);
24
25
  const [templateFieldCount, setTemplateFieldCount] = useMyLocalStorageItem(StorageKey.TEMPLATE_FIELD_COUNT, null);
25
- return (_jsxs(SettingsSection, { title: t('page.settings.local.title'), colSpan: 3, children: [_jsx(EditRow, { titleKey: "page.settings.local.compact.json", descriptionKey: "page.settings.local.compact.json.description", value: compactJson, type: "checkbox", onEdit: async (value) => setCompactJson(JSON.parse(value)) }), _jsx(EditRow, { titleKey: "page.settings.local.flatten.json", descriptionKey: "page.settings.local.flatten.json.description", value: flattenJson, type: "checkbox", onEdit: async (value) => setFlattenJson(JSON.parse(value)) }), _jsx(EditRow, { titleKey: "page.settings.local.details.drawer", descriptionKey: "page.settings.local.details.drawer.description", value: forceDrawer, type: "checkbox", onEdit: async (value) => setForceDrawer(JSON.parse(value)) }), _jsx(EditRow, { titleKey: "page.settings.local.search.width", descriptionKey: "page.settings.local.search.width.description", value: searchWidth, type: "range", min: 400, max: Math.floor(window.innerWidth / 100) * 100, optional: true, onEdit: async (value) => setSearchWidth(value ? parseInt(value) : null) }), _jsxs(TableRow, { children: [_jsx(TableCell, { sx: CELL_SX, style: { whiteSpace: 'nowrap' }, children: t('page.settings.local.hits.layout') }), _jsx(TableCell, { sx: CELL_SX, colSpan: 2, align: "right", children: _jsxs(ToggleButtonGroup, { size: "small", value: hitLayout, exclusive: true, onChange: (_, value) => setHitLayout(value), children: [_jsx(ToggleButton, { value: HitLayout.DENSE, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewCompact, {}), _jsx("span", { children: t('page.settings.local.hits.layout.dense') })] }) }), _jsx(ToggleButton, { value: HitLayout.NORMAL, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewModule, {}), _jsx("span", { children: t('page.settings.local.hits.layout.normal') })] }) }), _jsx(ToggleButton, { value: HitLayout.COMFY, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewComfy, {}), _jsx("span", { children: t('page.settings.local.hits.layout.comfy') })] }) })] }) })] }), _jsx(TableRow, { children: _jsx(TableCell, { colSpan: 3, sx: { paddingTop: '0 !important' }, children: _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('page.settings.local.hits.layout.description') }) }) }), _jsx(EditRow, { titleKey: "page.settings.local.hits.field_count", descriptionKey: "page.settings.local.hits.field_count.description", value: templateFieldCount, type: "range", min: 0, step: 1, max: 15, optional: true, onEdit: async (value) => setTemplateFieldCount(value ? parseInt(value) : null), valueLabelFormat: val => val.toString() }), _jsxs(TableRow, { children: [_jsx(TableCell, { sx: CELL_SX, style: { whiteSpace: 'nowrap' }, children: t('page.settings.local.hits.display_type') }), _jsx(TableCell, { sx: CELL_SX, colSpan: 2, align: "right", children: _jsxs(ToggleButtonGroup, { size: "small", value: displayType, exclusive: true, onChange: (_, value) => setDisplayType(value), children: [_jsx(ToggleButton, { value: "list", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(List, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.list') })] }) }), _jsx(ToggleButton, { value: "grid", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(TableChart, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.grid') })] }) })] }) })] }), _jsx(TableRow, { children: _jsx(TableCell, { colSpan: 3, sx: { paddingTop: '0 !important' }, children: _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('page.settings.local.hits.display_type.description') }) }) }), _jsxs(TableRow, { children: [_jsx(TableCell, { sx: CELL_SX, style: { whiteSpace: 'nowrap' }, children: t('page.settings.local.results.count') }), _jsx(TableCell, { sx: CELL_SX, colSpan: 2, align: "right", children: _jsxs(Select, { size: "small", sx: { minWidth: '75px', textAlign: 'left' }, value: pageCount, onChange: event => setPageCount(typeof event.target.value === 'string' ? parseInt(event.target.value) : event.target.value), children: [_jsx(MenuItem, { value: 5, children: "5" }), _jsx(MenuItem, { value: 10, children: "10" }), _jsx(MenuItem, { value: 25, children: "25" }), _jsx(MenuItem, { value: 50, children: "50" }), _jsx(MenuItem, { value: 75, children: "75" }), _jsx(MenuItem, { value: 100, children: "100" }), _jsx(MenuItem, { value: 150, children: "150" }), _jsx(MenuItem, { value: 250, children: "250" })] }) })] }), _jsx(TableRow, { children: _jsx(TableCell, { colSpan: 3, sx: { paddingTop: '0 !important' }, children: _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('page.settings.local.results.count.description') }) }) }), howlerPluginStore.plugins.map(plugin => pluginStore.executeFunction(`${plugin}.settings`, 'local'))] }));
26
+ return (_jsxs(SettingsSection, { title: t('page.settings.local.title'), colSpan: 3, children: [_jsx(EditRow, { titleKey: "page.settings.local.compact.json", descriptionKey: "page.settings.local.compact.json.description", value: compactJson, type: "checkbox", onEdit: async (value) => setCompactJson(JSON.parse(value)) }), _jsx(EditRow, { titleKey: "page.settings.local.flatten.json", descriptionKey: "page.settings.local.flatten.json.description", value: flattenJson, type: "checkbox", onEdit: async (value) => setFlattenJson(JSON.parse(value)) }), _jsx(EditRow, { titleKey: "page.settings.local.details.drawer", descriptionKey: "page.settings.local.details.drawer.description", value: forceDrawer, type: "checkbox", onEdit: async (value) => setForceDrawer(JSON.parse(value)) }), _jsx(EditRow, { titleKey: "page.settings.local.show.hit.summary.graph", descriptionKey: "page.settings.local.show.hit.summary.graph.description", value: showHitSummaryGraph, type: "checkbox", onEdit: async (value) => setShowHitSummaryGraph(JSON.parse(value)) }), _jsx(EditRow, { titleKey: "page.settings.local.search.width", descriptionKey: "page.settings.local.search.width.description", value: searchWidth, type: "range", min: 400, max: Math.floor(window.innerWidth / 100) * 100, optional: true, onEdit: async (value) => setSearchWidth(value ? parseInt(value) : null) }), _jsxs(TableRow, { children: [_jsx(TableCell, { sx: CELL_SX, style: { whiteSpace: 'nowrap' }, children: t('page.settings.local.hits.layout') }), _jsx(TableCell, { sx: CELL_SX, colSpan: 2, align: "right", children: _jsxs(ToggleButtonGroup, { size: "small", value: hitLayout, exclusive: true, onChange: (_, value) => setHitLayout(value), children: [_jsx(ToggleButton, { value: HitLayout.DENSE, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewCompact, {}), _jsx("span", { children: t('page.settings.local.hits.layout.dense') })] }) }), _jsx(ToggleButton, { value: HitLayout.NORMAL, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewModule, {}), _jsx("span", { children: t('page.settings.local.hits.layout.normal') })] }) }), _jsx(ToggleButton, { value: HitLayout.COMFY, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewComfy, {}), _jsx("span", { children: t('page.settings.local.hits.layout.comfy') })] }) })] }) })] }), _jsx(TableRow, { children: _jsx(TableCell, { colSpan: 3, sx: { paddingTop: '0 !important' }, children: _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('page.settings.local.hits.layout.description') }) }) }), _jsx(EditRow, { titleKey: "page.settings.local.hits.field_count", descriptionKey: "page.settings.local.hits.field_count.description", value: templateFieldCount, type: "range", min: 0, step: 1, max: 15, optional: true, onEdit: async (value) => setTemplateFieldCount(value ? parseInt(value) : null), valueLabelFormat: val => val.toString() }), _jsxs(TableRow, { children: [_jsx(TableCell, { sx: CELL_SX, style: { whiteSpace: 'nowrap' }, children: t('page.settings.local.hits.display_type') }), _jsx(TableCell, { sx: CELL_SX, colSpan: 2, align: "right", children: _jsxs(ToggleButtonGroup, { size: "small", value: displayType, exclusive: true, onChange: (_, value) => setDisplayType(value), children: [_jsx(ToggleButton, { value: "list", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(List, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.list') })] }) }), _jsx(ToggleButton, { value: "grid", children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(TableChart, {}), _jsx("span", { children: t('page.settings.local.hits.display_type.grid') })] }) })] }) })] }), _jsx(TableRow, { children: _jsx(TableCell, { colSpan: 3, sx: { paddingTop: '0 !important' }, children: _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('page.settings.local.hits.display_type.description') }) }) }), _jsxs(TableRow, { children: [_jsx(TableCell, { sx: CELL_SX, style: { whiteSpace: 'nowrap' }, children: t('page.settings.local.results.count') }), _jsx(TableCell, { sx: CELL_SX, colSpan: 2, align: "right", children: _jsxs(Select, { size: "small", sx: { minWidth: '75px', textAlign: 'left' }, value: pageCount, onChange: event => setPageCount(typeof event.target.value === 'string' ? parseInt(event.target.value) : event.target.value), children: [_jsx(MenuItem, { value: 5, children: "5" }), _jsx(MenuItem, { value: 10, children: "10" }), _jsx(MenuItem, { value: 25, children: "25" }), _jsx(MenuItem, { value: 50, children: "50" }), _jsx(MenuItem, { value: 75, children: "75" }), _jsx(MenuItem, { value: 100, children: "100" }), _jsx(MenuItem, { value: 150, children: "150" }), _jsx(MenuItem, { value: 250, children: "250" })] }) })] }), _jsx(TableRow, { children: _jsx(TableCell, { colSpan: 3, sx: { paddingTop: '0 !important' }, children: _jsx(Typography, { variant: "caption", color: "text.secondary", children: t('page.settings.local.results.count.description') }) }) }), howlerPluginStore.plugins.map(plugin => pluginStore.executeFunction(`${plugin}.settings`, 'local'))] }));
26
27
  };
27
28
  export default LocalSection;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render, screen } from '@testing-library/react';
4
+ import { describe, expect, it } from 'vitest';
5
+ import SettingsSection from './SettingsSection';
6
+ describe('SettingsSection', () => {
7
+ it('should render the title in a table header', () => {
8
+ render(_jsx(SettingsSection, { title: "Test Section", colSpan: 3, children: _jsx("tr", { children: _jsx("td", { children: "Content" }) }) }));
9
+ expect(screen.getByText('Test Section')).toBeInTheDocument();
10
+ });
11
+ it('should render children inside a table body', () => {
12
+ render(_jsx(SettingsSection, { title: "Test Section", colSpan: 2, children: _jsx("tr", { children: _jsx("td", { children: "Row Content" }) }) }));
13
+ expect(screen.getByText('Row Content')).toBeInTheDocument();
14
+ });
15
+ it('should use the title as the table aria-label', () => {
16
+ render(_jsx(SettingsSection, { title: "Accessible Section", colSpan: 3, children: _jsx("tr", { children: _jsx("td", { children: "Content" }) }) }));
17
+ expect(screen.getByRole('table', { name: 'Accessible Section' })).toBeInTheDocument();
18
+ });
19
+ });
@@ -1,7 +1,9 @@
1
1
  import type { Template } from '@cccsaurora/howler-ui/models/entities/generated/Template';
2
- import type { FC } from 'react';
2
+ import { type FC } from 'react';
3
3
  declare const TemplateCard: FC<{
4
4
  template: Template;
5
+ onRemove?: (templateId: string) => void;
6
+ error?: boolean;
5
7
  className?: string;
6
8
  }>;
7
9
  export default TemplateCard;
@@ -1,13 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Language, Lock, Person } from '@mui/icons-material';
3
- import { Card, Divider, Stack, Tooltip, Typography } from '@mui/material';
2
+ import { Language, Lock, Person, ReportProblem } from '@mui/icons-material';
3
+ import { Button, Card, Divider, Stack, Tooltip, Typography } from '@mui/material';
4
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
5
+ import ConfirmDeleteModal from '@cccsaurora/howler-ui/components/elements/display/modals/ConfirmDeleteModal';
6
+ import { useContext } from 'react';
4
7
  import { useTranslation } from 'react-i18next';
5
- const TemplateCard = ({ template, className }) => {
8
+ const TemplateCard = ({ template, onRemove, error, className }) => {
6
9
  const { t } = useTranslation();
10
+ const { showModal } = useContext(ModalContext);
7
11
  return (_jsx(Card, { variant: "outlined", sx: { p: 1, mb: 1 }, className: className, children: _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Tooltip, { title: t(`route.templates.manager.${template.type}`), children: {
8
12
  readonly: _jsx(Lock, {}),
9
13
  global: _jsx(Language, {}),
10
14
  personal: _jsx(Person, {})
11
- }[template.type] }), _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsxs(Stack, { children: [_jsxs(Typography, { variant: "body1", children: [t(template.analytic), " - ", t(template.detection ?? 'all')] }), template.keys.map(key => (_jsx(Typography, { variant: "caption", sx: { ml: 1 }, children: _jsx("code", { children: key }) }, template.template_id + key)))] })] }) }, template.template_id));
15
+ }[template.type] }), _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsxs(Stack, { children: [_jsxs(Typography, { variant: "body1", children: [t(template.analytic), " - ", t(template.detection ?? 'all')] }), template.keys.map(key => (_jsx(Typography, { variant: "caption", sx: { ml: 1 }, children: _jsx("code", { children: key }) }, template.template_id + key)))] }), error && (_jsx(Stack, { direction: "row", justifyContent: "end", width: "100%", children: _jsx(Stack, { children: _jsx(Tooltip, { title: t('route.templates.manager.error.action'), children: _jsx(Button, { startIcon: _jsx(ReportProblem, {}), color: "warning", onClick: () => showModal(_jsx(ConfirmDeleteModal, { onConfirm: () => onRemove?.(template.template_id), title: t('route.templates.manager.error.modal.title'), description: t('route.templates.manager.error.modal.description'), preferDelete: true })), children: t('route.templates.manager.error.message') }) }) }) }))] }) }, template.template_id));
12
16
  };
13
17
  export default TemplateCard;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render, screen } from '@testing-library/react';
4
+ import userEvent, {} from '@testing-library/user-event';
5
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
6
+ import i18n from '@cccsaurora/howler-ui/i18n';
7
+ import { I18nextProvider } from 'react-i18next';
8
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
9
+ import TemplateCard from './TemplateCard';
10
+ const mockShowModal = vi.fn();
11
+ const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(ModalContext.Provider, { value: { showModal: mockShowModal }, children: children }) }));
12
+ describe('TemplateCard', () => {
13
+ let user;
14
+ beforeEach(() => {
15
+ user = userEvent.setup();
16
+ vi.clearAllMocks();
17
+ });
18
+ it('should render template analytic and detection', () => {
19
+ const template = {
20
+ template_id: 'tmpl-1',
21
+ analytic: 'Test Analytic',
22
+ detection: 'Test Detection',
23
+ type: 'personal',
24
+ keys: ['howler.detection', 'event.id']
25
+ };
26
+ render(_jsx(TemplateCard, { template: template }), { wrapper: Wrapper });
27
+ expect(screen.getByText(/Test Analytic/)).toBeInTheDocument();
28
+ expect(screen.getByText(/Test Detection/)).toBeInTheDocument();
29
+ });
30
+ it('should render template keys', () => {
31
+ const template = {
32
+ template_id: 'tmpl-1',
33
+ analytic: 'Test Analytic',
34
+ detection: 'Test Detection',
35
+ type: 'personal',
36
+ keys: ['howler.detection', 'event.id']
37
+ };
38
+ render(_jsx(TemplateCard, { template: template }), { wrapper: Wrapper });
39
+ expect(screen.getByText('howler.detection')).toBeInTheDocument();
40
+ expect(screen.getByText('event.id')).toBeInTheDocument();
41
+ });
42
+ it('should render personal type icon with tooltip', () => {
43
+ const template = {
44
+ template_id: 'tmpl-1',
45
+ analytic: 'Test Analytic',
46
+ detection: null,
47
+ type: 'personal',
48
+ keys: ['howler.detection']
49
+ };
50
+ render(_jsx(TemplateCard, { template: template }), { wrapper: Wrapper });
51
+ // The personal icon (Person) should be rendered
52
+ expect(document.querySelector('[data-testid="PersonIcon"]')).toBeInTheDocument();
53
+ });
54
+ it('should render global type icon', () => {
55
+ const template = {
56
+ template_id: 'tmpl-1',
57
+ analytic: 'Test Analytic',
58
+ detection: null,
59
+ type: 'global',
60
+ keys: ['howler.detection']
61
+ };
62
+ render(_jsx(TemplateCard, { template: template }), { wrapper: Wrapper });
63
+ expect(document.querySelector('[data-testid="LanguageIcon"]')).toBeInTheDocument();
64
+ });
65
+ it('should render readonly type icon', () => {
66
+ const template = {
67
+ template_id: 'tmpl-1',
68
+ analytic: 'Test Analytic',
69
+ detection: null,
70
+ type: 'readonly',
71
+ keys: ['howler.detection']
72
+ };
73
+ render(_jsx(TemplateCard, { template: template }), { wrapper: Wrapper });
74
+ expect(document.querySelector('[data-testid="LockIcon"]')).toBeInTheDocument();
75
+ });
76
+ it('should not show error button when error is false', () => {
77
+ const template = {
78
+ template_id: 'tmpl-1',
79
+ analytic: 'Test Analytic',
80
+ detection: null,
81
+ type: 'personal',
82
+ keys: ['howler.detection']
83
+ };
84
+ render(_jsx(TemplateCard, { template: template, error: false }), { wrapper: Wrapper });
85
+ expect(document.querySelector('[data-testid="ReportProblemIcon"]')).not.toBeInTheDocument();
86
+ });
87
+ it('should show error button and trigger modal when error is true', async () => {
88
+ const mockOnRemove = vi.fn();
89
+ const template = {
90
+ template_id: 'tmpl-1',
91
+ analytic: 'Test Analytic',
92
+ detection: null,
93
+ type: 'personal',
94
+ keys: ['howler.detection']
95
+ };
96
+ render(_jsx(TemplateCard, { template: template, error: true, onRemove: mockOnRemove }), { wrapper: Wrapper });
97
+ expect(document.querySelector('[data-testid="ReportProblemIcon"]')).toBeInTheDocument();
98
+ const errorButton = screen.getByText('Invalid Detection');
99
+ await user.click(errorButton);
100
+ expect(mockShowModal).toHaveBeenCalled();
101
+ });
102
+ it('should apply custom className', () => {
103
+ const template = {
104
+ template_id: 'tmpl-1',
105
+ analytic: 'Test Analytic',
106
+ detection: null,
107
+ type: 'personal',
108
+ keys: ['howler.detection']
109
+ };
110
+ const { container } = render(_jsx(TemplateCard, { template: template, className: "custom-class" }), {
111
+ wrapper: Wrapper
112
+ });
113
+ expect(container.querySelector('.custom-class')).toBeInTheDocument();
114
+ });
115
+ });
@@ -3,12 +3,14 @@ import { Autocomplete, Button, CircularProgress, Divider, FormControl, LinearPro
3
3
  import api from '@cccsaurora/howler-ui/api';
4
4
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
5
5
  import TemplateEditor from '@cccsaurora/howler-ui/components/routes/templates/TemplateEditor';
6
- import { useCallback, useEffect, useMemo, useState } from 'react';
6
+ import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Check, Delete, SsidChart } from '@mui/icons-material';
9
9
  import AppInfoPanel from '@cccsaurora/howler-ui/commons/components/display/AppInfoPanel';
10
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
10
11
  import { DEFAULT_FIELDS } from '@cccsaurora/howler-ui/components/elements/hit/HitOutline';
11
12
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
13
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
12
14
  import isEqual from 'lodash-es/isEqual';
13
15
  import { useSearchParams } from 'react-router-dom';
14
16
  import hitsData from '@cccsaurora/howler-ui/utils/hit.json';
@@ -17,8 +19,11 @@ const TemplateViewer = () => {
17
19
  const { t } = useTranslation();
18
20
  const [params, setParams] = useSearchParams();
19
21
  const { dispatchApi } = useMyApi();
22
+ const { withConfirmDeleteModal } = useContext(ModalContext);
23
+ const { showSuccessMessage } = useMySnackbar();
20
24
  const [templateList, setTemplateList] = useState([]);
21
25
  const [selectedTemplate, setSelectedTemplate] = useState(null);
26
+ const [sessionTemplateList, setSessionTemplateList] = useState([]);
22
27
  const [displayFields, setDisplayFields] = useState([]);
23
28
  const [analytics, setAnalytics] = useState([]);
24
29
  const [detections, setDetections] = useState([]);
@@ -72,19 +77,22 @@ const TemplateViewer = () => {
72
77
  }, [analytic, detection, dispatchApi, params, setParams, type]);
73
78
  useEffect(() => {
74
79
  if (analytic && detection) {
75
- const template = (templateList ?? []).find(_template => _template.analytic === analytic &&
80
+ const findTemplate = (_templateList) => (_templateList ?? []).find(_template => _template.analytic === analytic &&
76
81
  ((detection === 'ANY' && !_template.detection) || _template.detection === detection) &&
77
82
  _template.type === type);
83
+ const template = findTemplate(templateList);
84
+ // check if a template has been modified in the session but not saved to the datastore
85
+ const sessionTemplate = findTemplate(sessionTemplateList);
78
86
  if (template) {
79
87
  setSelectedTemplate(template);
80
- setDisplayFields(template.keys);
88
+ setDisplayFields(sessionTemplate ? sessionTemplate.keys : template.keys);
81
89
  }
82
90
  else {
83
91
  setSelectedTemplate(null);
84
- setDisplayFields(DEFAULT_FIELDS);
92
+ setDisplayFields(sessionTemplate ? sessionTemplate.keys : DEFAULT_FIELDS);
85
93
  }
86
94
  }
87
- }, [analytic, detection, templateList, type]);
95
+ }, [analytic, detection, sessionTemplateList, templateList, type]);
88
96
  useEffect(() => {
89
97
  if (analytic) {
90
98
  params.set('analytic', analytic);
@@ -111,15 +119,31 @@ const TemplateViewer = () => {
111
119
  }
112
120
  return { ..._hit };
113
121
  }, [analytic]);
114
- const onDelete = useCallback(async () => {
115
- await dispatchApi(api.template.del(selectedTemplate.template_id), {
116
- logError: false,
117
- showError: true,
118
- throwError: false
122
+ const onDelete = useCallback(() => {
123
+ withConfirmDeleteModal(async () => {
124
+ await dispatchApi(api.template.del(selectedTemplate.template_id), {
125
+ logError: false,
126
+ showError: true,
127
+ throwError: true
128
+ });
129
+ setSessionTemplateList(l => l.filter(v => v.analytic != selectedTemplate.analytic ||
130
+ v.detection != selectedTemplate.detection ||
131
+ v.type != selectedTemplate.type));
132
+ setTemplateList(l => l.filter(v => v.analytic != selectedTemplate.analytic ||
133
+ v.detection != selectedTemplate.detection ||
134
+ v.type != selectedTemplate.type));
135
+ showSuccessMessage(t('route.templates.manager.delete.success'));
119
136
  });
120
- setSelectedTemplate(null);
121
- setDisplayFields(DEFAULT_FIELDS);
122
- }, [dispatchApi, selectedTemplate?.template_id]);
137
+ }, [
138
+ dispatchApi,
139
+ selectedTemplate?.analytic,
140
+ selectedTemplate?.detection,
141
+ selectedTemplate?.template_id,
142
+ selectedTemplate?.type,
143
+ withConfirmDeleteModal,
144
+ showSuccessMessage,
145
+ t
146
+ ]);
123
147
  const onSave = useCallback(async () => {
124
148
  if (analytic && detection) {
125
149
  try {
@@ -139,17 +163,39 @@ const TemplateViewer = () => {
139
163
  setSelectedTemplate(result);
140
164
  const newList = [result, ...templateList];
141
165
  setTemplateList(newList.filter((v1, i) => newList.findIndex(v2 => v1.template_id === v2.template_id) === i));
166
+ const updatedSessionList = sessionTemplateList.filter(v => v.analytic !== result.analytic || v.detection !== result.detection || v.type !== result.type);
167
+ setSessionTemplateList(updatedSessionList);
142
168
  }
143
169
  finally {
144
170
  setTemplateLoading(false);
145
171
  }
146
172
  }
147
- }, [analytic, detection, dispatchApi, displayFields, selectedTemplate, templateList, type]);
173
+ }, [analytic, detection, dispatchApi, displayFields, selectedTemplate, sessionTemplateList, templateList, type]);
148
174
  const analyticOrDetectionMissing = useMemo(() => !analytic || !detection, [analytic, detection]);
149
175
  const noFieldChange = useMemo(() => displayFields.length < 1 || isEqual(selectedTemplate?.keys ?? DEFAULT_FIELDS, displayFields), [displayFields, selectedTemplate?.keys]);
150
- return (_jsxs(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: [_jsx(LinearProgress, { sx: { mb: 1, opacity: +loading } }), _jsxs(Stack, { direction: "column", spacing: 2, divider: _jsx(Divider, { orientation: "horizontal", flexItem: true }), height: "100%", children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(FormControl, { sx: { flex: 1, maxWidth: '450px' }, children: _jsx(Autocomplete, { id: "analytic", options: analytics.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())), getOptionLabel: option => option.name, value: analytics.find(a => a.name === analytic) || null, onChange: (__, newValue) => setAnalytic(newValue ? newValue.name : ''), renderInput: autocompleteAnalyticParams => (_jsx(TextField, { ...autocompleteAnalyticParams, label: t('route.templates.analytic'), size: "small" })) }) }), !(detections?.length < 2 && detections[0]?.toLowerCase() === 'rule') ? (_jsx(FormControl, { sx: { flex: 1, maxWidth: '300px' }, disabled: !analytic, children: _jsx(Autocomplete, { id: "detection", options: ['ANY', ...detections.sort()], getOptionLabel: option => option, value: detection ?? '', onChange: (__, newValue) => setDetection(newValue), renderInput: autocompleteDetectionParams => (_jsx(TextField, { ...autocompleteDetectionParams, label: t('route.templates.detection'), size: "small" })) }) })) : (_jsx(Tooltip, { title: t('route.templates.rule.explanation'), children: _jsx(SsidChart, { color: "info", sx: { alignSelf: 'center' } }) })), _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr' }, size: "small", exclusive: true, value: type, disabled: analyticOrDetectionMissing, onChange: (__, _type) => {
176
+ const onTypeToggle = useCallback((_type) => {
177
+ setType(_type);
178
+ if (noFieldChange) {
179
+ return;
180
+ }
181
+ const sessionTemplate = {
182
+ analytic: analytic,
183
+ detection: detection !== 'ANY' ? detection : null,
184
+ type: type,
185
+ keys: displayFields
186
+ };
187
+ const newList = [sessionTemplate, ...sessionTemplateList];
188
+ setSessionTemplateList(newList.filter((v1, i) => newList.findIndex(v2 => v1.analytic === v2.analytic && v1.detection === v2.detection && v1.type === v2.type) === i));
189
+ }, [analytic, detection, displayFields, noFieldChange, sessionTemplateList, type]);
190
+ return (_jsxs(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: [_jsx(LinearProgress, { sx: { mb: 1, opacity: +loading } }), _jsxs(Stack, { direction: "column", spacing: 2, divider: _jsx(Divider, { orientation: "horizontal", flexItem: true }), height: "100%", children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(FormControl, { sx: { flex: 1, maxWidth: '450px' }, children: _jsx(Autocomplete, { id: "analytic", options: analytics.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())), getOptionLabel: option => option.name, value: analytics.find(a => a.name === analytic) || null, onChange: (__, newValue) => {
191
+ setAnalytic(newValue ? newValue.name : '');
192
+ setSessionTemplateList([]); // do not keep session memory if analytic or detection is changed
193
+ }, renderInput: autocompleteAnalyticParams => (_jsx(TextField, { ...autocompleteAnalyticParams, label: t('route.templates.analytic'), size: "small" })) }) }), !(detections?.length < 2 && detections[0]?.toLowerCase() === 'rule') ? (_jsx(FormControl, { sx: { flex: 1, maxWidth: '300px' }, disabled: !analytic, children: _jsx(Autocomplete, { id: "detection", options: ['ANY', ...detections.sort()], getOptionLabel: option => option, value: detection ?? '', onChange: (__, newValue) => {
194
+ setDetection(newValue);
195
+ setSessionTemplateList([]); // do not keep session memory if analytic or detection is changed
196
+ }, renderInput: autocompleteDetectionParams => (_jsx(TextField, { ...autocompleteDetectionParams, label: t('route.templates.detection'), size: "small" })) }) })) : (_jsx(Tooltip, { title: t('route.templates.rule.explanation'), children: _jsx(SsidChart, { color: "info", sx: { alignSelf: 'center' } }) })), _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr' }, size: "small", exclusive: true, value: type, disabled: analyticOrDetectionMissing, onChange: (__, _type) => {
151
197
  if (_type) {
152
- setType(_type);
198
+ onTypeToggle(_type);
153
199
  }
154
200
  }, children: [_jsx(ToggleButton, { sx: { flex: 1 }, value: "personal", "aria-label": "personal", children: t('route.templates.personal') }), _jsx(ToggleButton, { sx: { flex: 1 }, value: "global", "aria-label": "global", children: t('route.templates.global') })] }), selectedTemplate && (_jsx(Button, { variant: "outlined", startIcon: _jsx(Delete, {}), onClick: onDelete, children: t('button.delete') })), _jsx(Button, { variant: "outlined", disabled: analyticOrDetectionMissing || noFieldChange, startIcon: templateLoading ? _jsx(CircularProgress, { size: 16 }) : _jsx(Check, {}), onClick: onSave, children: t(!analyticOrDetectionMissing && !noFieldChange ? 'button.save' : 'button.saved') })] }), analyticOrDetectionMissing ? (_jsx(AppInfoPanel, { i18nKey: "route.templates.select", sx: { width: '100%', alignSelf: 'start' } })) : (_jsx(TemplateEditor, { hit: exampleHit, fields: displayFields, setFields: setDisplayFields, onAdd: field => setDisplayFields([...displayFields, field]), onRemove: field => setDisplayFields(displayFields.filter(f => f !== field)) }))] })] }));
155
201
  };
@@ -3,11 +3,14 @@ import { Article } from '@mui/icons-material';
3
3
  import { Stack, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
6
+ import { AnalyticContext } from '@cccsaurora/howler-ui/components/app/providers/AnalyticProvider';
7
+ import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
6
8
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
7
9
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
8
10
  import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
9
11
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
10
12
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
13
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
11
14
  import { useCallback, useContext, useEffect, useState } from 'react';
12
15
  import { useTranslation } from 'react-i18next';
13
16
  import { useNavigate, useSearchParams } from 'react-router-dom';
@@ -21,9 +24,11 @@ const TemplatesBase = () => {
21
24
  const [searchParams, setSearchParams] = useSearchParams();
22
25
  const { load } = useContext(TuiListMethodContext);
23
26
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
27
+ const { showSuccessMessage } = useMySnackbar();
28
+ const { analytics } = useContext(AnalyticContext);
29
+ const { response, request, remove, getSearchRequestData } = useContext(SearchResponseContext);
24
30
  const [phrase, setPhrase] = useState('');
25
31
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
26
- const [response, setResponse] = useState(null);
27
32
  const [types, setTypes] = useState([]);
28
33
  const [hasError, setHasError] = useState(false);
29
34
  const [searching, setSearching] = useState(false);
@@ -42,11 +47,11 @@ const TemplatesBase = () => {
42
47
  const phraseQuery = phrase ? `*:*${phrase}*` : '*:*';
43
48
  // Ensure the template should be visible and/or matches the type we are filtering for
44
49
  const typeQuery = `(type:global OR owner:(${user.username} OR none)) AND type:(${types.join(' ') || '*'})`;
45
- setResponse(await dispatchApi(api.search.template.post({
50
+ await request(api.search.template.post, {
46
51
  query: `${phraseQuery} AND ${typeQuery}`,
47
52
  rows: pageCount,
48
53
  offset
49
- })));
54
+ });
50
55
  }
51
56
  catch (e) {
52
57
  setHasError(true);
@@ -54,27 +59,33 @@ const TemplatesBase = () => {
54
59
  finally {
55
60
  setSearching(false);
56
61
  }
57
- }, [phrase, setSearchParams, searchParams, user.username, types, dispatchApi, pageCount, offset]);
62
+ }, [phrase, setSearchParams, searchParams, user.username, types, request, pageCount, offset]);
58
63
  // Load the items into list when response changes.
59
64
  // This hook should only trigger when the 'response' changes.
65
+ // or if the analytic list changes to refresh the disabled state
60
66
  useEffect(() => {
61
67
  if (response) {
62
68
  load(response.items.map((item) => ({
63
69
  id: item.template_id,
64
70
  item,
65
71
  selected: false,
66
- cursor: false
72
+ cursor: false,
73
+ disabled: item.detection &&
74
+ !analytics
75
+ .find(v => v.name === item.analytic)
76
+ ?.detections?.map((s) => s.toLowerCase())
77
+ ?.includes(item.detection?.toLowerCase())
67
78
  })));
68
79
  }
69
- // eslint-disable-next-line react-hooks/exhaustive-deps
70
- }, [response, load]);
80
+ }, [response, load, analytics]);
71
81
  const onPageChange = useCallback((_offset) => {
72
82
  if (_offset !== offset) {
73
- searchParams.set('offset', _offset.toString());
83
+ const modifiedRequest = getSearchRequestData({ offset: _offset });
84
+ searchParams.set('offset', modifiedRequest.offset.toString());
74
85
  setSearchParams(searchParams, { replace: true });
75
- setOffset(_offset);
86
+ setOffset(modifiedRequest.offset);
76
87
  }
77
- }, [offset, searchParams, setSearchParams]);
88
+ }, [offset, searchParams, setSearchParams, getSearchRequestData]);
78
89
  useEffect(() => {
79
90
  onSearch();
80
91
  if (!searchParams.has('offset')) {
@@ -96,14 +107,25 @@ const TemplatesBase = () => {
96
107
  }
97
108
  // eslint-disable-next-line react-hooks/exhaustive-deps
98
109
  }, [offset]);
99
- const renderer = useCallback((item, className) => _jsx(TemplateCard, { template: item, className: className }), []);
110
+ const removeTemplate = useCallback(async (templateId) => {
111
+ await dispatchApi(api.template.del(templateId), {
112
+ logError: false,
113
+ showError: true,
114
+ throwError: true
115
+ });
116
+ remove(templateId);
117
+ showSuccessMessage(t('route.templates.manager.delete.success'));
118
+ }, [dispatchApi, remove, showSuccessMessage, t]);
119
+ const renderer = useCallback((item, error, className) => (_jsx(TemplateCard, { template: item, error: error, onRemove: removeTemplate, className: className })), [removeTemplate]);
100
120
  return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, searchFilters: _jsx(Stack, { direction: "row", spacing: 1, alignItems: "center", children: _jsxs(ToggleButtonGroup, { sx: { display: 'grid', gridTemplateColumns: '1fr 1fr', alignSelf: 'start' }, size: "small", value: types, onChange: (__, _types) => {
101
121
  if (_types) {
102
122
  setTypes(_types.length < 2 ? _types : []);
103
123
  }
104
- }, children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.templates.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.templates.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.templates.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/templates/view?type=${item.item.type}&analytic=${item.item.analytic}${item.item.detection ? '&detection=' + item.item.detection : ''}`), onCreate: () => navigate('/templates/view'), createPrompt: "route.templates.create", searchPrompt: "route.templates.manager.search", createIcon: _jsx(Article, { sx: { mr: 1 } }) }));
124
+ }, children: [_jsx(ToggleButton, { value: "personal", "aria-label": "personal", children: t('route.templates.manager.personal') }), _jsx(ToggleButton, { value: "global", "aria-label": "global", children: t('route.templates.manager.global') })] }) }), aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.templates.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, !!item.disabled, classRenderer()), response: response, onSelect: (item) => {
125
+ navigate(`/templates/view?type=${item.item.type}&analytic=${item.item.analytic}${item.item.detection ? '&detection=' + item.item.detection : ''}`);
126
+ }, onCreate: () => navigate('/templates/view'), createPrompt: "route.templates.create", searchPrompt: "route.templates.manager.search", createIcon: _jsx(Article, { sx: { mr: 1 } }) }));
105
127
  };
106
128
  const Templates = () => {
107
- return (_jsx(TuiListProvider, { children: _jsx(TemplatesBase, {}) }));
129
+ return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "template_id", children: _jsx(TemplatesBase, {}) }) }));
108
130
  };
109
131
  export default Templates;
@@ -4,6 +4,8 @@ import { Clear, Edit, SavedSearch, Star, StarBorder } from '@mui/icons-material'
4
4
  import { Autocomplete, Card, Checkbox, IconButton, Skeleton, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
5
5
  import api from '@cccsaurora/howler-ui/api';
6
6
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
7
+ import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
8
+ import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
7
9
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
8
10
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
9
11
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
@@ -11,8 +13,8 @@ import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/
11
13
  import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
12
14
  import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
13
15
  import { ViewTitle } from '@cccsaurora/howler-ui/components/elements/view/ViewTitle';
14
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
15
16
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
17
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
16
18
  import { isNull, omitBy, size } from 'lodash-es';
17
19
  import React, { useCallback, useContext, useEffect, useState } from 'react';
18
20
  import { useTranslation } from 'react-i18next';
@@ -26,7 +28,8 @@ const ViewsBase = () => {
26
28
  const { t } = useTranslation();
27
29
  const { user } = useAppUser();
28
30
  const navigate = useNavigate();
29
- const { dispatchApi } = useMyApi();
31
+ const { withConfirmDeleteModal } = useContext(ModalContext);
32
+ const { showSuccessMessage } = useMySnackbar();
30
33
  const fetchViews = useContextSelector(ViewContext, ctx => ctx.fetchViews);
31
34
  const addFavourite = useContextSelector(ViewContext, ctx => ctx.addFavourite);
32
35
  const removeFavourite = useContextSelector(ViewContext, ctx => ctx.removeFavourite);
@@ -37,9 +40,9 @@ const ViewsBase = () => {
37
40
  const [searchParams, setSearchParams] = useSearchParams();
38
41
  const { load } = useContext(TuiListMethodContext);
39
42
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
43
+ const { response, request, remove, getSearchRequestData } = useContext(SearchResponseContext);
40
44
  const [phrase, setPhrase] = useState('');
41
45
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
42
- const [response, setResponse] = useState(null);
43
46
  const [type, setType] = useState(searchParams.get('type') || null);
44
47
  const [hasError, setHasError] = useState(false);
45
48
  const [searching, setSearching] = useState(false);
@@ -61,11 +64,11 @@ const ViewsBase = () => {
61
64
  const phraseQuery = FIELDS_TO_SEARCH.map(_field => `${_field}:${searchTerm}`).join(' OR ');
62
65
  const typeQuery = `(type:global OR owner:(${user.username} OR none)) AND type:(${type ?? '*'}${type === 'personal' ? ' OR readonly' : ''})`;
63
66
  const favouritesQuery = favouritesOnly && user.favourite_views.length > 0 ? ` AND view_id:(${user.favourite_views.join(' OR ')})` : '';
64
- setResponse(await dispatchApi(api.search.view.post({
67
+ await request(api.search.view.post, {
65
68
  query: `(${phraseQuery}) AND ${typeQuery}${favouritesQuery}`,
66
69
  rows: pageCount,
67
70
  offset
68
- })));
71
+ });
69
72
  }
70
73
  catch (e) {
71
74
  setHasError(true);
@@ -81,7 +84,7 @@ const ViewsBase = () => {
81
84
  user.favourite_views,
82
85
  type,
83
86
  favouritesOnly,
84
- dispatchApi,
87
+ request,
85
88
  pageCount,
86
89
  offset
87
90
  ]);
@@ -100,17 +103,21 @@ const ViewsBase = () => {
100
103
  }, [response, load]);
101
104
  const onPageChange = useCallback((_offset) => {
102
105
  if (_offset !== offset) {
103
- searchParams.set('offset', _offset.toString());
106
+ const modifiedRequest = getSearchRequestData({ offset: _offset });
107
+ searchParams.set('offset', modifiedRequest.offset.toString());
104
108
  setSearchParams(searchParams, { replace: true });
105
- setOffset(_offset);
109
+ setOffset(modifiedRequest.offset);
106
110
  }
107
- }, [offset, searchParams, setSearchParams]);
108
- const onDelete = useCallback(async (event, id) => {
111
+ }, [offset, searchParams, setSearchParams, getSearchRequestData]);
112
+ const onDelete = useCallback((event, id) => {
109
113
  event.preventDefault();
110
114
  event.stopPropagation();
111
- await removeView(id);
112
- onSearch();
113
- }, [onSearch, removeView]);
115
+ withConfirmDeleteModal(async () => {
116
+ await removeView(id);
117
+ remove(id);
118
+ showSuccessMessage(t('route.views.manager.delete.success'));
119
+ });
120
+ }, [remove, removeView, withConfirmDeleteModal, showSuccessMessage, t]);
114
121
  const onFavourite = useCallback(async (event, id) => {
115
122
  event.preventDefault();
116
123
  if (user.favourite_views?.includes(id)) {
@@ -178,6 +185,6 @@ const ViewsBase = () => {
178
185
  (item.item.type === 'global' && user.is_admin)) && (_jsx(Tooltip, { title: t('button.edit'), children: _jsx(IconButton, { component: Link, to: `/views/${item.item.view_id}/edit?query=${item.item.query}`, children: _jsx(Edit, {}) }) })), item.item.owner === user.username && item.item.type !== 'readonly' && (_jsx(Tooltip, { title: t('button.delete'), children: _jsx(IconButton, { onClick: event => onDelete(event, item.item.view_id), children: _jsx(Clear, {}) }) })), item.item.type === 'global' && item.item.owner !== user.username && (_jsx(Tooltip, { title: item.item.owner, children: _jsx("div", { children: _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important', marginLeft: '8px !important' }, userId: item.item.owner }) }) })), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { onClick: e => onFavourite(e, item.item.view_id), children: user.favourite_views?.includes(item.item.view_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }, item.item.view_id)), response: response, searchPrompt: "route.views.manager.search", onCreate: () => navigate('/views/create'), createPrompt: "route.views.create", createIcon: _jsx(SavedSearch, { sx: { mr: 1 } }) }));
179
186
  };
180
187
  const Views = () => {
181
- return (_jsx(TuiListProvider, { children: _jsx(ViewsBase, {}) }));
188
+ return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "view_id", children: _jsx(ViewsBase, {}) }) }));
182
189
  };
183
190
  export default Views;