diff --git a/backend/modules.go b/backend/modules.go index 633627f6d..4e5e94c8f 100644 --- a/backend/modules.go +++ b/backend/modules.go @@ -162,11 +162,6 @@ func initModules(db *gorm.DB, cfg *config) *modules { } datasourcesMod := datasources.NewModule(dsUC, dsGroupUC, dsReconciler, billingMod.License(), agentClient) - integrationsMod := integrations.NewModule(db, cipher, - env.String("INTEGRATIONS_TENANT_DIR", "/workdir/pipeline", false), - dsUC, - ) - opensearchMod := opensearchgw.NewModule(db, cfg.esHost != "") notificationsMod := notifications.NewModule(db, auditMod.Logger()) @@ -184,6 +179,7 @@ func initModules(db *gorm.DB, cfg *config) *modules { ) } + iamMod := iam.NewModule(authUsecase, userUsecase, roleUsecase, tfaUsecase, apiKeyUsecase, idpUsecase, samlUsecase, cfg.uploadDir) socAIMod := socai.NewModule(cfg.socAIBaseURL, cfg.internalKey, cipher, env.String("INTEGRATIONS_TENANT_DIR", "/workdir/pipeline", false)) @@ -196,6 +192,13 @@ func initModules(db *gorm.DB, cfg *config) *modules { ) adauditMod := adaudit.NewModule(db) + //loaded after opensearch has fully loaded + integrationsMod := integrations.NewModule(db, cipher, + env.String("INTEGRATIONS_TENANT_DIR", "/workdir/pipeline", false), + dsUC, + opensearchMod.GetIndexPatternUsecase(), + ) + var mcpModule *mcpmod.Module if cfg.mcpEnabled { mcpModule = mcpmod.NewModule(&mcpmod.Deps{ diff --git a/backend/modules/integrations/module.go b/backend/modules/integrations/module.go index 09630de4b..58edef70e 100644 --- a/backend/modules/integrations/module.go +++ b/backend/modules/integrations/module.go @@ -5,6 +5,7 @@ import ( "github.com/threatwinds/go-sdk/catcher" ds_connectors "github.com/utmstack/utmstack/backend/modules/datasources/connectors" + os_connectors "github.com/utmstack/utmstack/backend/modules/opensearch/connectors" "github.com/utmstack/utmstack/backend/modules/integrations/connectors" "github.com/utmstack/utmstack/backend/modules/integrations/repository" "github.com/utmstack/utmstack/backend/modules/integrations/usecase" @@ -17,16 +18,23 @@ type Module struct { tenants *usecase.TenantUsecase modules connectors.ModuleUsecase bootstrap *usecase.Bootstrap + opensearch os_connectors.IndexPatternUsecase } -func NewModule(db *gorm.DB, cipher connectors.Cipher, tenantDir string, datasources ds_connectors.DatasourceUsecase) *Module { +func NewModule( + db *gorm.DB, + cipher connectors.Cipher, + tenantDir string, + datasources ds_connectors.DatasourceUsecase, + opensearch os_connectors.IndexPatternUsecase, +) *Module { store := repository.NewTenantStore(tenantDir) schema := repository.NewCodeSchemaProvider() // field schema lives in code verif := verifier.NewBackendVerifier() moduleRepo := repository.NewModuleRepository(db) // store doubles as the tenant-file toggler: enable/disable .yaml(.disabled). - moduleUC := usecase.NewModuleUsecase(moduleRepo, store) + moduleUC := usecase.NewModuleUsecase(moduleRepo, store,opensearch) return &Module{ db: db, diff --git a/backend/modules/integrations/usecase/module.go b/backend/modules/integrations/usecase/module.go index ea581cbc4..b1507e7a5 100644 --- a/backend/modules/integrations/usecase/module.go +++ b/backend/modules/integrations/usecase/module.go @@ -3,24 +3,30 @@ package usecase import ( "context" "fmt" - + "github.com/threatwinds/go-sdk/catcher" "github.com/utmstack/utmstack/backend/modules/integrations/connectors" "github.com/utmstack/utmstack/backend/modules/integrations/domain" "github.com/utmstack/utmstack/backend/modules/integrations/dto" + + os_connectors "github.com/utmstack/utmstack/backend/modules/opensearch/connectors" + os_dto "github.com/utmstack/utmstack/backend/modules/opensearch/dto" ) type moduleUsecase struct { repo connectors.ModuleRepository tenantFileTog connectors.TenantFileToggler + opensearch os_connectors.IndexPatternUsecase } func NewModuleUsecase( repo connectors.ModuleRepository, tenantFile connectors.TenantFileToggler, + opensearch os_connectors.IndexPatternUsecase, ) connectors.ModuleUsecase { return &moduleUsecase{ repo: repo, tenantFileTog: tenantFile, + opensearch: opensearch, } } @@ -51,6 +57,32 @@ func (u *moduleUsecase) ActivateDeactivate(ctx context.Context, req dto.ModuleAc return nil, err } + results,count,err :=u.opensearch.List(ctx,os_dto.IndexPatternFilters{ + ModuleEquals: &module.ModuleName, + }) + if err != nil { + return nil,err + } + if count > 0 && len(results)>0 { + //if module has index patterns + pattern:=results[0] + + if _,err := u.opensearch.Update(ctx,os_dto.UpdateIndexPatternRequest{ + ID: pattern.ID, + Pattern: pattern.Pattern, + PatternModule: pattern.PatternModule, + IsActive: &fresh.ModuleActive, + PatternSystem:pattern.PatternSystem, + });err!=nil{ + catcher.Error("failed to toggle is active for module's index pattern, rolling back", err, map[string]any{"module":module.ModuleName,"pattern":pattern} ) + module.ModuleActive = !active + if err_ := u.repo.Save(ctx, module); err_ != nil { + catcher.Error("failed to change module status after index pattern activation toggle", err, map[string]any{"module":module.ModuleName,"pattern":pattern} ) + return nil, err + } + return nil,err + } + } resp := dto.FromModule(*fresh) return &resp, nil } @@ -128,9 +160,22 @@ func (u *moduleUsecase) Create(ctx context.Context, req dto.CreateModuleRequest) ModuleActive: false, IsSystem: false, } + + if err := u.repo.Save(ctx, m); err != nil { return nil, err } + + if _,err:= u.opensearch.Create(ctx,os_dto.CreateIndexPatternRequest{ + PatternModule: &req.ModuleName, + });err!=nil{ + //opensearch insertion fails, rolling back database + if err:=u.repo.Delete(ctx,m.ID);err!=nil{ + catcher.Error("failed to remove module after index pattern creation fail", err, map[string]any{"module":m.ModuleName} ) + } + return nil, err + } + resp := dto.FromModule(*m) return &resp, nil } @@ -163,5 +208,30 @@ func (u *moduleUsecase) Delete(ctx context.Context, id int64) error { if m.IsSystem { return domain.ErrSystemModule } - return u.repo.Delete(ctx, id) + + results,count,err :=u.opensearch.List(ctx,os_dto.IndexPatternFilters{ + ModuleEquals: &m.ModuleName, + }) + if err != nil { + return err + } + if count == 0 || len(results)==0{ + return fmt.Errorf("module %s index pattern not found.",m.ModuleName) + } + + err= u.repo.Delete(ctx, id) + if err!=nil{ + return err + } + + pattern:=results[0] + if err:=u.opensearch.Delete(ctx,pattern.ID);err!=nil{ + //if index patter deletion fails integration must be restored + if err:=u.repo.Save(ctx,m);err!=nil{ + catcher.Error("failed to restore module after index pattern remove fail", err, map[string]any{"module":m.ModuleName} ) + } + return err + } + + return nil } diff --git a/backend/modules/opensearch/connectors/repository.go b/backend/modules/opensearch/connectors/repository.go index e03032fca..71218e503 100644 --- a/backend/modules/opensearch/connectors/repository.go +++ b/backend/modules/opensearch/connectors/repository.go @@ -12,6 +12,7 @@ type GatewayRepository interface { PropertyValuesWithCount(ctx context.Context, index, field string, filters map[string]any, top int, orderByCount, sortAsc bool) (map[string]int64, error) IndexProperties(ctx context.Context, pattern string) ([]dto.IndexPropertyType, error) Indices(ctx context.Context, pattern string) ([]dto.IndexInfo, error) + EnsureIndexPattern(ctx context.Context, pattern string) error DeleteIndices(ctx context.Context, indices []string) error Search(ctx context.Context, index string, query map[string]any, from, size int, sortField, sortOrder string) ([]map[string]any, int64, error) Count(ctx context.Context, index string, query map[string]any) (int64, error) diff --git a/backend/modules/opensearch/repository/index_pattern_pg.go b/backend/modules/opensearch/repository/index_pattern_pg.go index 6fdc404f7..0847a7f13 100644 --- a/backend/modules/opensearch/repository/index_pattern_pg.go +++ b/backend/modules/opensearch/repository/index_pattern_pg.go @@ -19,6 +19,11 @@ func NewIndexPatternRepository(db *gorm.DB) connectors.IndexPatternRepository { } func (r *pgIndexPatternRepository) Create(ctx context.Context, p *domain.UtmIndexPattern) error { + sanitized, err := ensureIndexPattern(ctx, p.Pattern) + if err != nil { + return err + } + p.Pattern = sanitized return r.db.WithContext(ctx).Create(p).Error } diff --git a/backend/modules/opensearch/repository/opensearch_gateway.go b/backend/modules/opensearch/repository/opensearch_gateway.go index 2efd2120e..a44b377c6 100644 --- a/backend/modules/opensearch/repository/opensearch_gateway.go +++ b/backend/modules/opensearch/repository/opensearch_gateway.go @@ -3,7 +3,10 @@ package repository import ( "context" "encoding/json" + "errors" "fmt" + "net/http" + "regexp" "sort" "strconv" "strings" @@ -11,8 +14,22 @@ import ( osdk "github.com/threatwinds/go-sdk/os" "github.com/utmstack/utmstack/backend/modules/opensearch/connectors" "github.com/utmstack/utmstack/backend/modules/opensearch/dto" + "github.com/utmstack/utmstack/backend/pkg/constants" ) +var ( + reInvalidPatternChars = regexp.MustCompile(`[^a-z0-9_\-*]+`) + reCollapsePatternDash = regexp.MustCompile(`-+`) +) + +func sanitizePattern(pattern string) string { + s := strings.ToLower(strings.TrimSpace(pattern)) + s = reInvalidPatternChars.ReplaceAllString(s, "-") + s = reCollapsePatternDash.ReplaceAllString(s, "-") + s = strings.Trim(s, "-_") + return s +} + type osGatewayRepo struct { mapper *osdk.FieldMapper } @@ -135,6 +152,60 @@ func (r *osGatewayRepo) Indices(ctx context.Context, pattern string) ([]dto.Inde return out, nil } +func (r *osGatewayRepo) EnsureIndexPattern(ctx context.Context, pattern string) error { + _, err := ensureIndexPattern(ctx, pattern) + return err +} + +func ensureIndexPattern(ctx context.Context, pattern string) (string, error) { + sanitized := sanitizePattern(pattern) + if sanitized == "" || sanitized == "*" { + return "", errors.New("opensearch: pattern is empty") + } + name := templateNameFor(sanitized) + if name == "" { + return "", fmt.Errorf("opensearch: pattern %q has no valid template name", sanitized) + } + path := "_index_template/" + name + + _, status, err := osdk.DoRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return "", err + } + if status == http.StatusOK { + return sanitized, nil + } + if status != http.StatusNotFound { + return "", fmt.Errorf("opensearch: probe %q: status %d", path, status) + } + + body := map[string]any{ + "index_patterns": []string{sanitized}, + "priority": 0, + "template": map[string]any{ + "settings": map[string]any{ + "index.mapping.total_fields.limit": constants.OS_INDEX_FIELD_LIMIT, + "number_of_shards": constants.OS_INDEX_NUMBER_OF_SHARDS, + "number_of_replicas": constants.OS_INDEX_NUMBER_OF_REPLICAS, + }, + }, + } + data, status, err := osdk.DoRequest(ctx, http.MethodPut, path, body) + if err != nil { + return "", err + } + if status < 200 || status >= 300 { + return "", fmt.Errorf("opensearch: create index pattern %q: status %d: %s", sanitized, status, string(data)) + } + return sanitized, nil +} + +func templateNameFor(pattern string) string { + n := strings.ReplaceAll(pattern, "*", "") + n = strings.Trim(n, "-_.") + return n +} + func (r *osGatewayRepo) DeleteIndices(ctx context.Context, indices []string) error { for _, idx := range indices { if err := osdk.DeleteIndex(ctx, idx); err != nil { diff --git a/backend/modules/opensearch/repository/opensearch_ism.go b/backend/modules/opensearch/repository/opensearch_ism.go index e40705c67..cd9f960af 100644 --- a/backend/modules/opensearch/repository/opensearch_ism.go +++ b/backend/modules/opensearch/repository/opensearch_ism.go @@ -9,6 +9,7 @@ import ( osdk "github.com/threatwinds/go-sdk/os" "github.com/utmstack/utmstack/backend/modules/opensearch/domain" "github.com/utmstack/utmstack/backend/modules/opensearch/dto" + "github.com/utmstack/utmstack/backend/pkg/constants" ) // ISMClient performs Index State Management (and snapshot/mapping) calls against @@ -78,8 +79,6 @@ func (c *ISMClient) UpdatePolicy(ctx context.Context, policy domain.IndexPolicy, return nil } -const fieldLimit = 50000 - func (c *ISMClient) EnsureFieldLimitTemplate(ctx context.Context, patterns []string) error { if len(patterns) == 0 { return nil @@ -90,9 +89,9 @@ func (c *ISMClient) EnsureFieldLimitTemplate(ctx context.Context, patterns []str "priority": 0, "template": map[string]any{ "settings": map[string]any{ - "index.mapping.total_fields.limit": fieldLimit, - "number_of_shards": 3, - "number_of_replicas": 0, + "index.mapping.total_fields.limit": constants.OS_INDEX_FIELD_LIMIT, + "number_of_shards": constants.OS_INDEX_NUMBER_OF_SHARDS, + "number_of_replicas": constants.OS_INDEX_NUMBER_OF_REPLICAS, }, }, } @@ -108,7 +107,7 @@ func (c *ISMClient) EnsureFieldLimitTemplate(ctx context.Context, patterns []str func (c *ISMClient) RaiseFieldLimit(ctx context.Context, pattern string) error { path := fmt.Sprintf("%s/_settings", pattern) - body := map[string]any{"index.mapping.total_fields.limit": fieldLimit} + body := map[string]any{"index.mapping.total_fields.limit": constants.OS_INDEX_FIELD_LIMIT} data, status, err := c.do(ctx, http.MethodPut, path, body) if err != nil { return err diff --git a/backend/modules/opensearch/usecase/index_pattern.go b/backend/modules/opensearch/usecase/index_pattern.go index 836a83649..193fdaf5f 100644 --- a/backend/modules/opensearch/usecase/index_pattern.go +++ b/backend/modules/opensearch/usecase/index_pattern.go @@ -3,14 +3,18 @@ package usecase import ( "context" "errors" + "fmt" + "strings" "github.com/threatwinds/go-sdk/catcher" "github.com/utmstack/utmstack/backend/modules/opensearch/connectors" "github.com/utmstack/utmstack/backend/modules/opensearch/domain" "github.com/utmstack/utmstack/backend/modules/opensearch/dto" + "github.com/utmstack/utmstack/backend/pkg/constants" ) var errIDMustBeAbsent = errors.New("id must be absent on create") +var errInvalidPattern = errors.New("invalid pattern") type indexPatternUsecase struct { repo connectors.IndexPatternRepository @@ -36,6 +40,15 @@ func (u *indexPatternUsecase) Create(ctx context.Context, req dto.CreateIndexPat } systemFalse := false + if req.Pattern == "" && req.PatternModule != nil { + normalized := strings.ReplaceAll(strings.Trim(strings.ToLower(*req.PatternModule), ""), " ", "-") + req.Pattern = fmt.Sprintf("%s-%s-*", constants.CUSTOM_INDEX_PREFIX, normalized) + } + + if req.Pattern == "" || req.Pattern == " " { + return nil, errInvalidPattern + } + p := &domain.UtmIndexPattern{ Pattern: req.Pattern, PatternModule: req.PatternModule, diff --git a/backend/pkg/constants/custom_integrations.go b/backend/pkg/constants/custom_integrations.go new file mode 100644 index 000000000..b6386cbde --- /dev/null +++ b/backend/pkg/constants/custom_integrations.go @@ -0,0 +1,5 @@ +package constants + +const( + CUSTOM_INDEX_PREFIX = "custom"; +) diff --git a/backend/pkg/constants/opensearch_index.go b/backend/pkg/constants/opensearch_index.go new file mode 100644 index 000000000..c138c686b --- /dev/null +++ b/backend/pkg/constants/opensearch_index.go @@ -0,0 +1,7 @@ +package constants + +const ( + OS_INDEX_FIELD_LIMIT = 50000 + OS_INDEX_NUMBER_OF_SHARDS = 3 + OS_INDEX_NUMBER_OF_REPLICAS = 0 +) diff --git a/backend/pkg/database/pagination.go b/backend/pkg/database/pagination.go index 90b446c6e..7fb176cbc 100644 --- a/backend/pkg/database/pagination.go +++ b/backend/pkg/database/pagination.go @@ -15,9 +15,12 @@ func (p Params) Normalized() (page, size int) { if page < 0 { page = 0 } - if size < 1 || size > MaxPageSize { + if size < 1 { size = DefaultPageSize } + if size > MaxPageSize { + size = MaxPageSize + } return page, size } diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf index 9067a5f1f..8cc97e6c6 100644 --- a/frontend/nginx/default.conf +++ b/frontend/nginx/default.conf @@ -20,7 +20,7 @@ server { } location /integrations/ { - try_files $uri /index.html; + try_files $uri $uri/ /index.html; } location / { diff --git a/frontend/src/features/log-explorer/components/IndexPatternSelector.tsx b/frontend/src/features/log-explorer/components/IndexPatternSelector.tsx new file mode 100644 index 000000000..e300461a1 --- /dev/null +++ b/frontend/src/features/log-explorer/components/IndexPatternSelector.tsx @@ -0,0 +1,99 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ChevronDown, Database, Search } from 'lucide-react' +import { cn } from '@/shared/lib/utils' +import { Input } from '@/shared/components/ui/input' +import type { IndexPattern } from '../types/log-explorer.types' + +interface IndexPatternSelectorProps { + patterns: IndexPattern[] + pattern: IndexPattern | null + onPattern: (p: IndexPattern) => void +} + +export function IndexPatternSelector({ patterns, pattern, onPattern }: IndexPatternSelectorProps) { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + const [query, setQuery] = useState('') + const ref = useRef(null) + + useEffect(() => { + if (!open) { + setQuery('') + return + } + const onDoc = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) + } + document.addEventListener('mousedown', onDoc) + return () => document.removeEventListener('mousedown', onDoc) + }, [open]) + + const filtered = useMemo( + () => + query.trim() + ? patterns.filter((p) => p.pattern.toLowerCase().includes(query.trim().toLowerCase())) + : patterns, + [patterns, query] + ) + + return ( +
+ + {open && ( +
+
+ {t('logExplorer.query.indexPatterns')} +
+ {patterns.length > 0 && ( +
+
+ + setQuery(e.target.value)} + placeholder={t('logExplorer.query.filterPatterns')} + className="h-8 pl-8 text-xs" + autoFocus + /> +
+
+ )} +
+ {patterns.length === 0 ? ( +
{t('logExplorer.query.noPatterns')}
+ ) : filtered.length === 0 ? ( +
{t('logExplorer.query.noPatternsMatch')}
+ ) : ( + filtered.map((p) => ( + + )) + )} +
+
+ )} +
+ ) +} diff --git a/frontend/src/features/log-explorer/components/LogExplorerView.tsx b/frontend/src/features/log-explorer/components/LogExplorerView.tsx index 36a3eb5ab..5f500d305 100644 --- a/frontend/src/features/log-explorer/components/LogExplorerView.tsx +++ b/frontend/src/features/log-explorer/components/LogExplorerView.tsx @@ -5,11 +5,9 @@ import { Braces, Calendar, Check, - ChevronDown, ChevronRight, Columns3, Code2, - Database, Download, Bookmark, Filter, @@ -38,6 +36,7 @@ import { Button } from '@/shared/components/ui/button' import { Input } from '@/shared/components/ui/input' import { TimeRangePicker, presetRange, type TimeRange } from '@/shared/components/ui/time-range-picker' import { ResultsHeader, ResultRow, flattenDoc } from './log-results' +import { IndexPatternSelector } from './IndexPatternSelector' import { logExplorerHttpService as svc, LogExplorerHttpError, @@ -601,44 +600,10 @@ function QueryBar({ onExport: () => void }) { const { t } = useTranslation() - const [patternOpen, setPatternOpen] = useState(false) return (
- {/* Index pattern selector */} - - - {pattern?.pattern ?? '—'} - - - } - > -
- {t('logExplorer.query.indexPatterns')} -
- {patterns.length === 0 && ( -
{t('logExplorer.query.noPatterns')}
- )} - {patterns.map((p) => ( - - ))} -
+
@@ -701,54 +666,6 @@ function QueryBar({ ) } -function Dropdown({ - open, - onOpenChange, - trigger, - align = 'left', - children, -}: { - open: boolean - onOpenChange: (v: boolean) => void - trigger: React.ReactNode - align?: 'left' | 'right' - children: React.ReactNode -}) { - const ref = useRef(null) - useEffect(() => { - if (!open) return - const onDoc = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) onOpenChange(false) - } - document.addEventListener('mousedown', onDoc) - return () => document.removeEventListener('mousedown', onDoc) - }, [open, onOpenChange]) - - return ( -
- - {open && ( -
- {children} -
- )} -
- ) -} - /* ─── Active filter chips ──────────────────────────────────────────────── */ const OP_KEY: Record = { diff --git a/frontend/src/features/log-explorer/services/log-explorer-http.service.ts b/frontend/src/features/log-explorer/services/log-explorer-http.service.ts index 2e78676b3..72a669cfd 100644 --- a/frontend/src/features/log-explorer/services/log-explorer-http.service.ts +++ b/frontend/src/features/log-explorer/services/log-explorer-http.service.ts @@ -19,7 +19,7 @@ export const logExplorerHttpService = { // Active index patterns for the selector (few — fetch all). patterns: async (): Promise => { const { data } = await api.getPaged( - '/opensearch/index-patterns?isActive.equals=true&page=0&size=200&sort=pattern,asc' + '/opensearch/index-patterns?isActive.equals=true&page=0&size=2000&sort=pattern,asc' ) return data ?? [] }, diff --git a/frontend/src/shared/i18n/locales/de.json b/frontend/src/shared/i18n/locales/de.json index db2a6c12e..e72b520e3 100644 --- a/frontend/src/shared/i18n/locales/de.json +++ b/frontend/src/shared/i18n/locales/de.json @@ -1554,6 +1554,8 @@ "query": { "indexPatterns": "Index patterns", "noPatterns": "Keine Patterns", + "noPatternsMatch": "Keine passenden Patterns", + "filterPatterns": "Patterns filtern…", "searchPlaceholder": "In allen Feldern suchen…", "refresh": "Aktualisieren", "exportCsv": "CSV exportieren", diff --git a/frontend/src/shared/i18n/locales/en.json b/frontend/src/shared/i18n/locales/en.json index e2a90f8a8..e8534eda1 100644 --- a/frontend/src/shared/i18n/locales/en.json +++ b/frontend/src/shared/i18n/locales/en.json @@ -1554,6 +1554,8 @@ "query": { "indexPatterns": "Index patterns", "noPatterns": "No patterns", + "noPatternsMatch": "No matching patterns", + "filterPatterns": "Filter patterns…", "searchPlaceholder": "Search across all fields…", "refresh": "Refresh", "exportCsv": "Export CSV", diff --git a/frontend/src/shared/i18n/locales/es.json b/frontend/src/shared/i18n/locales/es.json index 25c42cf6a..fe32ff3fb 100644 --- a/frontend/src/shared/i18n/locales/es.json +++ b/frontend/src/shared/i18n/locales/es.json @@ -1554,6 +1554,8 @@ "query": { "indexPatterns": "Index patterns", "noPatterns": "Sin patterns", + "noPatternsMatch": "Sin patterns coincidentes", + "filterPatterns": "Filtrar patterns…", "searchPlaceholder": "Buscar en todos los campos…", "refresh": "Actualizar", "exportCsv": "Exportar CSV", diff --git a/frontend/src/shared/i18n/locales/fr.json b/frontend/src/shared/i18n/locales/fr.json index 07dc3314a..651d3f188 100644 --- a/frontend/src/shared/i18n/locales/fr.json +++ b/frontend/src/shared/i18n/locales/fr.json @@ -1554,6 +1554,8 @@ "query": { "indexPatterns": "Index patterns", "noPatterns": "Aucun pattern", + "noPatternsMatch": "Aucun pattern correspondant", + "filterPatterns": "Filtrer les patterns…", "searchPlaceholder": "Rechercher dans tous les champs…", "refresh": "Actualiser", "exportCsv": "Exporter CSV", diff --git a/frontend/src/shared/i18n/locales/it.json b/frontend/src/shared/i18n/locales/it.json index fc47f5731..4dde33313 100644 --- a/frontend/src/shared/i18n/locales/it.json +++ b/frontend/src/shared/i18n/locales/it.json @@ -1554,6 +1554,8 @@ "query": { "indexPatterns": "Index patterns", "noPatterns": "Nessun pattern", + "noPatternsMatch": "Nessun pattern corrispondente", + "filterPatterns": "Filtra pattern…", "searchPlaceholder": "Cerca in tutti i campi…", "refresh": "Aggiorna", "exportCsv": "Esporta CSV", diff --git a/frontend/src/shared/i18n/locales/pt.json b/frontend/src/shared/i18n/locales/pt.json index 7d1532564..6ecc5fb01 100644 --- a/frontend/src/shared/i18n/locales/pt.json +++ b/frontend/src/shared/i18n/locales/pt.json @@ -1554,6 +1554,8 @@ "query": { "indexPatterns": "Index patterns", "noPatterns": "Sem patterns", + "noPatternsMatch": "Sem patterns correspondentes", + "filterPatterns": "Filtrar patterns…", "searchPlaceholder": "Buscar em todos os campos…", "refresh": "Atualizar", "exportCsv": "Exportar CSV", diff --git a/frontend/src/shared/i18n/locales/ru.json b/frontend/src/shared/i18n/locales/ru.json index 384f5a414..33e2ec797 100644 --- a/frontend/src/shared/i18n/locales/ru.json +++ b/frontend/src/shared/i18n/locales/ru.json @@ -1450,6 +1450,8 @@ "query": { "indexPatterns": "Index patterns", "noPatterns": "Нет patterns", + "noPatternsMatch": "Нет подходящих patterns", + "filterPatterns": "Фильтр patterns…", "searchPlaceholder": "Поиск по всем полям…", "refresh": "Обновить", "exportCsv": "Экспорт CSV",