diff --git a/core/api-server/README.md b/core/api-server/README.md index 0483c9d7d..f740adafb 100644 --- a/core/api-server/README.md +++ b/core/api-server/README.md @@ -248,7 +248,9 @@ INPUT "to": "2021-01-19T20:00:00Z", "entity" :"module", "entity_name": "traefik1", - "timezone": "Europe/Rome" + "timezone": "Europe/Rome", + "instance": "loki1", + "regexp": false } } ``` @@ -264,6 +266,8 @@ INPUT - `entity`: must be `cluster` or `node` or `module` - `string` - `entity_name`: could be empty (`cluster` case) or name of the entity - `string` (ex. hostname of the node or module id like `traefik1`) - `timezone`: could be empty (default UTC) or a specific valid timezone (eg. Europe/Rome) + - `instance`: could be empty (default loki instance) or the id of a specific loki instance - `string` (ex. `loki1`) + - `regexp`: could be empty (default `false`) - `bool` (when `true` the `filter` is a RE2 regular expression instead of a substring) ```json OUTPUT @@ -282,6 +286,7 @@ OUTPUT - `payload`: contains the response logs - `message`: contains the log message - `pid`: is the pid of the process that actually reads log + - a failed query has no attribute of its own: the `logcli` error text is sent as `message`, and in `tail` mode a `logs-stop` follows it - `timestamp`: timestamp of the action - `type`: used to identify the websocket outputs @@ -384,15 +389,17 @@ Use "api-server-logs [command] --help" for more information about a command. api-server-logs logs [flags] Flags: - -e, --entity string get logs for a specific entity: cluster, node, module (default "cluster") - -f, --from string get logs from a specific date. ISO8601 format - -h, --help help for logs - -l, --lines string get logs for a specific lines in dump mode (default "25") - -m, --mode string get logs in a specific mode: tail or dump (default "tail") - -n, --name string get logs for a specific entity name. used in node or module - -s, --search string get logs for a specific search string - -t, --to string get logs to a specific date. ISO8601 format - -z, --timezone string get logs in a specific timezone + -e, --entity string get logs for a specific entity: cluster, node, module (default "cluster") + -f, --from string get logs from a specific date. ISO8601 format + -h, --help help for logs + -i, --instance string search for logs in a specific instance. (Example: loki1, loki2, ...) + -l, --lines string get logs for a specific lines in dump mode (default "25") + -m, --mode string get logs in a specific mode: tail or dump (default "tail") + -n, --name string get logs for a specific entity name. used in node or module + -r, --regexp treat the search string as a regular expression instead of a substring + -s, --search string get logs for a specific search string + -z, --timezone string get logs in a specific timezone + -t, --to string get logs to a specific date. ISO8601 format ``` - `version`: prints the command version diff --git a/core/api-server/api-server-logs.go b/core/api-server/api-server-logs.go index 3eb309fc6..0a962d14e 100644 --- a/core/api-server/api-server-logs.go +++ b/core/api-server/api-server-logs.go @@ -25,10 +25,7 @@ package main import ( "github.com/NethServer/ns8-core/core/api-server/models" "github.com/NethServer/ns8-core/core/api-server/socket" - "github.com/NethServer/ns8-core/core/api-server/utils" - "github.com/pkg/errors" - "encoding/json" "fmt" "os" "sync" @@ -48,6 +45,7 @@ var ( searchFlag = "" timezone = "" instance = "" + regexpFlag = false ) var RootCmd = &cobra.Command{ @@ -86,6 +84,7 @@ func Execute() { LogsCmd.Flags().StringVarP(&searchFlag, "search", "s", "", "get logs for a specific search string") LogsCmd.Flags().StringVarP(&timezone, "timezone", "z", "", "get logs in a specific timezone") LogsCmd.Flags().StringVarP(&instance, "instance", "i", "", "search for logs in a specific instance. (Example: loki1, loki2, ...)") + LogsCmd.Flags().BoolVarP(®expFlag, "regexp", "r", false, "treat the search string as a regular expression instead of a substring") // check errors on cmd execution if err := RootCmd.Execute(); err != nil { @@ -99,29 +98,22 @@ func Logs() { var wg sync.WaitGroup wg.Add(1) - // define payload - payload := ` - { - "action": "logs-start", - "payload": { - "id": "` + uuid.New().String() + `", - "mode": "` + modeFlag + `", - "lines": "` + linesFlag + `", - "filter": "` + searchFlag + `", - "from": "` + fromFlag + `", - "to": "` + toFlag + `", - "entity": "` + entityFlag + `", - "entity_name": "` + entityNameFlag + `", - "timezone": "` + timezone + `", - "instance": "` + instance + `" - } - } - ` - // init command to execute - var action models.SocketAction - if errAction := json.Unmarshal([]byte(payload), &action); errAction != nil { - utils.LogError(errors.Wrap(errAction, "[LOG-CLI] error in Action json unmarshal")) + action := models.SocketAction{ + Action: "logs-start", + Payload: models.LogsStartAction{ + Id: uuid.New().String(), + Mode: modeFlag, + Lines: linesFlag, + Filter: searchFlag, + From: fromFlag, + To: toFlag, + Entity: entityFlag, + EntityName: entityNameFlag, + TimeZone: timezone, + Instance: instance, + Regexp: regexpFlag, + }, } // execute command diff --git a/core/api-server/models/socket.go b/core/api-server/models/socket.go index 9230b1bd7..a7e1862be 100644 --- a/core/api-server/models/socket.go +++ b/core/api-server/models/socket.go @@ -38,6 +38,7 @@ type LogsStartAction struct { EntityName string `json:"entity_name" structs:"entity_name"` TimeZone string `json:"timezone" structs:"timezone"` Instance string `json:"instance" structs:"instance"` + Regexp bool `json:"regexp" structs:"regexp"` } type LogsStopAction struct { diff --git a/core/api-server/socket/action.go b/core/api-server/socket/action.go index 778578b2c..2e80adc13 100644 --- a/core/api-server/socket/action.go +++ b/core/api-server/socket/action.go @@ -100,9 +100,6 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr // filter logs params var mode = "" - var filter = "" - var streamSelector = "" - var logqlPipeline = "" var from = "" var to = "" var timezone = "UTC" @@ -143,29 +140,8 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr } args = append(args, mode) - // check filter - if len(logsAction.Filter) > 0 { - filter = ` |= "` + strings.ReplaceAll(logsAction.Filter, `"`, `\"`) + `"` - } else { - filter = `` - } - - // switch entity - switch logsAction.Entity { - default: - streamSelector = `{node_id=~".+"}` - - case "node": - streamSelector = `{node_id="` + logsAction.EntityName + `"}` - - case "module": - streamSelector = `{module_id="` + logsAction.EntityName + `"}` - } - - logqlPipeline = ` | json syslog_id="SYSLOG_IDENTIFIER", message="MESSAGE" | line_format "[{{.node_id}}:{{.module_id}}:{{.syslog_id}}] {{.message}}"` - // Compose and append the query strings to logcli arguments - args = append(args, streamSelector+logqlPipeline+filter) + args = append(args, buildLogqlQuery(logsAction)) // define command cmd := exec.Command("/usr/local/bin/logcli", args...) @@ -179,33 +155,39 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr if logsAction.Mode == "tail" { // execute command follow mode go func() { - pid := "" + // whatever ends the stream - an error, a clean logcli exit, + // logs-stop - the frontend must leave the follow state + if s != nil { + defer func() { + writeSocketResponse(s, "logs-stop", gin.H{"id": logsAction.Id, "pid": "", "message": "logs follow stopped"}) + }() + } - // create a pipe for the output of the script - stdout, errStdOut := cmd.StdoutPipe() - if errStdOut != nil { + // stdout and stderr share one pipe: what logcli writes about a + // failure is log text like any other, and a single descriptor + // keeps it in order with the lines around it + pipeReader, pipeWriter, errPipe := os.Pipe() + if errPipe != nil { + writeLogsError(s, logsAction.Id, errPipe.Error()) return } - - // create scanner to listen to command outputs - scannerStdOut := bufio.NewScanner(stdout) - go func() { - // foreach command outputs send to websocket - for scannerStdOut.Scan() { - if s != nil { - writeSocketResponse(s, "logs-start", gin.H{"id": logsAction.Id, "pid": pid, "message": scannerStdOut.Text()}) - } else { - fmt.Println(scannerStdOut.Text()) - } - } - }() + cmd.Stdout = pipeWriter + cmd.Stderr = pipeWriter // start command err = cmd.Start() if err != nil { + pipeWriter.Close() + pipeReader.Close() + writeLogsError(s, logsAction.Id, err.Error()) return } + // the child holds its own copy of the write end: drop ours, or + // the reader never reaches EOF + pipeWriter.Close() + defer pipeReader.Close() + if s != nil { // In a Melody session, store the command pid so it // can be killed if connection is closed @@ -221,9 +203,37 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr writeSocketResponse(s, "logs-start", gin.H{"id": logsAction.Id, "pid": strconv.Itoa(cmd.Process.Pid), "message": ""}) } + // read to EOF before Wait: the command output reaches the + // frontend whole, whether the stream ends well or badly + wroteOutput := false + scannerOutput := bufio.NewScanner(pipeReader) + for scannerOutput.Scan() { + wroteOutput = true + if s != nil { + // the pid matters only in the first logs-start + // message, where the frontend picks it up to stop + // the follow later on + writeSocketResponse(s, "logs-start", gin.H{"id": logsAction.Id, "pid": "", "message": scannerOutput.Text()}) + } else { + fmt.Println(scannerOutput.Text()) + } + } + // use Wait to avoid defunct process when killed err = cmd.Wait() if err != nil { + // logs-stop kills the process: that exit is expected + if exitErr, isExitErr := err.(*exec.ExitError); isExitErr { + if status, isStatus := exitErr.Sys().(syscall.WaitStatus); isStatus && status.Signal() == syscall.SIGTERM { + return + } + } + + // a command that said nothing at all still owes the + // frontend a reason for the empty output area + if !wroteOutput { + writeLogsError(s, logsAction.Id, err.Error()) + } return } }() @@ -233,7 +243,12 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr go func() { out, err := cmd.Output() if err != nil { - utils.LogError(errors.Wrap(err, "[SOCKET] error executing Cmd for dump")) + message := err.Error() + if exitErr, isExitErr := err.(*exec.ExitError); isExitErr && len(exitErr.Stderr) > 0 { + message = strings.TrimSpace(string(exitErr.Stderr)) + } + writeLogsError(s, logsAction.Id, message) + return } // reverse logs orders @@ -291,6 +306,53 @@ func Action(socketAction models.SocketAction, s *melody.Session, wg *sync.WaitGr } } +// A failed query is log text like any other: logcli and Loki word it, and the +// frontend prints it in the output area without a case of its own. +func writeLogsError(s *melody.Session, id string, message string) { + if s == nil { + // nothing waits for the WaitGroup of the CLI: the exit ends the process + fmt.Fprintln(os.Stderr, message) + os.Exit(1) + } + + writeSocketResponse(s, "logs-start", gin.H{"id": id, "pid": "", "message": message}) +} + +// LogQL strings use Go escape rules: a raw backslash must be doubled. +func escapeLogqlString(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + return strings.ReplaceAll(s, `"`, `\"`) +} + +func buildLogqlQuery(logsAction models.LogsStartAction) string { + var streamSelector string + + switch logsAction.Entity { + default: + streamSelector = `{node_id=~".+"}` + + case "node": + streamSelector = `{node_id="` + escapeLogqlString(logsAction.EntityName) + `"}` + + case "module": + streamSelector = `{module_id="` + escapeLogqlString(logsAction.EntityName) + `"}` + } + + logqlPipeline := ` | json syslog_id="SYSLOG_IDENTIFIER", message="MESSAGE" | line_format "[{{.node_id}}:{{.module_id}}:{{.syslog_id}}] {{.message}}"` + + filter := "" + if len(logsAction.Filter) > 0 { + operator := `|=` + if logsAction.Regexp { + // a bad pattern is rejected by Loki, which compiles it anyway + operator = `|~` + } + filter = ` ` + operator + ` "` + escapeLogqlString(logsAction.Filter) + `"` + } + + return streamSelector + logqlPipeline + filter +} + func reverse(ss []string) []string { last := len(ss) - 1 for i := 0; i < len(ss)/2; i++ { diff --git a/core/api-server/socket/socket.go b/core/api-server/socket/socket.go index 62770efb3..0fdafdae0 100644 --- a/core/api-server/socket/socket.go +++ b/core/api-server/socket/socket.go @@ -44,6 +44,9 @@ func Instance() *melody.Melody { muClock = new(utils.MuClock) muClock.Sync() socketConnection = melody.New() + // melody defaults to 512 bytes and closes the connection past it: a + // logs-start payload is 280 bytes with an empty search query + socketConnection.Config.MaxMessageSize = 65536 socketConnection.HandleDisconnect(onDisconnect) socketConnection.HandleMessage(onMessage) socketConnection.HandlePong(onPong) diff --git a/core/ui/public/i18n/en/translation.json b/core/ui/public/i18n/en/translation.json index 8330ac4fc..1e4fc6e4a 100644 --- a/core/ui/public/i18n/en/translation.json +++ b/core/ui/public/i18n/en/translation.json @@ -1178,8 +1178,9 @@ "title": "System logs", "start_date": "Start date", "end_date": "End date", - "start_time_label": "Start time (24-hour)", - "end_time_label": "End time (24-hour)", + "start_time_label": "Start time", + "end_time_label": "End time", + "time_format_helper": "24-hour", "start": "Start", "end": "End", "context": "Context", @@ -1187,6 +1188,14 @@ "context_node": "Node", "context_module": "Application", "search_query": "Search query", + "regexp": "Regular expression", + "regexp_helper": "RE2 syntax is accepted. Hint: '(?i)' ignores case, '|' matches any of the words", + "substring_helper": "Exact and case-sensitive substring match", + "websocket_disconnected": "The connection to the server was lost while the query was running. Run the search again.", + "showing_lines": "Showing {n} lines from {from} to {to}", + "showing_lines_of": "Showing {n} of {max} lines from {from} to {to}", + "showing_lines_no_interval": "Showing {n} lines", + "showing_lines_of_no_interval": "Showing {n} of {max} lines", "max_lines": "Max lines", "wrap_text": "Wrap text", "search": "Search", diff --git a/core/ui/src/components/system-logs/LogOutput.vue b/core/ui/src/components/system-logs/LogOutput.vue index 6865ae8fa..8b96ba426 100644 --- a/core/ui/src/components/system-logs/LogOutput.vue +++ b/core/ui/src/components/system-logs/LogOutput.vue @@ -178,9 +178,10 @@ export default { background-image: none; } -// show scrollbar +// show scrollbar, and grow so it lands on the edge, not on the longest line .system-logs .logs-output.bx--snippet--multi .bx--snippet-container { overflow-y: auto !important; + flex: 1 1 auto; } .logs-output { diff --git a/core/ui/src/components/system-logs/LogSearch.vue b/core/ui/src/components/system-logs/LogSearch.vue index 7624c4265..a58cc7f4c 100644 --- a/core/ui/src/components/system-logs/LogSearch.vue +++ b/core/ui/src/components/system-logs/LogSearch.vue @@ -24,7 +24,7 @@ {{ $t("system_logs.context") }} @@ -74,7 +74,7 @@ :auto-highlight="true" :options="apps" :disabled="loadingApps" - class="mg-bottom-md" + class="mg-bottom-lg" key="csbApp" > @@ -90,21 +90,34 @@ ')' " v-model.trim="internalSearchQuery" - :placeholder="$t('common.search_placeholder')" - :helper-text="$t('common.case_sensitive')" + :placeholder="searchPlaceholder" + :helper-text="searchHelperText" @keypress.enter="onEnterKeyPress()" - class="search-query mg-bottom-md" + class="search-query mg-bottom-lg" > + + + + + + - + - - - +
+ + +
+ +
+ {{ $t("system_logs.time_format_helper") }} +
+
+
- - - +
+ + +
+ +
+ {{ $t("system_logs.time_format_helper") }} +
+
+
- + @@ -381,6 +408,9 @@ import { } from "@nethserver/ns8-ui-lib"; import Close16 from "@carbon/icons-vue/es/close/16"; +// leading timestamp emitted by logcli, e.g. 2026-05-15T11:06:07+01:00 +const LOG_TIMESTAMP_PATTERN = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/; + export default { name: "LogSearch", props: { @@ -455,6 +485,11 @@ export default { }, mainSearch: Boolean, followLogs: Boolean, + regexp: Boolean, + filtersShown: { + type: Boolean, + default: true, + }, verticalLayout: Boolean, loadingApps: Boolean, loadingLoki: Boolean, @@ -466,9 +501,9 @@ export default { data() { return { MAX_LINES_LIMIT: 2000, - filtersShown: true, internalContext: "", internalSearchQuery: "", + internalRegexp: false, calOptions: { dateFormat: "Y-m-d", }, @@ -489,6 +524,8 @@ export default { searchStarted: false, noLogsFound: false, highlight: "", + searchedMaxLines: "", + searchedFollowLogs: false, loading: { logs: false, stopFollowing: false, @@ -538,6 +575,58 @@ export default { csbFollowModeSelected() { return this.internalFollowLogs; }, + searchPlaceholder() { + // not translated: log lines are English whatever the UI locale + return this.internalRegexp + ? this.$t("common.eg_value", { value: "(?i)(error|failed)" }) + : this.$t("common.eg_value", { value: "Connect call failed" }); + }, + searchHelperText() { + return this.internalRegexp + ? this.$t("system_logs.regexp_helper") + : this.$t("system_logs.substring_helper"); + }, + logsInterval() { + // dump output is reversed by the backend, so sort instead of assuming + const boundaries = [ + this.findTimestamp(false), + this.findTimestamp(true), + ].filter(Boolean); + + if (!boundaries.length) { + return null; + } + boundaries.sort(); + return { + from: boundaries[0], + to: boundaries[boundaries.length - 1], + }; + }, + resultsFeedback() { + const numLines = this.outputLines.length; + const interval = this.logsInterval; + + if (this.searchedFollowLogs) { + return interval + ? this.$t("system_logs.showing_lines", { + n: numLines, + from: interval.from, + to: interval.to, + }) + : this.$t("system_logs.showing_lines_no_interval", { n: numLines }); + } + return interval + ? this.$t("system_logs.showing_lines_of", { + n: numLines, + max: this.searchedMaxLines, + from: interval.from, + to: interval.to, + }) + : this.$t("system_logs.showing_lines_of_no_interval", { + n: numLines, + max: this.searchedMaxLines, + }); + }, }, watch: { searchQuery: function () { @@ -550,6 +639,26 @@ export default { this.$emit("updateSearchQuery", this.internalSearchQuery); } }, + regexp: function () { + if (this.mainSearch) { + this.internalRegexp = this.regexp; + } + }, + internalRegexp: function () { + if (this.mainSearch) { + this.$emit("updateRegexp", this.internalRegexp); + } + }, + // the reply to a search in flight dies with the connection + isWebsocketConnected: function () { + if ( + this.isWebsocketConnected || + !(this.loading.logs || this.isFollowing) + ) { + return; + } + this.onQueryAborted(this.$t("system_logs.websocket_disconnected")); + }, timezone: function () { if (this.mainSearch) { this.internalTimezone = this.timezone; @@ -697,17 +806,12 @@ export default { this.initFilters(); // register event listeners - this.$root.$on( - `collapseSystemLogsFilters-${this.searchId}`, - this.collapseFilters - ); this.$root.$on("logSearchClosed", this.onLogSearchClosed); }, beforeDestroy() { // remove event listeners this.$root.$off(`logsStart-${this.searchId}`); this.$root.$off(`logsStop-${this.searchId}`); - this.$root.$off(`collapseSystemLogsFilters-${this.searchId}`); this.$root.$off("logSearchClosed"); if (this.pid) { @@ -715,11 +819,19 @@ export default { } }, methods: { - toggleFilters() { - this.filtersShown = !this.filtersShown; - }, - collapseFilters() { - this.filtersShown = false; + // the raw prefix is reused as-is: it already honours the query timezone + findTimestamp(fromEnd) { + const numLines = this.outputLines.length; + + for (let i = 0; i < numLines; i++) { + const line = this.outputLines[fromEnd ? numLines - 1 - i : i]; + const match = LOG_TIMESTAMP_PATTERN.exec(line); + + if (match) { + return `${match[1]} ${match[2]}`; + } + } + return null; }, onContextSelected(value) { this.internalContext = value; @@ -742,6 +854,7 @@ export default { this.internalTimezone = "local"; this.internalMaxLines = "500"; this.internalFollowLogs = false; + this.internalRegexp = false; }, validateSearchLogs() { this.clearErrors(); @@ -820,7 +933,11 @@ export default { const format = "yyyy-MM-dd'T'HH:mm:ssX"; const startUtcString = this.formatInTimeZone(startLocal, format, "UTC"); const endUtcString = this.formatInTimeZone(endLocal, format, "UTC"); - this.highlight = this.internalSearchQuery; + // a pattern written for RE2 is not one the browser engine can locate, so + // the regexp mode leaves the lines unmarked + this.highlight = this.internalRegexp ? "" : this.internalSearchQuery; + this.searchedMaxLines = this.internalMaxLines; + this.searchedFollowLogs = this.internalFollowLogs; this.clearLogs(); let entityName; let timezone = "UTC"; @@ -839,11 +956,15 @@ export default { if (this.internalFollowLogs) { this.$root.$on(`logsStart-${this.searchId}`, this.onLogsStartFollow); + // the stream also ends without us asking: a logcli failure, a clean exit + this.$root.$once(`logsStop-${this.searchId}`, this.onLogsStop); } else { this.$root.$once(`logsStart-${this.searchId}`, this.onLogsStartDump); } - if (this.internalTimezone == "local") { + // follow mode hides the timezone field, so a UTC left over from a previous + // dump search must not keep driving the timestamps of live lines + if (this.internalFollowLogs || this.internalTimezone == "local") { timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } @@ -860,6 +981,7 @@ export default { id: this.searchId, timezone: timezone, instance: this.internalSelectedLokiId, + regexp: this.internalRegexp, }, }; @@ -880,9 +1002,11 @@ export default { this.$socket.sendObj(logsStopObj); }, onLogsStop() { + this.loading.logs = false; this.loading.stopFollowing = false; this.isFollowing = false; this.pid = ""; + this.$root.$off(`logsStart-${this.searchId}`); }, onLogsStartDump(payload) { this.loading.logs = false; @@ -897,6 +1021,18 @@ export default { // signal LogOutput this.$root.$emit(`logsUpdated-${this.searchId}`); }, + // a query dying in the browser reads like one dying on the server: the + // reason is printed where the logs are + onQueryAborted(message) { + this.loading.logs = false; + this.loading.stopFollowing = false; + this.isFollowing = false; + this.pid = ""; + this.noLogsFound = false; + this.outputLines = [...this.outputLines, message]; + this.$root.$off(`logsStart-${this.searchId}`); + this.$root.$emit(`logsUpdated-${this.searchId}`); + }, onLogsStartFollow(payload) { if (this.loading.logs) { this.loading.logs = false; @@ -967,13 +1103,16 @@ export default { position: relative; } -.interval-date { - display: inline-flex; - margin-right: $spacing-06; +// one Carbon gutter apart, so the pair lines up with the single fields above +.interval-group { + display: flex; + gap: $spacing-07; } +.interval-date, .interval-time { - display: inline-flex; + flex: 1 1 0; + min-width: 0; } .checkbox-filter { @@ -981,8 +1120,10 @@ export default { align-items: center; } -.toggle-filters { - margin-bottom: $spacing-06; +.results-feedback { + margin-left: auto; + color: $text-02; + font-size: 0.875rem; } .filter-collapsed { @@ -997,6 +1138,9 @@ export default { display: flex; flex-wrap: wrap; align-items: center; + // tops up the buttons' mg-bottom-sm to the rhythm of the filter rows + margin-top: $spacing-03; + margin-bottom: $spacing-06; .mg-right { margin-right: $spacing-06; @@ -1009,10 +1153,40 @@ export default { // global styles +// Carbon caps fields at 38rem and both pickers ship a fixed pixel width @media (min-width: $breakpoint-medium) { - .system-logs .search-query .bx--text-input, - .system-logs .search-query .bx--text-input__field-wrapper { + .system-logs .log-search .bx--text-input, + .system-logs .log-search .bx--text-input__field-wrapper, + .system-logs .log-search .cv-select, + .system-logs .log-search .cv-combo-box { max-width: none; } } + +.system-logs .interval-date, +.system-logs .interval-date .bx--date-picker, +.system-logs .interval-date .bx--date-picker-container, +.system-logs .interval-date .bx--date-picker__input { + width: 100%; +} + +.system-logs .interval-time .bx--time-picker, +.system-logs .interval-time .bx--time-picker__input { + width: 100%; +} + +// the picker's scoped rule ties at (0,4,0) and is injected last, so only +// !important gets past it +.system-logs .interval-time .time-picker-field.narrow-width { + width: 100% !important; +} + +.system-logs .interval-time .time-picker-field.narrow-width input { + width: 100% !important; +} + +// Carbon sizes the content switcher on its labels; Figma gives it the column +.system-logs .log-search .bx--content-switcher { + width: 100%; +} diff --git a/core/ui/src/views/SystemLogs.vue b/core/ui/src/views/SystemLogs.vue index dfa809acd..2ee0cb4af 100644 --- a/core/ui/src/views/SystemLogs.vue +++ b/core/ui/src/views/SystemLogs.vue @@ -5,10 +5,12 @@